@@ -11,14 +11,41 @@ import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective
11
11
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Directive
12
12
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart
13
13
import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan
14
+ import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults
14
15
import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType
15
16
import gov.nasa.jpl.aerie.scheduler.DirectiveIdGenerator
16
17
import gov.nasa.jpl.aerie.scheduler.model.*
17
18
import gov.nasa.jpl.aerie.types.ActivityDirectiveId
19
+ import java.lang.ref.WeakReference
18
20
import java.time.Instant
19
21
import kotlin.jvm.optionals.getOrNull
20
22
import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults as TimelineSimResults
21
23
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
+ */
22
49
data class InMemoryEditablePlan (
23
50
private val missionModel : MissionModel <* >,
24
51
private var idGenerator : DirectiveIdGenerator ,
@@ -27,16 +54,39 @@ data class InMemoryEditablePlan(
27
54
private val lookupActivityType : (String ) -> ActivityType
28
55
) : EditablePlan, Plan by plan {
29
56
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 ())
31
71
var uncommittedChanges = mutableListOf<Edit >()
32
72
private set
33
73
34
74
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
36
82
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
+ }
40
90
41
91
override fun create (directive : NewDirective ): ActivityDirectiveId {
42
92
class ParentSearchException (id : ActivityDirectiveId , size : Int ): Exception(" Expected one parent activity with id $id , found $size " )
@@ -55,16 +105,33 @@ data class InMemoryEditablePlan(
55
105
uncommittedChanges.add(Edit .Create (resolved))
56
106
resolved.validateArguments(lookupActivityType)
57
107
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
+
58
115
return id
59
116
}
60
117
61
118
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
63
125
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)
65
129
}
66
130
67
131
override fun rollback (): List <Edit > {
132
+ // Early return if there are no changes, to keep staleness accuracy
133
+ if (uncommittedChanges.isEmpty()) return emptyList()
134
+
68
135
val result = uncommittedChanges
69
136
uncommittedChanges = mutableListOf ()
70
137
for (edit in result) {
@@ -74,6 +141,13 @@ data class InMemoryEditablePlan(
74
141
}
75
142
}
76
143
}
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
77
151
return result
78
152
}
79
153
0 commit comments