diff --git a/build.gradle b/build.gradle index 41c2ab8b0b..5e0676d0a3 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,7 @@ configure(subprojects.findAll { it.projectDir.toPath().resolve('sql').toFile().e // Remove distributed SQL as part of `clean` task task undoDistributeSql(type: Delete) { - doLast { // Explicity do last to avoid running during configuration step + doLast { // Explicitly do last to avoid running during configuration step file("${sp.projectDir}/sql").list().each { delete "$rootDir/deployment/postgres-init-db/sql/$it" } diff --git a/constraints/build.gradle b/constraints/build.gradle index 8a219a80a4..5daa876e15 100644 --- a/constraints/build.gradle +++ b/constraints/build.gradle @@ -12,6 +12,9 @@ java { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/contrib/build.gradle b/contrib/build.gradle index ebb6146fb6..673231ed0f 100644 --- a/contrib/build.gradle +++ b/contrib/build.gradle @@ -15,6 +15,9 @@ java { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/db-tests/build.gradle b/db-tests/build.gradle index 24d3960f58..c4420085f9 100644 --- a/db-tests/build.gradle +++ b/db-tests/build.gradle @@ -18,6 +18,9 @@ jacocoTestReport { task e2eTest(type: Test) { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } dependencies { diff --git a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql index 86b0a91888..7d7f7900da 100644 --- a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql +++ b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql @@ -6,6 +6,7 @@ RESTORE ORIGINAL */ create table scheduling_goal ( id integer generated always as identity, + old_id integer, revision integer not null default 0, name text not null, definition text not null, @@ -59,12 +60,85 @@ create trigger update_logging_on_update_scheduling_goal_trigger when (pg_trigger_depth() < 1) execute function update_logging_on_update_scheduling_goal(); +/* +ANALYSIS TABLES +*/ +/* Dropped FKs before data migration first */ +alter table scheduling_goal_analysis_satisfying_activities + drop constraint satisfying_activities_references_scheduling_goal; + +alter table scheduling_goal_analysis_created_activities + drop constraint created_activities_references_scheduling_goal; + +alter table scheduling_goal_analysis + drop constraint scheduling_goal_analysis_references_scheduling_goal; + +alter table scheduling_specification_goals + drop constraint scheduling_spec_goal_definition_exists, + drop constraint scheduling_spec_goal_exists; + +alter table metadata.scheduling_goal_tags +drop constraint scheduling_goal_tags_goal_id_fkey; + +/* +DATA MIGRATION +*/ +-- Goals not on a model spec will not be kept, as the scheduler DB can't get the model id from the plan id +-- Because multiple spec may be using the same goal/goal definition, we have to regenerate the id +with specified_definition(goal_id, goal_revision, model_id, definition, definition_creation) as ( + select gd.goal_id, gd.revision, s.model_id, gd.definition, gd.created_at + from scheduling_model_specification_goals s + left join scheduling_goal_definition gd using (goal_id) + where ((s.goal_revision is not null and s.goal_revision = gd.revision) + or (s.goal_revision is null and gd.revision = (select def.revision + from scheduling_goal_definition def + where def.goal_id = s.goal_id + order by def.revision desc limit 1)))), + new_goal_ids(old_goal_id, new_goal_id) as ( + insert into scheduling_goal(old_id, revision, name, definition, model_id, description, + author, last_modified_by, created_date, modified_date) + select m.id, sd.goal_revision, m.name, sd.definition, sd.model_id, m.description, + m.owner, m.updated_by, m.created_at, greatest(m.updated_at::timestamptz, sd.definition_creation::timestamptz) + from scheduling_goal_metadata m + inner join specified_definition sd on m.id = sd.goal_id + returning old_id, id), + satisfying_acts as ( + update scheduling_goal_analysis_satisfying_activities + set goal_id = ngi.new_goal_id + from new_goal_ids ngi + where goal_id = ngi.old_goal_id), + create_acts as ( + update scheduling_goal_analysis_created_activities + set goal_id = ngi.new_goal_id + from new_goal_ids ngi + where goal_id = ngi.old_goal_id), + analysis as ( + update scheduling_goal_analysis + set goal_id = ngi.new_goal_id + from new_goal_ids ngi + where goal_id = ngi.old_goal_id), + tags as ( + update metadata.scheduling_goal_tags + set goal_id = ngi.new_goal_id + from new_goal_ids ngi + where goal_id = ngi.old_goal_id) + update scheduling_specification_goals + set goal_id = ngi.new_goal_id + from new_goal_ids ngi + where goal_id = ngi.old_goal_id; + +/* +POST DATA MIGRATION TABLE CHANGES +*/ +alter table scheduling_goal drop column old_id; +drop trigger set_timestamp on scheduling_goal_metadata; +drop function scheduling_goal_metadata_set_updated_at(); + /* ANALYSIS TABLES */ /* Dropped FKs are restored first */ alter table scheduling_goal_analysis_satisfying_activities - drop constraint satisfying_activities_references_scheduling_goal, add constraint satisfying_activities_references_scheduling_goal foreign key (goal_id) references scheduling_goal @@ -76,7 +150,6 @@ alter table scheduling_goal_analysis_satisfying_activities drop column goal_revision; alter table scheduling_goal_analysis_created_activities - drop constraint created_activities_references_scheduling_goal, add constraint created_activities_references_scheduling_goal foreign key (goal_id) references scheduling_goal @@ -88,7 +161,6 @@ alter table scheduling_goal_analysis_created_activities drop column goal_revision; alter table scheduling_goal_analysis - drop constraint scheduling_goal_analysis_references_scheduling_goal, add constraint scheduling_goal_analysis_references_scheduling_goal foreign key (goal_id) references scheduling_goal @@ -170,33 +242,6 @@ alter table scheduling_goal_analysis on update cascade on delete cascade; -/* -DATA MIGRATION -*/ --- Goals not on a model spec will not be kept, as the scheduler DB can't get the model id from the plan id --- Because multiple spec may be using the same goal/goal definition, we have to regenerate the id -with specified_definition(goal_id, goal_revision, model_id, definition, definition_creation) as ( - select gd.goal_id, gd.revision, s.model_id, gd.definition, gd.created_at - from scheduling_model_specification_goals s - left join scheduling_goal_definition gd using (goal_id) - where ((s.goal_revision is not null and s.goal_revision = gd.revision) - or (s.goal_revision is null and gd.revision = (select def.revision - from scheduling_goal_definition def - where def.goal_id = s.goal_id - order by def.revision desc limit 1))) -) -insert into scheduling_goal(revision, name, definition, model_id, description, - author, last_modified_by, created_date, modified_date) -select sd.goal_revision, m.name, sd.definition, sd.model_id, m.description, - m.owner, m.updated_by, m.created_at, greatest(m.updated_at::timestamptz, sd.definition_creation::timestamptz) - from scheduling_goal_metadata m - inner join specified_definition sd on m.id = sd.goal_id; -/* -POST DATA MIGRATION TABLE CHANGES -*/ -drop trigger set_timestamp on scheduling_goal_metadata; -drop function scheduling_goal_metadata_set_updated_at(); - /* SCHEDULING SPECIFICATION */ @@ -287,8 +332,6 @@ language plpgsql; alter table scheduling_specification_goals add constraint scheduling_specification_unique_goal_id unique (goal_id), - drop constraint scheduling_spec_goal_definition_exists, - drop constraint scheduling_spec_goal_exists, add constraint scheduling_specification_goals_references_scheduling_goals foreign key (goal_id) references scheduling_goal @@ -330,7 +373,6 @@ TAGS */ drop table metadata.scheduling_goal_definition_tags; alter table metadata.scheduling_goal_tags -drop constraint scheduling_goal_tags_goal_id_fkey, add foreign key (goal_id) references public.scheduling_goal on update cascade on delete cascade; diff --git a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql index e2e8af38f9..6c9044b940 100644 --- a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql +++ b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql @@ -160,18 +160,7 @@ alter table scheduling_specification_conditions foreign key (specification_id) references scheduling_specification on update cascade - on delete cascade, - drop constraint scheduling_specification_conditions_references_scheduling_conditions, - add constraint scheduling_specification_condition_exists - foreign key (condition_id) - references scheduling_condition_metadata - on update cascade - on delete restrict, - add constraint scheduling_specification_condition_definition_exists - foreign key (condition_id, condition_revision) - references scheduling_condition_definition - on update cascade - on delete restrict; + on delete cascade; comment on table scheduling_specification_conditions is e'' 'The set of scheduling conditions to be used on a given plan.'; @@ -252,6 +241,19 @@ before update on scheduling_condition_metadata for each row execute function scheduling_condition_metadata_set_updated_at(); +alter table scheduling_specification_conditions + drop constraint scheduling_specification_conditions_references_scheduling_conditions, + add constraint scheduling_specification_condition_exists + foreign key (condition_id) + references scheduling_condition_metadata + on update cascade + on delete restrict, + add constraint scheduling_specification_condition_definition_exists + foreign key (condition_id, condition_revision) + references scheduling_condition_definition + on update cascade + on delete restrict; + /* DROP ORIGINAL */ @@ -360,29 +362,6 @@ create trigger scheduling_goal_definition_set_revision for each row execute function scheduling_goal_definition_set_revision(); - -/* -TAGS -*/ -alter table metadata.scheduling_goal_tags -drop constraint scheduling_goal_tags_goal_id_fkey, -add foreign key (goal_id) references public.scheduling_goal_metadata - on update cascade - on delete cascade; - -create table metadata.scheduling_goal_definition_tags ( - goal_id integer not null, - goal_revision integer not null, - tag_id integer not null, - primary key (goal_id, goal_revision, tag_id), - foreign key (goal_id, goal_revision) references scheduling_goal_definition - on update cascade - on delete cascade -); - -comment on table metadata.scheduling_goal_definition_tags is e'' - 'The tags associated with a specific scheduling condition definition.'; - /* SPECIFICATIONS */ @@ -538,17 +517,6 @@ alter table scheduling_specification_goals references scheduling_specification on update cascade on delete cascade, - drop constraint scheduling_specification_goals_references_scheduling_goals, - add constraint scheduling_spec_goal_exists - foreign key (goal_id) - references scheduling_goal_metadata - on update cascade - on delete restrict, - add constraint scheduling_spec_goal_definition_exists - foreign key (goal_id, goal_revision) - references scheduling_goal_definition - on update cascade - on delete restrict, drop constraint scheduling_specification_unique_goal_id; comment on table scheduling_specification_goals is e'' @@ -705,6 +673,19 @@ POST DATA MIGRATION TABLE CHANGES alter table scheduling_goal_metadata alter column id set generated always; +alter table scheduling_specification_goals + drop constraint scheduling_specification_goals_references_scheduling_goals, + add constraint scheduling_spec_goal_exists + foreign key (goal_id) + references scheduling_goal_metadata + on update cascade + on delete restrict, + add constraint scheduling_spec_goal_definition_exists + foreign key (goal_id, goal_revision) + references scheduling_goal_definition + on update cascade + on delete restrict; + create function scheduling_goal_metadata_set_updated_at() returns trigger security definer @@ -718,6 +699,28 @@ before update on scheduling_goal_metadata for each row execute function scheduling_goal_metadata_set_updated_at(); +/* +TAGS +*/ +alter table metadata.scheduling_goal_tags +drop constraint scheduling_goal_tags_goal_id_fkey, +add foreign key (goal_id) references public.scheduling_goal_metadata + on update cascade + on delete cascade; + +create table metadata.scheduling_goal_definition_tags ( + goal_id integer not null, + goal_revision integer not null, + tag_id integer not null, + primary key (goal_id, goal_revision, tag_id), + foreign key (goal_id, goal_revision) references scheduling_goal_definition + on update cascade + on delete cascade +); + +comment on table metadata.scheduling_goal_definition_tags is e'' + 'The tags associated with a specific scheduling condition definition.'; + /* SCHEDULING REQUEST */ diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index 33d5e47d01..68e0a9392e 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -43,10 +43,14 @@ task e2eTest(type: Test) { // Run the tests in parallel to improve performance maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 useJUnitPlatform() + + testLogging { + exceptionFormat = 'full' + } } dependencies { - testImplementation project(":timeline") + testImplementation project(":procedural:timeline") testImplementation "com.zaxxer:HikariCP:5.1.0" testImplementation("org.postgresql:postgresql:42.6.0") testImplementation project(':merlin-driver') diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java index 2955842836..ada11145be 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java @@ -736,7 +736,7 @@ void beforeEach() throws IOException, InterruptedException { // Insert the Plan fooPlan = hasura.createPlan( fooId, - "Foo Plan - Simulation Tests", + "Foo Plan - Scheduling Tests", "720:00:00", planStartTimestamp); diff --git a/examples/banananation/build.gradle b/examples/banananation/build.gradle index 402c81ca9e..3637a0ecd4 100644 --- a/examples/banananation/build.gradle +++ b/examples/banananation/build.gradle @@ -12,6 +12,9 @@ java { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/examples/config-with-defaults/build.gradle b/examples/config-with-defaults/build.gradle index 2fa0e7e215..1e745a759f 100644 --- a/examples/config-with-defaults/build.gradle +++ b/examples/config-with-defaults/build.gradle @@ -19,6 +19,9 @@ jar { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/examples/config-without-defaults/build.gradle b/examples/config-without-defaults/build.gradle index 2fa0e7e215..1e745a759f 100644 --- a/examples/config-without-defaults/build.gradle +++ b/examples/config-without-defaults/build.gradle @@ -19,6 +19,9 @@ jar { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/examples/foo-missionmodel/build.gradle b/examples/foo-missionmodel/build.gradle index 2fa0e7e215..1e745a759f 100644 --- a/examples/foo-missionmodel/build.gradle +++ b/examples/foo-missionmodel/build.gradle @@ -19,6 +19,9 @@ jar { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/examples/minimal-mission-model/build.gradle b/examples/minimal-mission-model/build.gradle index 2fa0e7e215..1e745a759f 100644 --- a/examples/minimal-mission-model/build.gradle +++ b/examples/minimal-mission-model/build.gradle @@ -19,6 +19,9 @@ jar { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/examples/streamline-demo/build.gradle b/examples/streamline-demo/build.gradle index b0679446c5..48afd9f8c3 100644 --- a/examples/streamline-demo/build.gradle +++ b/examples/streamline-demo/build.gradle @@ -19,6 +19,9 @@ jar { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/merlin-driver/build.gradle b/merlin-driver/build.gradle index 76cbca76bb..acc07bd493 100644 --- a/merlin-driver/build.gradle +++ b/merlin-driver/build.gradle @@ -17,6 +17,9 @@ test { useJUnitPlatform { includeEngines 'jqwik', 'junit-jupiter' } + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java index d53ee219a4..03ae26c4ee 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java @@ -5,6 +5,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; import gov.nasa.jpl.aerie.merlin.protocol.model.Resource; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; @@ -55,7 +56,7 @@ public TaskFactory getTaskFactory(final SerializedActivity specification) thr public TaskFactory getDaemon() { return executor -> scheduler -> { - MissionModel.this.daemons.forEach(scheduler::spawn); + MissionModel.this.daemons.forEach($ -> scheduler.spawn(InSpan.Fresh, $)); return TaskStatus.completed(Unit.UNIT); }; } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java index 9608639f2d..d68be6f4d1 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; @@ -165,11 +166,11 @@ void simulateTask(final MissionModel missionModel, final TaskFactory TaskFactory makeTaskFactory( ) { // Emit the current activity (defined by directiveId) - return executor -> scheduler0 -> TaskStatus.calling((TaskFactory) (executor1 -> scheduler1 -> { + return executor -> scheduler0 -> TaskStatus.calling(InSpan.Fresh, (TaskFactory) (executor1 -> scheduler1 -> { scheduler1.emit(directiveId, activityTopic); return task.create(executor1).step(scheduler1); }), scheduler2 -> { @@ -240,7 +241,7 @@ private static TaskFactory makeTaskFactory( final List> dependents = resolved.get(directiveId) == null ? List.of() : resolved.get(directiveId); // Iterate over the dependents for (final var dependent : dependents) { - scheduler2.spawn(executor2 -> scheduler3 -> + scheduler2.spawn(InSpan.Parent, executor2 -> scheduler3 -> // Delay until the dependent starts TaskStatus.delayed(dependent.getRight(), scheduler4 -> { final var dependentDirectiveId = dependent.getLeft(); @@ -258,7 +259,7 @@ private static TaskFactory makeTaskFactory( // Schedule the dependent // When it finishes, it will schedule the activities depending on it to know their start time - scheduler4.spawn(makeTaskFactory( + scheduler4.spawn(InSpan.Parent, makeTaskFactory( dependentDirectiveId, dependantTask, schedule, diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SignalId.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SignalId.java deleted file mode 100644 index da6057ea63..0000000000 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SignalId.java +++ /dev/null @@ -1,18 +0,0 @@ -package gov.nasa.jpl.aerie.merlin.driver.engine; - -/** A typed wrapper for signal IDs. */ -public sealed interface SignalId { - /** A signal controlled by a task. */ - record TaskSignalId(TaskId id) implements SignalId {} - - /** A signal controlled by a condition. */ - record ConditionSignalId(ConditionId id) implements SignalId {} - - static TaskSignalId forTask(final TaskId task) { - return new TaskSignalId(task); - } - - static ConditionSignalId forCondition(final ConditionId condition) { - return new ConditionSignalId(condition); - } -} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index efbddb54f2..fc79081af7 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -21,10 +21,12 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import org.apache.commons.lang3.mutable.MutableInt; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; @@ -52,8 +54,10 @@ public final class SimulationEngine implements AutoCloseable { /** The set of all jobs waiting for time to pass. */ private final JobSchedule scheduledJobs = new JobSchedule<>(); - /** The set of all jobs waiting on a given signal. */ - private final Subscriptions waitingTasks = new Subscriptions<>(); + /** The set of all jobs waiting on a condition. */ + private final Map waitingTasks = new HashMap<>(); + /** The set of all tasks blocked on some number of subtasks. */ + private final Map blockedTasks = new HashMap<>(); /** The set of conditions depending on a given set of topics. */ private final Subscriptions, ConditionId> waitingConditions = new Subscriptions<>(); /** The set of queries depending on a given set of topics. */ @@ -66,23 +70,27 @@ public final class SimulationEngine implements AutoCloseable { /** The profiling state for each tracked resource. */ private final Map> resources = new HashMap<>(); - /** The task that spawned a given task (if any). */ - private final Map taskParent = new HashMap<>(); - /** The set of children for each task (if any). */ - @DerivedFrom("taskParent") - private final Map> taskChildren = new HashMap<>(); + /** The set of all spans of work contributed to by modeled tasks. */ + private final Map spans = new HashMap<>(); + /** A count of the direct contributors to each span, including child spans and tasks. */ + private final Map spanContributorCount = new HashMap<>(); /** A thread pool that modeled tasks can use to keep track of their state between steps. */ private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); /** Schedule a new task to be performed at the given time. */ - public TaskId scheduleTask(final Duration startTime, final TaskFactory state) { + public SpanId scheduleTask(final Duration startTime, final TaskFactory state) { if (startTime.isNegative()) throw new IllegalArgumentException("Cannot schedule a task before the start time of the simulation"); + final var span = SpanId.generate(); + this.spans.put(span, new Span(Optional.empty(), startTime, Optional.empty())); + final var task = TaskId.generate(); - this.tasks.put(task, new ExecutionState.InProgress<>(startTime, state.create(this.executor))); + this.spanContributorCount.put(span, new MutableInt(1)); + this.tasks.put(task, new ExecutionState<>(span, Optional.empty(), state.create(this.executor))); this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(startTime)); - return task; + + return span; } /** Register a resource whose profile should be accumulated over time. */ @@ -105,7 +113,7 @@ public void invalidateTopic(final Topic topic, final Duration invalidationTim for (final var condition : conditions) { // If we were going to signal tasks on this condition, well, don't do that. // Schedule the condition to be rechecked ASAP. - this.scheduledJobs.unschedule(JobId.forSignal(SignalId.forCondition(condition))); + this.scheduledJobs.unschedule(JobId.forSignal(condition)); this.scheduledJobs.schedule(JobId.forCondition(condition), SubInstant.Conditions.at(invalidationTime)); } } @@ -119,8 +127,7 @@ public JobSchedule.Batch extractNextJobs(final Duration maximumTime) { // that the condition depends on, in which case we might accidentally schedule an update for a condition // that no longer exists. for (final var job : batch.jobs()) { - if (!(job instanceof JobId.SignalJobId j)) continue; - if (!(j.id() instanceof SignalId.ConditionSignalId s)) continue; + if (!(job instanceof JobId.SignalJobId s)) continue; this.conditions.remove(s.id()); this.waitingConditions.unsubscribeQuery(s.id()); @@ -156,7 +163,7 @@ public void performJob( if (job instanceof JobId.TaskJobId j) { this.stepTask(j.id(), frame, currentTime); } else if (job instanceof JobId.SignalJobId j) { - this.stepSignalledTasks(j.id(), frame); + this.stepTask(this.waitingTasks.remove(j.id()), frame, currentTime); } else if (job instanceof JobId.ConditionJobId j) { this.updateCondition(j.id(), frame, currentTime, maximumTime); } else if (job instanceof JobId.ResourceJobId j) { @@ -168,108 +175,95 @@ public void performJob( /** Perform the next step of a modeled task. */ public void stepTask(final TaskId task, final TaskFrame frame, final Duration currentTime) { - // The handler for each individual task stage is responsible - // for putting an updated lifecycle back into the task set. - var lifecycle = this.tasks.remove(task); + // The handler for the next status of the task is responsible + // for putting an updated state back into the task set. + var state = this.tasks.remove(task); - stepTaskHelper(task, frame, currentTime, lifecycle); - } - - private void stepTaskHelper( - final TaskId task, - final TaskFrame frame, - final Duration currentTime, - final ExecutionState lifecycle) - { - // Extract the current modeling state. - if (lifecycle instanceof ExecutionState.InProgress e) { - stepEffectModel(task, e, frame, currentTime); - } else if (lifecycle instanceof ExecutionState.AwaitingChildren e) { - stepWaitingTask(task, e, frame, currentTime); - } else { - // TODO: Log this issue to somewhere more general than stderr. - System.err.println("Task %s is ready but in unexpected execution state %s".formatted(task, lifecycle)); - } + stepEffectModel(task, state, frame, currentTime); } /** Make progress in a task by stepping its associated effect model forward. */ - private void stepEffectModel( + private void stepEffectModel( final TaskId task, - final ExecutionState.InProgress progress, + final ExecutionState progress, final TaskFrame frame, final Duration currentTime ) { // Step the modeling state forward. - final var scheduler = new EngineScheduler(currentTime, task, frame); + final var scheduler = new EngineScheduler(currentTime, progress.span(), progress.caller(), frame); final var status = progress.state().step(scheduler); // TODO: Report which topics this activity wrote to at this point in time. This is useful insight for any user. // TODO: Report which cells this activity read from at this point in time. This is useful insight for any user. // Based on the task's return status, update its execution state and schedule its resumption. - if (status instanceof TaskStatus.Completed) { - final var children = new LinkedList<>(this.taskChildren.getOrDefault(task, Collections.emptySet())); - - this.tasks.put(task, progress.completedAt(currentTime, children)); - this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(currentTime)); - } else if (status instanceof TaskStatus.Delayed s) { - if (s.delay().isNegative()) throw new IllegalArgumentException("Cannot schedule a task in the past"); - - this.tasks.put(task, progress.continueWith(s.continuation())); - this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(currentTime.plus(s.delay()))); - } else if (status instanceof TaskStatus.CallingTask s) { - final var target = TaskId.generate(); - SimulationEngine.this.tasks.put(target, new ExecutionState.InProgress<>(currentTime, s.child().create(this.executor))); - SimulationEngine.this.taskParent.put(target, task); - SimulationEngine.this.taskChildren.computeIfAbsent(task, $ -> new HashSet<>()).add(target); - frame.signal(JobId.forTask(target)); - - this.tasks.put(task, progress.continueWith(s.continuation())); - this.waitingTasks.subscribeQuery(task, Set.of(SignalId.forTask(target))); - } else if (status instanceof TaskStatus.AwaitingCondition s) { - final var condition = ConditionId.generate(); - this.conditions.put(condition, s.condition()); - this.scheduledJobs.schedule(JobId.forCondition(condition), SubInstant.Conditions.at(currentTime)); - - this.tasks.put(task, progress.continueWith(s.continuation())); - this.waitingTasks.subscribeQuery(task, Set.of(SignalId.forCondition(condition))); - } else { - throw new IllegalArgumentException("Unknown subclass of %s: %s".formatted(TaskStatus.class, status)); - } - } + switch (status) { + case TaskStatus.Completed s -> { + // Propagate completion up the span hierarchy. + // TERMINATION: The span hierarchy is a finite tree, so eventually we find a parentless span. + var span = scheduler.span; + while (true) { + if (this.spanContributorCount.get(span).decrementAndGet() > 0) break; + this.spanContributorCount.remove(span); - /** Make progress in a task by checking if all of the tasks it's waiting on have completed. */ - private void stepWaitingTask( - final TaskId task, - final ExecutionState.AwaitingChildren awaiting, - final TaskFrame frame, - final Duration currentTime - ) { - // TERMINATION: We break when there are no remaining children, - // and we always remove one if we don't break for other reasons. - while (true) { - if (awaiting.remainingChildren().isEmpty()) { - this.tasks.put(task, awaiting.joinedAt(currentTime)); - frame.signal(JobId.forSignal(SignalId.forTask(task))); - break; - } + this.spans.compute(span, (_id, $) -> $.close(currentTime)); - final var nextChild = awaiting.remainingChildren().getFirst(); - if (!(this.tasks.get(nextChild) instanceof ExecutionState.Terminated)) { - this.tasks.put(task, awaiting); - this.waitingTasks.subscribeQuery(task, Set.of(SignalId.forTask(nextChild))); - break; - } + final var span$ = this.spans.get(span).parent; + if (span$.isEmpty()) break; - // This child is complete, so skip checking it next time; move to the next one. - awaiting.remainingChildren().removeFirst(); - } - } + span = span$.get(); + } + + // Notify any blocked caller of our completion. + progress.caller().ifPresent($ -> { + if (this.blockedTasks.get($).decrementAndGet() == 0) { + this.blockedTasks.remove($); + this.scheduledJobs.schedule(JobId.forTask($), SubInstant.Tasks.at(currentTime)); + } + }); + } + + case TaskStatus.Delayed s -> { + if (s.delay().isNegative()) throw new IllegalArgumentException("Cannot schedule a task in the past"); + + this.tasks.put(task, progress.continueWith(s.continuation())); + this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(currentTime.plus(s.delay()))); + } + + case TaskStatus.CallingTask s -> { + // Prepare a span for the child task. + final var childSpan = switch (s.childSpan()) { + case Parent -> + scheduler.span; + + case Fresh -> { + final var freshSpan = SpanId.generate(); + SimulationEngine.this.spans.put(freshSpan, new Span(Optional.of(scheduler.span), currentTime, Optional.empty())); + SimulationEngine.this.spanContributorCount.put(freshSpan, new MutableInt(1)); + yield freshSpan; + } + }; + + // Spawn the child task. + final var childTask = TaskId.generate(); + SimulationEngine.this.spanContributorCount.get(scheduler.span).increment(); + SimulationEngine.this.tasks.put(childTask, new ExecutionState<>(childSpan, Optional.of(task), s.child().create(this.executor))); + frame.signal(JobId.forTask(childTask)); - /** Cause any tasks waiting on the given signal to be resumed concurrently with other jobs in the current frame. */ - public void stepSignalledTasks(final SignalId signal, final TaskFrame frame) { - final var tasks = this.waitingTasks.invalidateTopic(signal); - for (final var task : tasks) frame.signal(JobId.forTask(task)); + // Arrange for the parent task to resume.... later. + SimulationEngine.this.blockedTasks.put(task, new MutableInt(1)); + this.tasks.put(task, progress.continueWith(s.continuation())); + } + + case TaskStatus.AwaitingCondition s -> { + final var condition = ConditionId.generate(); + this.conditions.put(condition, s.condition()); + this.scheduledJobs.schedule(JobId.forCondition(condition), SubInstant.Conditions.at(currentTime)); + + this.tasks.put(task, progress.continueWith(s.continuation())); + this.waitingTasks.put(condition, task); + } + } } /** Determine when a condition is next true, and schedule a signal to be raised at that time. */ @@ -289,7 +283,7 @@ public void updateCondition( final var expiry = querier.expiry.map(currentTime::plus); if (prediction.isPresent() && (expiry.isEmpty() || prediction.get().shorterThan(expiry.get()))) { - this.scheduledJobs.schedule(JobId.forSignal(SignalId.forCondition(condition)), SubInstant.Tasks.at(prediction.get())); + this.scheduledJobs.schedule(JobId.forSignal(condition), SubInstant.Tasks.at(prediction.get())); } else { // Try checking again later -- where "later" is in some non-zero amount of time! final var nextCheckTime = Duration.max(expiry.orElse(horizonTime), currentTime.plus(Duration.EPSILON)); @@ -318,85 +312,91 @@ public void updateResource( @Override public void close() { for (final var task : this.tasks.values()) { - if (task instanceof ExecutionState.InProgress r) { - r.state.release(); - } + task.state().release(); } this.executor.shutdownNow(); } - /** Determine if a given task has fully completed. */ - public boolean isTaskComplete(final TaskId task) { - return (this.tasks.get(task) instanceof ExecutionState.Terminated); - } - - private record TaskInfo( - Map taskToPlannedDirective, - Map input, - Map output + private record SpanInfo( + Map spanToPlannedDirective, + Map input, + Map output ) { - public TaskInfo() { + public SpanInfo() { this(new HashMap<>(), new HashMap<>(), new HashMap<>()); } - public boolean isActivity(final TaskId id) { - return this.input.containsKey(id.id()); + public boolean isActivity(final SpanId id) { + return this.input.containsKey(id); + } + + public boolean isDirective(SpanId id) { + return this.spanToPlannedDirective.containsKey(id); } - public record Trait(Iterable> topics, Topic activityTopic) implements EffectTrait> { + public ActivityDirectiveId getDirective(SpanId id) { + return this.spanToPlannedDirective.get(id); + } + + public record Trait(Iterable> topics, Topic activityTopic) implements EffectTrait> { @Override - public Consumer empty() { - return taskInfo -> {}; + public Consumer empty() { + return spanInfo -> {}; } @Override - public Consumer sequentially(final Consumer prefix, final Consumer suffix) { - return taskInfo -> { prefix.accept(taskInfo); suffix.accept(taskInfo); }; + public Consumer sequentially(final Consumer prefix, final Consumer suffix) { + return spanInfo -> { prefix.accept(spanInfo); suffix.accept(spanInfo); }; } @Override - public Consumer concurrently(final Consumer left, final Consumer right) { - // SAFETY: For each task, `left` commutes with `right`, because no task runs concurrently with itself. - return taskInfo -> { left.accept(taskInfo); right.accept(taskInfo); }; + public Consumer concurrently(final Consumer left, final Consumer right) { + // SAFETY: `left` and `right` should commute. HOWEVER, if a span happens to directly contain two activities + // -- that is, two activities both contribute events under the same span's provenance -- then this + // does not actually commute. + // Arguably, this is a model-specific analysis anyway, since we're looking for specific events + // and inferring model structure from them, and at this time we're only working with models + // for which every activity has a span to itself. + return spanInfo -> { left.accept(spanInfo); right.accept(spanInfo); }; } - public Consumer atom(final Event ev) { - return taskInfo -> { + public Consumer atom(final Event ev) { + return spanInfo -> { // Identify activities. ev.extract(this.activityTopic) - .ifPresent(directiveId -> taskInfo.taskToPlannedDirective.put(ev.provenance().id(), directiveId)); + .ifPresent(directiveId -> spanInfo.spanToPlannedDirective.put(ev.provenance(), directiveId)); for (final var topic : this.topics) { // Identify activity inputs. - extractInput(topic, ev, taskInfo); + extractInput(topic, ev, spanInfo); // Identify activity outputs. - extractOutput(topic, ev, taskInfo); + extractOutput(topic, ev, spanInfo); } }; } private static - void extractInput(final SerializableTopic topic, final Event ev, final TaskInfo taskInfo) { + void extractInput(final SerializableTopic topic, final Event ev, final SpanInfo spanInfo) { if (!topic.name().startsWith("ActivityType.Input.")) return; ev.extract(topic.topic()).ifPresent(input -> { final var activityType = topic.name().substring("ActivityType.Input.".length()); - taskInfo.input.put( - ev.provenance().id(), + spanInfo.input.put( + ev.provenance(), new SerializedActivity(activityType, topic.outputType().serialize(input).asMap().orElseThrow())); }); } private static - void extractOutput(final SerializableTopic topic, final Event ev, final TaskInfo taskInfo) { + void extractOutput(final SerializableTopic topic, final Event ev, final SpanInfo spanInfo) { if (!topic.name().startsWith("ActivityType.Output.")) return; ev.extract(topic.topic()).ifPresent(output -> { - taskInfo.output.put( - ev.provenance().id(), + spanInfo.output.put( + ev.provenance(), topic.outputType().serialize(output)); }); } @@ -418,14 +418,14 @@ public static SimulationResults computeResults( final TemporalEventSource timeline, final Iterable> serializableTopics ) { - // Collect per-task information from the event graph. - final var taskInfo = new TaskInfo(); + // Collect per-span information from the event graph. + final var spanInfo = new SpanInfo(); for (final var point : timeline) { if (!(point instanceof TemporalEventSource.TimePoint.Commit p)) continue; - final var trait = new TaskInfo.Trait(serializableTopics, activityTopic); - p.events().evaluate(trait, trait::atom).accept(taskInfo); + final var trait = new SpanInfo.Trait(serializableTopics, activityTopic); + p.events().evaluate(trait, trait::atom).accept(spanInfo); } // Extract profiles for every resource. @@ -458,87 +458,77 @@ public static SimulationResults computeResults( } } + // Identify the nearest ancestor *activity* (excluding intermediate anonymous tasks). + final var activityParents = new HashMap(); + final var activityDirectiveIds = new HashMap(); + engine.spans.forEach((span, state) -> { + if (!spanInfo.isActivity(span)) return; + + if (spanInfo.isDirective(span)) activityDirectiveIds.put(span, spanInfo.getDirective(span)); + + var parent = state.parent(); + while (parent.isPresent() && !spanInfo.isActivity(parent.get())) { + parent = engine.spans.get(parent.get()).parent(); + } + + if (parent.isPresent()) { + activityParents.put(span, parent.get()); + } + }); + + final var activityChildren = new HashMap>(); + activityParents.forEach((activity, parent) -> { + activityChildren.computeIfAbsent(parent, $ -> new LinkedList<>()).add(activity); + }); // Give every task corresponding to a child activity an ID that doesn't conflict with any root activity. - final var taskToSimulatedActivityId = new HashMap(taskInfo.taskToPlannedDirective.size()); + final var spanToSimulatedActivityId = new HashMap(activityDirectiveIds.size()); final var usedSimulatedActivityIds = new HashSet<>(); - for (final var entry : taskInfo.taskToPlannedDirective.entrySet()) { - taskToSimulatedActivityId.put(entry.getKey(), new SimulatedActivityId(entry.getValue().id())); + for (final var entry : activityDirectiveIds.entrySet()) { + spanToSimulatedActivityId.put(entry.getKey(), new SimulatedActivityId(entry.getValue().id())); usedSimulatedActivityIds.add(entry.getValue().id()); } long counter = 1L; - for (final var task : engine.tasks.keySet()) { - if (!taskInfo.isActivity(task)) continue; - if (taskToSimulatedActivityId.containsKey(task.id())) continue; + for (final var span : engine.spans.keySet()) { + if (!spanInfo.isActivity(span)) continue; + if (spanToSimulatedActivityId.containsKey(span)) continue; while (usedSimulatedActivityIds.contains(counter)) counter++; - taskToSimulatedActivityId.put(task.id(), new SimulatedActivityId(counter++)); + spanToSimulatedActivityId.put(span, new SimulatedActivityId(counter++)); } - // Identify the nearest ancestor *activity* (excluding intermediate anonymous tasks). - final var activityParents = new HashMap(); - engine.tasks.forEach((task, state) -> { - if (!taskInfo.isActivity(task)) return; - - var parent = engine.taskParent.get(task); - while (parent != null && !taskInfo.isActivity(parent)) { - parent = engine.taskParent.get(parent); - } - - if (parent != null) { - activityParents.put(taskToSimulatedActivityId.get(task.id()), taskToSimulatedActivityId.get(parent.id())); - } - }); - - final var activityChildren = new HashMap>(); - activityParents.forEach((task, parent) -> { - activityChildren.computeIfAbsent(parent, $ -> new LinkedList<>()).add(task); - }); - final var simulatedActivities = new HashMap(); final var unfinishedActivities = new HashMap(); - engine.tasks.forEach((task, state) -> { - if (!taskInfo.isActivity(task)) return; + engine.spans.forEach((span, state) -> { + if (!spanInfo.isActivity(span)) return; - final var activityId = taskToSimulatedActivityId.get(task.id()); - final var directiveId = taskInfo.taskToPlannedDirective.get(task.id()); // will be null for non-directives + final var activityId = spanToSimulatedActivityId.get(span); + final var directiveId = activityDirectiveIds.get(span); - if (state instanceof ExecutionState.Terminated e) { - final var inputAttributes = taskInfo.input().get(task.id()); - final var outputAttributes = taskInfo.output().get(task.id()); + if (state.endOffset().isPresent()) { + final var inputAttributes = spanInfo.input().get(span); + final var outputAttributes = spanInfo.output().get(span); simulatedActivities.put(activityId, new SimulatedActivity( inputAttributes.getTypeName(), inputAttributes.getArguments(), - startTime.plus(e.startOffset().in(Duration.MICROSECONDS), ChronoUnit.MICROS), - e.joinOffset().minus(e.startOffset()), - activityParents.get(activityId), - activityChildren.getOrDefault(activityId, Collections.emptyList()), - (activityParents.containsKey(activityId)) ? Optional.empty() : Optional.of(directiveId), + startTime.plus(state.startOffset().in(Duration.MICROSECONDS), ChronoUnit.MICROS), + state.endOffset().get().minus(state.startOffset()), + spanToSimulatedActivityId.get(activityParents.get(span)), + activityChildren.getOrDefault(span, Collections.emptyList()).stream().map(spanToSimulatedActivityId::get).toList(), + (activityParents.containsKey(span)) ? Optional.empty() : Optional.ofNullable(directiveId), outputAttributes )); - } else if (state instanceof ExecutionState.InProgress e){ - final var inputAttributes = taskInfo.input().get(task.id()); - unfinishedActivities.put(activityId, new UnfinishedActivity( - inputAttributes.getTypeName(), - inputAttributes.getArguments(), - startTime.plus(e.startOffset().in(Duration.MICROSECONDS), ChronoUnit.MICROS), - activityParents.get(activityId), - activityChildren.getOrDefault(activityId, Collections.emptyList()), - (activityParents.containsKey(activityId)) ? Optional.empty() : Optional.of(directiveId) - )); - } else if (state instanceof ExecutionState.AwaitingChildren e){ - final var inputAttributes = taskInfo.input().get(task.id()); + } else { + final var inputAttributes = spanInfo.input().get(span); unfinishedActivities.put(activityId, new UnfinishedActivity( inputAttributes.getTypeName(), inputAttributes.getArguments(), - startTime.plus(e.startOffset().in(Duration.MICROSECONDS), ChronoUnit.MICROS), - activityParents.get(activityId), - activityChildren.getOrDefault(activityId, Collections.emptyList()), - (activityParents.containsKey(activityId)) ? Optional.empty() : Optional.of(directiveId) + startTime.plus(state.startOffset().in(Duration.MICROSECONDS), ChronoUnit.MICROS), + spanToSimulatedActivityId.get(activityParents.get(span)), + activityChildren.getOrDefault(span, Collections.emptyList()).stream().map(spanToSimulatedActivityId::get).toList(), + (activityParents.containsKey(span)) ? Optional.empty() : Optional.of(directiveId) )); - } else { - throw new Error("Unexpected subtype of %s: %s".formatted(ExecutionState.class, state.getClass())); } }); @@ -585,12 +575,8 @@ public static SimulationResults computeResults( serializedTimeline); } - public Optional getTaskDuration(TaskId taskId){ - final var state = tasks.get(taskId); - if (state instanceof ExecutionState.Terminated e) { - return Optional.of(e.joinOffset().minus(e.startOffset())); - } - return Optional.empty(); + public Span getSpan(SpanId spanId) { + return this.spans.get(spanId); } @@ -680,12 +666,19 @@ private static Optional min(final Optional a, final Optional /** A handle for processing requests and effects from a modeled task. */ private final class EngineScheduler implements Scheduler { private final Duration currentTime; - private final TaskId activeTask; + private final SpanId span; + private final Optional caller; private final TaskFrame frame; - public EngineScheduler(final Duration currentTime, final TaskId activeTask, final TaskFrame frame) { + public EngineScheduler( + final Duration currentTime, + final SpanId span, + final Optional caller, + final TaskFrame frame) + { this.currentTime = Objects.requireNonNull(currentTime); - this.activeTask = Objects.requireNonNull(activeTask); + this.span = Objects.requireNonNull(span); + this.caller = Objects.requireNonNull(caller); this.frame = Objects.requireNonNull(frame); } @@ -704,18 +697,32 @@ public State get(final CellId token) { @Override public void emit(final EventType event, final Topic topic) { // Append this event to the timeline. - this.frame.emit(Event.create(topic, event, this.activeTask)); + this.frame.emit(Event.create(topic, event, this.span)); SimulationEngine.this.invalidateTopic(topic, this.currentTime); } @Override - public void spawn(final TaskFactory state) { - final var task = TaskId.generate(); - SimulationEngine.this.tasks.put(task, new ExecutionState.InProgress<>(this.currentTime, state.create(SimulationEngine.this.executor))); - SimulationEngine.this.taskParent.put(task, this.activeTask); - SimulationEngine.this.taskChildren.computeIfAbsent(this.activeTask, $ -> new HashSet<>()).add(task); - this.frame.signal(JobId.forTask(task)); + public void spawn(final InSpan inSpan, final TaskFactory state) { + // Prepare a span for the child task + final var childSpan = switch (inSpan) { + case Parent -> + this.span; + + case Fresh -> { + final var freshSpan = SpanId.generate(); + SimulationEngine.this.spans.put(freshSpan, new Span(Optional.of(this.span), currentTime, Optional.empty())); + SimulationEngine.this.spanContributorCount.put(freshSpan, new MutableInt(1)); + yield freshSpan; + } + }; + + final var childTask = TaskId.generate(); + SimulationEngine.this.spanContributorCount.get(this.span).increment(); + SimulationEngine.this.tasks.put(childTask, new ExecutionState<>(childSpan, this.caller, state.create(SimulationEngine.this.executor))); + this.frame.signal(JobId.forTask(childTask)); + + this.caller.ifPresent($ -> SimulationEngine.this.blockedTasks.get($).increment()); } } @@ -724,8 +731,8 @@ public sealed interface JobId { /** A job to step a task. */ record TaskJobId(TaskId id) implements JobId {} - /** A job to step all tasks waiting on a signal. */ - record SignalJobId(SignalId id) implements JobId {} + /** A job to resume a task blocked on a condition. */ + record SignalJobId(ConditionId id) implements JobId {} /** A job to query a resource. */ record ResourceJobId(ResourceId id) implements JobId {} @@ -737,7 +744,7 @@ static TaskJobId forTask(final TaskId task) { return new TaskJobId(task); } - static SignalJobId forSignal(final SignalId signal) { + static SignalJobId forSignal(final ConditionId signal) { return new SignalJobId(signal); } @@ -750,40 +757,27 @@ static ConditionJobId forCondition(final ConditionId condition) { } } - /** The lifecycle stages every task passes through. */ - private sealed interface ExecutionState { - /** The task is in its primary operational phase. */ - record InProgress(Duration startOffset, Task state) - implements ExecutionState - { - public AwaitingChildren completedAt( - final Duration endOffset, - final LinkedList remainingChildren) { - return new AwaitingChildren<>(this.startOffset, endOffset, remainingChildren); - } + /** The state of an executing task. */ + private record ExecutionState(SpanId span, Optional caller, Task state) { + public ExecutionState continueWith(final Task newState) { + return new ExecutionState<>(this.span, this.caller, newState); + } + } - public InProgress continueWith(final Task newState) { - return new InProgress<>(this.startOffset, newState); - } + /** The span of time over which a subtree of tasks has acted. */ + public record Span(Optional parent, Duration startOffset, Optional endOffset) { + /** Close out a span, marking it as inactive past the given time. */ + public Span close(final Duration endOffset) { + if (this.endOffset.isPresent()) throw new Error("Attempt to close an already-closed span"); + return new Span(this.parent, this.startOffset, Optional.of(endOffset)); } - /** The task has completed its primary operation, but has unfinished children. */ - record AwaitingChildren( - Duration startOffset, - Duration endOffset, - LinkedList remainingChildren - ) implements ExecutionState - { - public Terminated joinedAt(final Duration joinOffset) { - return new Terminated<>(this.startOffset, this.endOffset, joinOffset); - } + public Optional duration() { + return this.endOffset.map($ -> $.minus(this.startOffset)); } - /** The task and all its delegated children have completed. */ - record Terminated( - Duration startOffset, - Duration endOffset, - Duration joinOffset - ) implements ExecutionState {} + public boolean isComplete() { + return this.endOffset.isPresent(); + } } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SpanId.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SpanId.java new file mode 100644 index 0000000000..96f5795b0c --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SpanId.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.merlin.driver.engine; + +import java.util.UUID; + +/** A typed wrapper for span IDs. */ +public record SpanId(String id) { + public static SpanId generate() { + return new SpanId(UUID.randomUUID().toString()); + } +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/Event.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/Event.java index e9f334a7da..7fd8840319 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/Event.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/Event.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.merlin.driver.timeline; -import gov.nasa.jpl.aerie.merlin.driver.engine.TaskId; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import java.util.Objects; @@ -16,7 +16,7 @@ private Event(final Event.GenericEvent inner) { } public static - Event create(final Topic topic, final EventType event, final TaskId provenance) { + Event create(final Topic topic, final EventType event, final SpanId provenance) { return new Event(new Event.GenericEvent<>(topic, event, provenance)); } @@ -34,7 +34,7 @@ public Topic topic() { return this.inner.topic(); } - public TaskId provenance() { + public SpanId provenance() { return this.inner.provenance(); } @@ -43,7 +43,7 @@ public String toString() { return "<@%s, %s>".formatted(System.identityHashCode(this.inner.topic), this.inner.event); } - private record GenericEvent(Topic topic, EventType event, TaskId provenance) { + private record GenericEvent(Topic topic, EventType event, SpanId provenance) { private GenericEvent { Objects.requireNonNull(topic); Objects.requireNonNull(event); diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java index fac9709efb..d7e8984d81 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java @@ -9,6 +9,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; @@ -1113,14 +1114,14 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { Duration.ZERO, $ -> { try { - $.spawn(delayedActivityDirective.getTaskFactory(null, null)); + $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( ex.toString())); } return TaskStatus.delayed(Duration.of(120, Duration.SECOND), $$ -> { try { - $$.spawn(delayedActivityDirective.getTaskFactory(null, null)); + $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error( "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/engine/TaskFrameTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/engine/TaskFrameTest.java index 44e8cbed5e..1b435dfbce 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/engine/TaskFrameTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/engine/TaskFrameTest.java @@ -28,7 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public final class TaskFrameTest { - private static final TaskId ORIGIN = TaskId.generate(); + private static final SpanId ORIGIN = SpanId.generate(); // This regression test identified a bug in the LiveCells-chain-avoidance optimization in TaskFrame. @Test diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java index be17603c33..a09bffff68 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java @@ -376,7 +376,7 @@ public JavaFile generateActivityActions(final MissionModelRecord missionModel) { missionModel.getTypesName(), entry.inputType().mapper().name.canonicalName().replace(".", "_")) .addStatement( - "$T.spawn($L.getTaskFactory($L, $L))", + "$T.spawnWithSpan($L.getTaskFactory($L, $L))", gov.nasa.jpl.aerie.merlin.framework.ModelActions.class, "mapper", "model", @@ -403,7 +403,7 @@ public JavaFile generateActivityActions(final MissionModelRecord missionModel) { missionModel.getTypesName(), entry.inputType().mapper().name.canonicalName().replace(".", "_")) .addStatement( - "$T.defer($L, $L.getTaskFactory($L, $L))", + "$T.deferWithSpan($L, $L.getTaskFactory($L, $L))", gov.nasa.jpl.aerie.merlin.framework.ModelActions.class, "duration", "mapper", @@ -459,7 +459,7 @@ public JavaFile generateActivityActions(final MissionModelRecord missionModel) { missionModel.getTypesName(), entry.inputType().mapper().name.canonicalName().replace(".", "_")) .addStatement( - "$T.call($L.getTaskFactory($L, $L))", + "$T.callWithSpan($L.getTaskFactory($L, $L))", gov.nasa.jpl.aerie.merlin.framework.ModelActions.class, "mapper", "model", diff --git a/merlin-framework/build.gradle b/merlin-framework/build.gradle index a52dde0379..66a07a16c6 100644 --- a/merlin-framework/build.gradle +++ b/merlin-framework/build.gradle @@ -15,6 +15,9 @@ java { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Context.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Context.java index 6738237a37..643c2181bb 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Context.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Context.java @@ -5,6 +5,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.CellType; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import java.util.function.Function; @@ -29,8 +30,8 @@ enum ContextType { Initializing, Reacting, Querying } // Usable during simulation void emit(Event event, Topic topic); - void spawn(TaskFactory task); - void call(TaskFactory task); + void spawn(InSpan inSpan, TaskFactory task); + void call(InSpan inSpan, TaskFactory task); void delay(Duration duration); void waitUntil(Condition condition); diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/InitializationContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/InitializationContext.java index 9615830a69..46c1ef1860 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/InitializationContext.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/InitializationContext.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.CellType; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import java.util.Objects; import java.util.function.Function; @@ -52,12 +53,14 @@ public void emit(final Event event, final Topic topic) { } @Override - public void spawn(final TaskFactory task) { + public void spawn(final InSpan _inSpan, final TaskFactory task) { + // As top-level tasks, daemons always get their own span. + // TODO: maybe produce a warning if inSpan is not Fresh in initialization context this.builder.daemon(task); } @Override - public void call(final TaskFactory task) { + public void call(final InSpan inSpan, final TaskFactory task) { throw new IllegalStateException("Cannot yield during initialization"); } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ModelActions.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ModelActions.java index 1c9019f328..6e23b98678 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ModelActions.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ModelActions.java @@ -3,6 +3,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; import java.util.function.Supplier; @@ -54,7 +55,7 @@ public static void spawn(final Runnable task) { } public static void spawn(final TaskFactory task) { - context.get().spawn(task); + context.get().spawn(InSpan.Parent, task); } public static void call(final Runnable task) { @@ -66,7 +67,35 @@ public static void call(final Supplier task) { } public static void call(final TaskFactory task) { - context.get().call(task); + context.get().call(InSpan.Parent, task); + } + + + public static void spawnWithSpan(final Supplier task) { + spawnWithSpan(threaded(task)); + } + + public static void spawnWithSpan(final Runnable task) { + spawnWithSpan(() -> { + task.run(); + return Unit.UNIT; + }); + } + + public static void spawnWithSpan(final TaskFactory task) { + context.get().spawn(InSpan.Fresh, task); + } + + public static void callWithSpan(final Runnable task) { + callWithSpan(threaded(task)); + } + + public static void callWithSpan(final Supplier task) { + callWithSpan(threaded(task)); + } + + public static void callWithSpan(final TaskFactory task) { + context.get().call(InSpan.Fresh, task); } public static void defer(final Duration duration, final Runnable task) { @@ -85,6 +114,22 @@ public static void defer(final long quantity, final Duration unit, final TaskFac spawn(replaying(() -> { delay(quantity, unit); spawn(task); })); } + public static void deferWithSpan(final Duration duration, final Runnable task) { + spawn(replaying(() -> { delay(duration); spawnWithSpan(task); })); + } + + public static void deferWithSpan(final Duration duration, final TaskFactory task) { + spawn(replaying(() -> { delay(duration); spawnWithSpan(task); })); + } + + public static void deferWithSpan(final long quantity, final Duration unit, final Runnable task) { + spawn(replaying(() -> { delay(quantity, unit); spawnWithSpan(task); })); + } + + public static void deferWithSpan(final long quantity, final Duration unit, final TaskFactory task) { + spawn(replaying(() -> { delay(quantity, unit); spawnWithSpan(task); })); + } + public static void delay(final Duration duration) { context.get().delay(duration); diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/QueryContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/QueryContext.java index 2425a9ab77..438970d9f6 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/QueryContext.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/QueryContext.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.CellType; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import java.util.function.Function; @@ -43,12 +44,12 @@ public void emit(final Event event, final Topic topic) { } @Override - public void spawn(final TaskFactory task) { + public void spawn(final InSpan inSpan, final TaskFactory task) { throw new IllegalStateException("Cannot schedule tasks in a query-only context"); } @Override - public void call(final TaskFactory task) { + public void call(final InSpan inSpan, final TaskFactory task) { throw new IllegalStateException("Cannot schedule tasks in a query-only context"); } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingReactionContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingReactionContext.java index e9998ff256..672f862c32 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingReactionContext.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingReactionContext.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.CellType; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import org.apache.commons.lang3.mutable.MutableInt; import java.util.List; @@ -64,17 +65,17 @@ public void emit(final Event event, final Topic topic) { } @Override - public void spawn(final TaskFactory task) { + public void spawn(final InSpan inSpan, final TaskFactory task) { this.memory.doOnce(() -> { - this.scheduler.spawn(task); + this.scheduler.spawn(inSpan, task); }); } @Override - public void call(final TaskFactory task) { + public void call(final InSpan inSpan, final TaskFactory task) { this.memory.doOnce(() -> { this.scheduler = null; // Relinquish the current scheduler before yielding, in case an exception is thrown. - this.scheduler = this.handle.call(task); + this.scheduler = this.handle.call(inSpan, task); }); } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java index b18f38f6ca..a89e4048b3 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; import org.apache.commons.lang3.mutable.MutableInt; @@ -52,8 +53,8 @@ public Scheduler delay(final Duration delay) { } @Override - public Scheduler call(final TaskFactory child) { - return this.yield(TaskStatus.calling(child, ReplayingTask.this)); + public Scheduler call(final InSpan inSpan, final TaskFactory child) { + return this.yield(TaskStatus.calling(inSpan, child, ReplayingTask.this)); } @Override diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/TaskHandle.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/TaskHandle.java index 609453f0c9..82b0108384 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/TaskHandle.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/TaskHandle.java @@ -3,11 +3,12 @@ import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; public interface TaskHandle { Scheduler delay(Duration delay); - Scheduler call(TaskFactory child); + Scheduler call(InSpan inSpan, TaskFactory child); Scheduler await(gov.nasa.jpl.aerie.merlin.protocol.model.Condition condition); } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java index 3edd568d9e..d4bc94e559 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.CellType; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import java.util.Objects; import java.util.function.Function; @@ -53,14 +54,14 @@ public void emit(final Event event, final Topic topic) { } @Override - public void spawn(final TaskFactory task) { - this.scheduler.spawn(task); + public void spawn(final InSpan inSpan, final TaskFactory task) { + this.scheduler.spawn(inSpan, task); } @Override - public void call(final TaskFactory task) { + public void call(final InSpan inSpan, final TaskFactory task) { this.scheduler = null; // Relinquish the current scheduler before yielding, in case an exception is thrown. - this.scheduler = this.handle.call(task); + this.scheduler = this.handle.call(inSpan, task); } @Override diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java index 58e3e3b626..fec837e0c2 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; import java.util.Objects; @@ -203,8 +204,8 @@ public Scheduler delay(final Duration delay) { } @Override - public Scheduler call(final TaskFactory child) { - return this.yield(TaskStatus.calling(child, ThreadedTask.this)); + public Scheduler call(final InSpan inSpan, final TaskFactory child) { + return this.yield(TaskStatus.calling(inSpan, child, ThreadedTask.this)); } @Override diff --git a/merlin-framework/src/test/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTaskTest.java b/merlin-framework/src/test/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTaskTest.java index ac3942dcd0..229f990cc4 100644 --- a/merlin-framework/src/test/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTaskTest.java +++ b/merlin-framework/src/test/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTaskTest.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,7 +29,7 @@ public void emit(final Event event, final Topic topic) { } @Override - public void spawn(final TaskFactory task) { + public void spawn(final InSpan inSpan, final TaskFactory task) { throw new UnsupportedOperationException(); } }; diff --git a/merlin-sdk/build.gradle b/merlin-sdk/build.gradle index 908b26fad7..15cd7aa7bc 100644 --- a/merlin-sdk/build.gradle +++ b/merlin-sdk/build.gradle @@ -15,6 +15,9 @@ java { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/driver/Scheduler.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/driver/Scheduler.java index 035658c136..8a1c1ab949 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/driver/Scheduler.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/driver/Scheduler.java @@ -1,11 +1,12 @@ package gov.nasa.jpl.aerie.merlin.protocol.driver; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; public interface Scheduler { State get(CellId cellId); void emit(Event event, Topic topic); - void spawn(TaskFactory task); + void spawn(InSpan taskSpan, TaskFactory task); } diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/InSpan.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/InSpan.java new file mode 100644 index 0000000000..4389731524 --- /dev/null +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/InSpan.java @@ -0,0 +1,12 @@ +package gov.nasa.jpl.aerie.merlin.protocol.types; + +public enum InSpan { + /** + * Spawn a child task into the same span as its parent. + */ + Parent, + /** + * Spawn a child task into a fresh span under its parent's span. + */ + Fresh +} diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java index 178b320ba4..6cdc1cd688 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java @@ -9,7 +9,7 @@ record Completed(Return returnValue) implements TaskStatus {} record Delayed(Duration delay, Task continuation) implements TaskStatus {} - record CallingTask(TaskFactory child, Task continuation) implements TaskStatus {} + record CallingTask(InSpan childSpan, TaskFactory child, Task continuation) implements TaskStatus {} record AwaitingCondition(Condition condition, Task continuation) implements TaskStatus {} @@ -22,8 +22,8 @@ static Delayed delayed(final Duration delay, final Task return new Delayed<>(delay, continuation); } - static CallingTask calling(final TaskFactory child, final Task continuation) { - return new CallingTask<>(child, continuation); + static CallingTask calling(final InSpan childSpan, final TaskFactory child, final Task continuation) { + return new CallingTask<>(childSpan, child, continuation); } static AwaitingCondition awaiting(final Condition condition, final Task continuation) { diff --git a/merlin-server/build.gradle b/merlin-server/build.gradle index 7e4557f5b6..d13902ecff 100644 --- a/merlin-server/build.gradle +++ b/merlin-server/build.gradle @@ -60,6 +60,10 @@ test { environment "CONSTRAINTS_DSL_COMPILER_ROOT", projectDir.toPath().resolve('constraints-dsl-compiler') environment "CONSTRAINTS_DSL_COMPILER_COMMAND", './build/main.js' + + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/parsing-utilities/build.gradle b/parsing-utilities/build.gradle index 3eaa311b8b..cb2790ca01 100644 --- a/parsing-utilities/build.gradle +++ b/parsing-utilities/build.gradle @@ -12,6 +12,9 @@ java { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/procedural/README.md b/procedural/README.md new file mode 100644 index 0000000000..a17cee1523 --- /dev/null +++ b/procedural/README.md @@ -0,0 +1,7 @@ +# Procedural post-simulation libraries + +This subproject holds the libraries provided to the users for procedural scheduling / constraints / conditions. + +## Documentation + +To generate the unified docs, run `./gradlew :procedural:dokkaHtmlMultiModule`. It will be available in `procedural/build/dokka/htmlMultiModule/index.html`. To view it locally, you'll need a static file server to avoid CORS problems. In IntelliJ, you can right-click on `index.html` and select `Open In -> Browser -> ...` and this will start a server for you. diff --git a/procedural/build.gradle b/procedural/build.gradle new file mode 100644 index 0000000000..65061f0d89 --- /dev/null +++ b/procedural/build.gradle @@ -0,0 +1,8 @@ +plugins { + id "org.jetbrains.kotlin.jvm" version "1.9.22" + id 'org.jetbrains.dokka' version '1.9.10' +} + +repositories { + mavenCentral() +} diff --git a/procedural/constraints/MODULE_DOCS.md b/procedural/constraints/MODULE_DOCS.md new file mode 100644 index 0000000000..76386e400a --- /dev/null +++ b/procedural/constraints/MODULE_DOCS.md @@ -0,0 +1,6 @@ +# Module Constraints + +This library provides basic tools for creating constraints, based on the timeline library. +To define a constraint, just create a class that implements the Constraint interface. +Then, follow the tutorial documentation (TODO: link to tutorial documentation once written) +to package and upload your constraint to Aerie. diff --git a/procedural/constraints/README.md b/procedural/constraints/README.md new file mode 100644 index 0000000000..700351caff --- /dev/null +++ b/procedural/constraints/README.md @@ -0,0 +1,9 @@ +# Constraints + +This library provides the `Constraint` interface and `Violations` timeline type, for users to create procedural constraints. + +- Building and testing: `./gradlew :procedural:constraints:build` +- Generating a jar for local experimentation: `./gradlew :procedural:constraints:shadowJar` + - jar will be available at `procedural/constraints/build/libs/constraints-all.jar` + +See `/procedural/README.md` for instructions on generating viewing documentation. diff --git a/procedural/constraints/build.gradle b/procedural/constraints/build.gradle new file mode 100644 index 0000000000..80bce8c81c --- /dev/null +++ b/procedural/constraints/build.gradle @@ -0,0 +1,54 @@ +import org.jetbrains.kotlin.gradle.dsl.jvm.JvmTargetValidationMode +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + +plugins { + id 'com.github.johnrengelman.shadow' version '8.1.1' + id "org.jetbrains.kotlin.jvm" version "1.9.22" + id 'java-library' + id 'org.jetbrains.dokka' version '1.9.10' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':procedural:timeline') + + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' + testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType(KotlinJvmCompile.class).configureEach { + jvmTargetValidationMode = JvmTargetValidationMode.WARNING +} + +tasks.named('test') { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(21) +} + +var timelineSource = "${project(":procedural:timeline").projectDir}/src/main/kotlin" + +dokkaHtmlPartial.configure { + dokkaSourceSets { + configureEach { + // used as project name in the header + moduleName.set("Constraints") + + reportUndocumented.set(true) + failOnWarning.set(true) + + // contains descriptions for the module and the packages + includes.from("MODULE_DOCS.md") + + sourceRoots.from(timelineSource) + suppressedFiles.from(timelineSource) + } + } +} + diff --git a/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/ActivityId.kt b/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/ActivityId.kt new file mode 100644 index 0000000000..337df89065 --- /dev/null +++ b/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/ActivityId.kt @@ -0,0 +1,7 @@ +package gov.nasa.jpl.aerie.procedural.constraints + +/** An activity ID, referencing either a directive or instance. */ +sealed interface ActivityId { + /***/ data class InstanceId(/***/ val id: Long): ActivityId + /***/ data class DirectiveId(/***/ val id: Long): ActivityId +} diff --git a/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Constraint.kt b/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Constraint.kt new file mode 100644 index 0000000000..2fbd717b9b --- /dev/null +++ b/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Constraint.kt @@ -0,0 +1,18 @@ +package gov.nasa.jpl.aerie.procedural.constraints + +import gov.nasa.jpl.aerie.timeline.CollectOptions +import gov.nasa.jpl.aerie.timeline.plan.Plan + +/** The interface that all constraints must satisfy. */ +interface Constraint { + /** + * Run the constraint on a plan. + * + * The provided collect options are the options that the [Violations] result will be collected on after + * the constraint is run. The constraint does not need to use the options unless it collects a timeline prematurely. + * + * @param plan the plan to check the constraint on + * @param options the [CollectOptions] that the result will be collected with + */ + fun run(plan: Plan, options: CollectOptions): Violations +} diff --git a/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Violation.kt b/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Violation.kt new file mode 100644 index 0000000000..9e51a81e08 --- /dev/null +++ b/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Violation.kt @@ -0,0 +1,22 @@ +package gov.nasa.jpl.aerie.procedural.constraints + +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike + +/** A single violation of a constraint. */ +data class Violation( + /** Interval on which the violation occurs. */ + override val interval: Interval, + + /** List of associated activities (directives or instances) that are related to the violation. */ + val ids: List = listOf() +) : IntervalLike { + + override fun withNewInterval(i: Interval) = Violation(i, ids) + + /** Constructs a violation on the same interval with a different list of ids. */ + fun withNewIds(vararg id: ActivityId) = Violation(interval, id.asList()) + + /** Constructs a violation on the same interval with a different list of ids. */ + fun withNewIds(ids: List) = Violation(interval, ids) +} diff --git a/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Violations.kt b/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Violations.kt new file mode 100644 index 0000000000..43af1ee4ac --- /dev/null +++ b/procedural/constraints/src/main/kotlin/gov/nasa/jpl/aerie/procedural/constraints/Violations.kt @@ -0,0 +1,82 @@ +package gov.nasa.jpl.aerie.procedural.constraints + +import gov.nasa.jpl.aerie.procedural.constraints.ActivityId.DirectiveId +import gov.nasa.jpl.aerie.procedural.constraints.ActivityId.InstanceId +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.BoundsTransformer +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.collections.Intervals +import gov.nasa.jpl.aerie.timeline.collections.Windows +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.ops.* +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceNoOp +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike +import gov.nasa.jpl.aerie.timeline.payloads.activities.Directive +import gov.nasa.jpl.aerie.timeline.payloads.activities.Instance +import gov.nasa.jpl.aerie.timeline.util.preprocessList + +/** A timeline of [Violations][Violation]. */ +data class Violations(private val timeline: Timeline): + Timeline by timeline, + ParallelOps, + NonZeroDurationOps, + CoalesceNoOp +{ + constructor(vararg violation: Violation): this(violation.asList()) + constructor(violations: List): this(BaseTimeline(::Violations, preprocessList(violations, null))) + + /** + * Maps the list of associated activity ids on each violation. + * + * @param f a function which takes a [Violation] and returns a new list of ids. + */ + fun mapIds(f: (Violation) -> List) = unsafeMap(BoundsTransformer.IDENTITY, false) { it.withNewIds(f(it)) } + + /***/ companion object { + /** Creates a [Violations] object that violates when this profile equals a given value. */ + @JvmStatic fun SerialConstantOps.violateOn(v: V) = isolateEqualTo(v).violations() + + /** Creates a [Violations] object that violates when this profile equals a given value. */ + @JvmStatic fun Real.violateOn(n: Number) = equalTo(n).violateOn(true) + + /** + * Creates a [Violations] object that violates on every object in the timeline. + * + * If the object is an activity, it will record the directive or instance id. + */ + @JvmStatic fun > ParallelOps.violations() = + unsafeMap(::Violations, BoundsTransformer.IDENTITY, false) { + Violation( + it.interval, + listOfNotNull(it.getActivityId()) + ) + } + + /** Creates a [Violations] object that violates inside each interval. */ + @JvmStatic fun Windows.violateInside() = unsafeCast(::Intervals).violations() + /** Creates a [Violations] object that violates outside each interval. */ + @JvmStatic fun Windows.violateOutside() = complement().violateInside() + + /** + * Creates a [Violations] object from two timelines, that violates whenever they have overlap. + * + * If either object is an activity, it will record the directive or instance id. + */ + @JvmStatic infix fun , W: IntervalLike> GeneralOps.mutex(other: GeneralOps) = + unsafeMap2(::Violations, other) { l, r, i -> Violation( + i, + listOfNotNull( + l.getActivityId(), + r.getActivityId() + ) + )} + + private fun > V.getActivityId() = when (this) { + is Instance<*> -> InstanceId(id) + is Directive<*> -> DirectiveId(id) + else -> null + } + } +} + + diff --git a/timeline/MODULE_DOCS.md b/procedural/timeline/MODULE_DOCS.md similarity index 100% rename from timeline/MODULE_DOCS.md rename to procedural/timeline/MODULE_DOCS.md diff --git a/procedural/timeline/README.md b/procedural/timeline/README.md new file mode 100644 index 0000000000..3ad47c0cf3 --- /dev/null +++ b/procedural/timeline/README.md @@ -0,0 +1,27 @@ +# Timelines + +This library provides tools for querying and manipulating "timelines" from an Aerie plan or set of +simulation results. This includes things like resource profiles, activity instances, and activity directives, +but can be extended to support more kinds if needed. + +See [MODULE_DOCS.md](./MODULE_DOCS.md) for a description of the architecture and design of the library. + +- Building and testing: `./gradlew :procedural:timeline:build` +- Generating a jar for local experimentation: `./gradlew :procedural:timeline:shadowJar` + - jar will be available at `procedural/timeline/build/libs/timeline-all.jar` + +See `/procedural/README.md` for instructions on generating viewing documentation. + +## Potential future optimizations + +- **caching/memoization:** currently collecting a timeline twice will result in it being + computed twice. Automatically memoizing the results has potential for unsoundness, + but to be fair, so does the entire concept of evaluating on restricted bounds. But if + we do it right, it could give theoretically unbounded speedup for complex timelines. +- **inlining durations:** Turn the Duration class into an `inline value` class. This speeds + up the entire library by about 20%, based on a rudimentary benchmark. On the downside, + it makes the interface in Java use a bunch of longs instead of duration objects. Should + be possible to do cleanly, but will take some cleverness. +- **streaming:** rework the core to be create streams instead of lists. It should be somewhat faster, + but I don't know how much. The main utility is that with segment streaming from simulation, + you could evaluate timelines in parallel with simulation. diff --git a/timeline/build.gradle b/procedural/timeline/build.gradle similarity index 91% rename from timeline/build.gradle rename to procedural/timeline/build.gradle index 13ed1a2a17..5869350d05 100644 --- a/timeline/build.gradle +++ b/procedural/timeline/build.gradle @@ -35,9 +35,9 @@ kotlin { jvmToolchain(21) } -dokkaHtml.configure { +dokkaHtmlPartial.configure { dokkaSourceSets { - named("main") { + configureEach { // used as project name in the header moduleName.set("Timeline") @@ -49,10 +49,10 @@ dokkaHtml.configure { // adds source links that lead to this repository, allowing readers // to easily find source code for inspected declarations - sourceLink.configure { + sourceLink { localDirectory.set(file("timeline/src/main/kotlin")) - remoteUrl.set(URL( - "https://github.com/NASA-AMMOS/aerie/blob/feat/java-timeline-library/timeline/src/main/kotlin" + remoteUrl.set(new URL( + "https://github.com/NASA-AMMOS/aerie/blob/feat/java-timeline-library/timeline/src/main/kotlin/" )) remoteLineSuffix.set("#L") } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BaseTimeline.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BaseTimeline.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BaseTimeline.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BaseTimeline.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/NullBinaryOperation.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/NullBinaryOperation.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/NullBinaryOperation.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/NullBinaryOperation.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceIntervalsOp.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceIntervalsOp.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceIntervalsOp.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceIntervalsOp.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceNoOp.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceNoOp.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceNoOp.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceNoOp.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceSegmentsOp.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceSegmentsOp.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceSegmentsOp.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceSegmentsOp.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/NumericOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/NumericOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/NumericOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/NumericOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Coalesce.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Coalesce.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Coalesce.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Coalesce.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt b/procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt similarity index 100% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt rename to procedural/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformerTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformerTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformerTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformerTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/IntervalTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/IntervalTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/IntervalTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/IntervalTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/SegmentTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/SegmentTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/SegmentTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/SegmentTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/WindowsTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/WindowsTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/WindowsTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/WindowsTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/BooleansTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/BooleansTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/BooleansTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/BooleansTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/NumbersTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/NumbersTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/NumbersTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/NumbersTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/RealTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/RealTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/RealTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/RealTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOpsTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOpsTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOpsTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOpsTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOpsTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOpsTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOpsTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOpsTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOpsTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOpsTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOpsTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOpsTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOpsTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOpsTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOpsTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOpsTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOpsTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOpsTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOpsTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOpsTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/CoalesceTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/CoalesceTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/CoalesceTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/CoalesceTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/LinearEquationTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/LinearEquationTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/LinearEquationTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/LinearEquationTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/ListCollectorTest.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/ListCollectorTest.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/ListCollectorTest.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/ListCollectorTest.kt diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2Test.kt b/procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2Test.kt similarity index 100% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2Test.kt rename to procedural/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2Test.kt diff --git a/scheduler-driver/build.gradle b/scheduler-driver/build.gradle index fef5cca0f9..77686981ed 100644 --- a/scheduler-driver/build.gradle +++ b/scheduler-driver/build.gradle @@ -12,6 +12,9 @@ java { test { useJUnitPlatform() + testLogging { + exceptionFormat = 'full' + } } jacocoTestReport { diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java index 2cf23d0897..a214abb44a 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java @@ -8,7 +8,7 @@ import gov.nasa.jpl.aerie.merlin.driver.StartOffsetReducer; import gov.nasa.jpl.aerie.merlin.driver.engine.JobSchedule; import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; -import gov.nasa.jpl.aerie.merlin.driver.engine.TaskId; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; @@ -52,10 +52,10 @@ public class ResumableSimulationDriver implements AutoCloseable { private final Topic activityTopic = new Topic<>(); //mapping each activity name to its task id (in String form) in the simulation engine - private final Map plannedDirectiveToTask; + private final Map plannedDirectiveToTask; //subset of plannedDirectiveToTask to check for scheduling dependent tasks - private final Map toCheckForDependencyScheduling; + private final Map toCheckForDependencyScheduling; //simulation results so far private SimulationResults lastSimResults; @@ -317,7 +317,7 @@ private void simulateSchedule(final Map if (!plannedDirectiveToTask.isEmpty() && plannedDirectiveToTask .values() .stream() - .allMatch(engine::isTaskComplete)) { + .allMatch($ -> engine.getSpan($).isComplete())) { allTaskFinished = true; } @@ -340,7 +340,7 @@ private void simulateSchedule(final Map public Optional getActivityDuration(ActivityDirectiveId activityDirectiveId){ //potential cause of non presence: (1) activity is outside plan bounds (2) activity has not been simulated yet if(!plannedDirectiveToTask.containsKey(activityDirectiveId)) return Optional.empty(); - return engine.getTaskDuration(plannedDirectiveToTask.get(activityDirectiveId)); + return engine.getSpan(plannedDirectiveToTask.get(activityDirectiveId)).duration(); } private Set getSuccessorsToSchedule(final SimulationEngine engine) { @@ -348,7 +348,7 @@ private Set getSuccessorsToSchedule(final SimulationEngine final var iterator = toCheckForDependencyScheduling.entrySet().iterator(); while(iterator.hasNext()){ final var taskToCheck = iterator.next(); - if(engine.isTaskComplete(taskToCheck.getValue())){ + if(engine.getSpan(taskToCheck.getValue()).isComplete()){ toSchedule.add(taskToCheck.getKey()); iterator.remove(); } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java index 24f668ef9f..c6c47c1a72 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java @@ -17,6 +17,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; @@ -696,14 +697,14 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { Duration.ZERO, $ -> { try { - $.spawn(delayedActivityDirective.getTaskFactory(null, null)); + $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( ex.toString())); } return TaskStatus.delayed(Duration.of(120, Duration.SECOND), $$ -> { try { - $$.spawn(delayedActivityDirective.getTaskFactory(null, null)); + $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error( "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( diff --git a/settings.gradle b/settings.gradle index 95d0c6ce7e..e8d8c7311b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,7 +11,10 @@ include 'contrib' // Service support include 'parsing-utilities' include 'permissions' -include 'timeline' + +// Procedural post-simulation libraries +include 'procedural:timeline' +include 'procedural:constraints' // Services for deployment within the Aerie infrastructure include 'merlin-server' diff --git a/timeline/README.md b/timeline/README.md deleted file mode 100644 index 128439d7d1..0000000000 --- a/timeline/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Timelines - -This library provides tools for querying and manipulating "timelines" from an Aerie plan or set of -simulation results. This includes things like resource profiles, activity instances, and activity directives, -but can be extended to support more kinds if needed. - -See [MODULE_DOCS.md](./MODULE_DOCS.md) for a description of the architecture and design of the library. - -- Building and testing: `./gradlew :timeline:build` -- Generating a jar for local experimentation: `./gradlew :timeline:shadowJar` - - jar will be available at `timeline/build/libs/timeline-all.jar` -- Generating documentation: `./gradlew :timeline:dokkaHtml` - - docs will be available at `timeline/build/dokka/html/index.html`