Skip to content

Commit f271123

Browse files
authored
Merge pull request #1595 from NASA-AMMOS/feat/sim-result-staleness
Simulation result staleness checking
2 parents ed4969e + 3ba9566 commit f271123

File tree

4 files changed

+229
-15
lines changed

4 files changed

+229
-15
lines changed

scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/Commit.kt

-7
This file was deleted.

scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/InMemoryEditablePlan.kt

+81-7
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,41 @@ import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective
1111
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Directive
1212
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart
1313
import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan
14+
import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults
1415
import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType
1516
import gov.nasa.jpl.aerie.scheduler.DirectiveIdGenerator
1617
import gov.nasa.jpl.aerie.scheduler.model.*
1718
import gov.nasa.jpl.aerie.types.ActivityDirectiveId
19+
import java.lang.ref.WeakReference
1820
import java.time.Instant
1921
import kotlin.jvm.optionals.getOrNull
2022
import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults as TimelineSimResults
2123

24+
/*
25+
* An implementation of [EditablePlan] that stores the plan in memory for use in the internal scheduler.
26+
*
27+
* ## Staleness checking
28+
*
29+
* The editable plan instance keeps track of sim results that it has produced using weak references, and can dynamically
30+
* update their staleness if the plan is changed after it was simulated. The process is this:
31+
*
32+
* 1. [InMemoryEditablePlan] has a set of weak references to simulation results objects that are currently up-to-date.
33+
* I used weak references because if the user can't access it anymore, staleness doesn't matter and we might as well
34+
* let it get gc'ed.
35+
* 2. When the user gets simulation results, either through simulation or by getting the latest, it always checks for
36+
* plan equality between the returned results and the current plan, even if we just simulated. If it is up-to-date, a
37+
* weak ref is added to the set.
38+
* 3. When an edit is made, the sim results in the current set are marked stale; then the set is reset to new reference
39+
* to an empty set.
40+
* 4. When a commit is made, the commit object takes *shared ownership* of the set. If a new simulation is run (step 2)
41+
* the plan can still add to the set while it is still jointly owned by the commit. Then when an edit is made (step 3)
42+
* the commit will become the sole owner of the set.
43+
* 5. When changes are rolled back, any sim results currently in the plan's set are marked stale, the previous commit's
44+
* sim results are marked not stale, then the plan will resume joint ownership of the previous commit's set.
45+
*
46+
* The joint ownership freaks me out a wee bit, but I think it's safe because the commits are only used to keep the
47+
* previous sets from getting gc'ed in the event of a rollback. Only the plan object actually mutates the set.
48+
*/
2249
data class InMemoryEditablePlan(
2350
private val missionModel: MissionModel<*>,
2451
private var idGenerator: DirectiveIdGenerator,
@@ -27,16 +54,39 @@ data class InMemoryEditablePlan(
2754
private val lookupActivityType: (String) -> ActivityType
2855
) : EditablePlan, Plan by plan {
2956

30-
private val commits = mutableListOf<Commit>()
57+
private data class Commit(
58+
val diff: List<Edit>,
59+
60+
/**
61+
* A record of the simulation results objects that were up-to-date when the commit
62+
* was created.
63+
*
64+
* This has SHARED OWNERSHIP with [InMemoryEditablePlan]; the editable plan may add more to
65+
* this list AFTER the commit is created.
66+
*/
67+
val upToDateSimResultsSet: MutableSet<WeakReference<MerlinToProcedureSimulationResultsAdapter>>
68+
)
69+
70+
private var committedChanges = Commit(listOf(), mutableSetOf())
3171
var uncommittedChanges = mutableListOf<Edit>()
3272
private set
3373

3474
val totalDiff: List<Edit>
35-
get() = commits.flatMap { it.diff }
75+
get() = committedChanges.diff
76+
77+
// Jointly owned set of up-to-date simulation results. See class-level comment for algorithm explanation.
78+
private var upToDateSimResultsSet: MutableSet<WeakReference<MerlinToProcedureSimulationResultsAdapter>> = mutableSetOf()
79+
80+
override fun latestResults(): SimulationResults? {
81+
val merlinResults = simulationFacade.latestSimulationData.getOrNull() ?: return null
3682

37-
override fun latestResults() =
38-
simulationFacade.latestSimulationData.getOrNull()
39-
?.let { MerlinToProcedureSimulationResultsAdapter(it.driverResults, false, plan) }
83+
// kotlin checks structural equality by default, not referential equality.
84+
val isStale = merlinResults.plan.activities != plan.activities
85+
86+
val results = MerlinToProcedureSimulationResultsAdapter(merlinResults.driverResults, isStale, plan)
87+
if (!isStale) upToDateSimResultsSet.add(WeakReference(results))
88+
return results
89+
}
4090

4191
override fun create(directive: NewDirective): ActivityDirectiveId {
4292
class ParentSearchException(id: ActivityDirectiveId, size: Int): Exception("Expected one parent activity with id $id, found $size")
@@ -55,16 +105,33 @@ data class InMemoryEditablePlan(
55105
uncommittedChanges.add(Edit.Create(resolved))
56106
resolved.validateArguments(lookupActivityType)
57107
plan.add(resolved.toSchedulingActivity(lookupActivityType, true))
108+
109+
for (simResults in upToDateSimResultsSet) {
110+
simResults.get()?.stale = true
111+
}
112+
// create a new list instead of `.clear` because commit objects have the same reference
113+
upToDateSimResultsSet = mutableSetOf()
114+
58115
return id
59116
}
60117

61118
override fun commit() {
62-
val committedEdits = uncommittedChanges
119+
// Early return if there are no changes. This prevents multiple commits from sharing ownership of the set,
120+
// because new sets are only created when edits are made.
121+
// Probably unnecessary, but shared ownership freaks me out enough already.
122+
if (uncommittedChanges.isEmpty()) return
123+
124+
val newCommittedChanges = uncommittedChanges
63125
uncommittedChanges = mutableListOf()
64-
commits.add(Commit(committedEdits))
126+
127+
// Create a commit that shares ownership of the simResults set.
128+
committedChanges = Commit(committedChanges.diff + newCommittedChanges, upToDateSimResultsSet)
65129
}
66130

67131
override fun rollback(): List<Edit> {
132+
// Early return if there are no changes, to keep staleness accuracy
133+
if (uncommittedChanges.isEmpty()) return emptyList()
134+
68135
val result = uncommittedChanges
69136
uncommittedChanges = mutableListOf()
70137
for (edit in result) {
@@ -74,6 +141,13 @@ data class InMemoryEditablePlan(
74141
}
75142
}
76143
}
144+
for (simResult in upToDateSimResultsSet) {
145+
simResult.get()?.stale = true
146+
}
147+
for (simResult in committedChanges.upToDateSimResultsSet) {
148+
simResult.get()?.stale = false
149+
}
150+
upToDateSimResultsSet = committedChanges.upToDateSimResultsSet
77151
return result
78152
}
79153

scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import kotlin.jvm.optionals.getOrNull
1818

1919
class MerlinToProcedureSimulationResultsAdapter(
2020
private val results: gov.nasa.jpl.aerie.merlin.driver.SimulationResults,
21-
private val stale: Boolean,
21+
var stale: Boolean,
2222
private val plan: Plan
2323
): SimulationResults {
2424

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package gov.nasa.jpl.aerie.scheduler;
2+
3+
import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan;
4+
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart;
5+
import gov.nasa.jpl.aerie.merlin.driver.MissionModel;
6+
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;
7+
import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue;
8+
import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory;
9+
import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon;
10+
import gov.nasa.jpl.aerie.scheduler.model.Problem;
11+
import gov.nasa.jpl.aerie.scheduler.plan.InMemoryEditablePlan;
12+
import gov.nasa.jpl.aerie.scheduler.plan.SchedulerToProcedurePlanAdapter;
13+
import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade;
14+
import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade;
15+
import org.junit.jupiter.api.AfterEach;
16+
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.Test;
18+
19+
import java.time.Instant;
20+
import java.util.Map;
21+
22+
import static org.junit.jupiter.api.Assertions.assertFalse;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
24+
25+
public class EditablePlanStalenessTest {
26+
27+
MissionModel<?> missionModel;
28+
Problem problem;
29+
SimulationFacade facade;
30+
EditablePlan plan;
31+
32+
private static final Instant start = TestUtility.timeFromEpochMillis(0);
33+
private static final Instant end = TestUtility.timeFromEpochDays(1);
34+
35+
private static final PlanningHorizon horizon = new PlanningHorizon(start, end);
36+
37+
@BeforeEach
38+
public void setUp() {
39+
missionModel = SimulationUtility.getBananaMissionModel();
40+
final var schedulerModel = SimulationUtility.getBananaSchedulerModel();
41+
facade = new CheckpointSimulationFacade(horizon, missionModel, schedulerModel);
42+
problem = new Problem(missionModel, horizon, facade, schedulerModel);
43+
plan = new InMemoryEditablePlan(
44+
missionModel,
45+
new DirectiveIdGenerator(0),
46+
new SchedulerToProcedurePlanAdapter(
47+
new PlanInMemory(
48+
49+
),
50+
horizon,
51+
Map.of(), Map.of(), Map.of()
52+
),
53+
facade,
54+
problem::getActivityType
55+
);
56+
}
57+
58+
@AfterEach
59+
public void tearDown() {
60+
missionModel = null;
61+
problem = null;
62+
facade = null;
63+
plan = null;
64+
}
65+
66+
@Test
67+
public void simResultMarkedStale() {
68+
plan.create(
69+
"BiteBanana",
70+
new DirectiveStart.Absolute(Duration.MINUTE),
71+
Map.of("biteSize", SerializedValue.of(1))
72+
);
73+
74+
final var simResults = plan.simulate();
75+
76+
assertFalse(simResults.isStale());
77+
78+
plan.create(
79+
"GrowBanana",
80+
new DirectiveStart.Absolute(Duration.HOUR),
81+
Map.of(
82+
"growingDuration", SerializedValue.of(10),
83+
"quantity", SerializedValue.of(1)
84+
)
85+
);
86+
87+
assertTrue(simResults.isStale());
88+
}
89+
90+
@Test
91+
public void simResultMarkedNotStaleAfterRollback_CommitThenSimulate() {
92+
plan.create(
93+
"BiteBanana",
94+
new DirectiveStart.Absolute(Duration.MINUTE),
95+
Map.of("biteSize", SerializedValue.of(1))
96+
);
97+
98+
plan.commit();
99+
final var simResults = plan.simulate();
100+
101+
assertFalse(simResults.isStale());
102+
103+
plan.create(
104+
"GrowBanana",
105+
new DirectiveStart.Absolute(Duration.HOUR),
106+
Map.of(
107+
"growingDuration", SerializedValue.of(10),
108+
"quantity", SerializedValue.of(1)
109+
)
110+
);
111+
112+
assertTrue(simResults.isStale());
113+
114+
plan.rollback();
115+
116+
assertFalse(simResults.isStale());
117+
}
118+
119+
@Test
120+
public void simResultMarkedNotStaleAfterRollback_SimulateThenCommit() {
121+
plan.create(
122+
"BiteBanana",
123+
new DirectiveStart.Absolute(Duration.MINUTE),
124+
Map.of("biteSize", SerializedValue.of(1))
125+
);
126+
127+
final var simResults = plan.simulate();
128+
plan.commit();
129+
130+
assertFalse(simResults.isStale());
131+
132+
plan.create(
133+
"GrowBanana",
134+
new DirectiveStart.Absolute(Duration.HOUR),
135+
Map.of(
136+
"growingDuration", SerializedValue.of(10),
137+
"quantity", SerializedValue.of(1)
138+
)
139+
);
140+
141+
assertTrue(simResults.isStale());
142+
143+
plan.rollback();
144+
145+
assertFalse(simResults.isStale());
146+
}
147+
}

0 commit comments

Comments
 (0)