diff --git a/deployment/hasura/migrations/Aerie/12_activity_goal_invocation_id/down.sql b/deployment/hasura/migrations/Aerie/12_activity_goal_invocation_id/down.sql new file mode 100644 index 0000000000..b3f97f5c99 --- /dev/null +++ b/deployment/hasura/migrations/Aerie/12_activity_goal_invocation_id/down.sql @@ -0,0 +1,6 @@ +alter table merlin.activity_directive + drop constraint activity_directive_source_invocation_id_exists, + + drop column source_scheduling_goal_invocation_id; + +call migrations.mark_migration_rolled_back('12'); diff --git a/deployment/hasura/migrations/Aerie/12_activity_goal_invocation_id/up.sql b/deployment/hasura/migrations/Aerie/12_activity_goal_invocation_id/up.sql new file mode 100644 index 0000000000..9739953df7 --- /dev/null +++ b/deployment/hasura/migrations/Aerie/12_activity_goal_invocation_id/up.sql @@ -0,0 +1,13 @@ +alter table merlin.activity_directive + add column source_scheduling_goal_invocation_id integer default null, + + add constraint activity_directive_source_invocation_id_exists + foreign key (source_scheduling_goal_invocation_id) + references scheduler.scheduling_specification_goals + on update cascade + on delete set null; + +comment on column merlin.activity_directive.source_scheduling_goal_invocation_id is e'' + 'The scheduling goal invocation that this activity_directive was generated by.'; + +call migrations.mark_migration_applied('12'); diff --git a/deployment/postgres-init-db/sql/tables/merlin/activity_directive/activity_directive.sql b/deployment/postgres-init-db/sql/tables/merlin/activity_directive/activity_directive.sql index 9509621387..172b658fdd 100644 --- a/deployment/postgres-init-db/sql/tables/merlin/activity_directive/activity_directive.sql +++ b/deployment/postgres-init-db/sql/tables/merlin/activity_directive/activity_directive.sql @@ -4,6 +4,7 @@ create table merlin.activity_directive ( name text, source_scheduling_goal_id integer, + source_scheduling_goal_invocation_id integer default null, created_at timestamptz not null default now(), created_by text, last_modified_at timestamptz not null default now(), @@ -38,6 +39,11 @@ create table merlin.activity_directive ( foreign key (created_by) references permissions.users on update cascade + on delete set null, + constraint activity_directive_source_invocation_id_exists + foreign key (source_scheduling_goal_invocation_id) + references scheduler.scheduling_specification_goals + on update cascade on delete set null ); @@ -56,6 +62,8 @@ comment on column merlin.activity_directive.name is e'' 'The name of this activity_directive.'; comment on column merlin.activity_directive.source_scheduling_goal_id is e'' 'The scheduling goal that this activity_directive was generated by.'; +comment on column merlin.activity_directive.source_scheduling_goal_invocation_id is e'' + 'The scheduling goal invocation that this activity_directive was generated by.'; comment on column merlin.activity_directive.created_at is e'' 'The time at which this activity_directive was created.'; comment on column merlin.activity_directive.created_by is e'' diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ActivityAutoDeletionGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ActivityAutoDeletionGoal.java new file mode 100644 index 0000000000..d8f736d8e2 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ActivityAutoDeletionGoal.java @@ -0,0 +1,43 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.scheduling.ActivityAutoDelete; +import gov.nasa.ammos.aerie.procedural.scheduling.Goal; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.DeletedAnchorStrategy; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan; +import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +/** + * Creates one activity, and deletes it automatically on subsequent runs. + */ +@SchedulingProcedure +public record ActivityAutoDeletionGoal(boolean deleteAtBeginning) implements Goal { + @NotNull + @Override + public ActivityAutoDelete shouldDeletePastCreations( + @NotNull final Plan plan, + @Nullable final SimulationResults simResults) + { + if (deleteAtBeginning) return new ActivityAutoDelete.AtBeginning(DeletedAnchorStrategy.Error, false); + else return new ActivityAutoDelete.JustBefore(DeletedAnchorStrategy.Error); + } + + @Override + public void run(@NotNull final EditablePlan plan) { + plan.create( + "BiteBanana", + new DirectiveStart.Absolute(Duration.MINUTE), + Map.of("biteSize", SerializedValue.of(1)) + ); + + plan.commit(); + } +} diff --git a/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ActivityDeletionGoal.java b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ActivityDeletionGoal.java new file mode 100644 index 0000000000..874e6a5bc1 --- /dev/null +++ b/e2e-tests/src/main/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/procedures/ActivityDeletionGoal.java @@ -0,0 +1,47 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.scheduling.Goal; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.DeletedAnchorStrategy; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.types.ActivityDirectiveId; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Optional; + +/** + * Creates three activities in a chain of anchors, then deletes one. + */ +@SchedulingProcedure +public record ActivityDeletionGoal(int whichToDelete, DeletedAnchorStrategy anchorStrategy) implements Goal { + @Override + public void run(@NotNull final EditablePlan plan) { + final var ids = new ActivityDirectiveId[3]; + + ids[0] = plan.create( + "BiteBanana", + new DirectiveStart.Absolute(Duration.HOUR), + Map.of("biteSize", SerializedValue.of(0)) + ); + ids[1] = plan.create( + "BiteBanana", + new DirectiveStart.Anchor(ids[0], Duration.HOUR, DirectiveStart.Anchor.AnchorPoint.End), + Map.of("biteSize", SerializedValue.of(1)) + ); + ids[2] = plan.create( + "BiteBanana", + new DirectiveStart.Anchor(ids[1], Duration.HOUR, DirectiveStart.Anchor.AnchorPoint.Start), + Map.of("biteSize", SerializedValue.of(2)) + ); + + if (whichToDelete >= 0) { + plan.delete(ids[whichToDelete], anchorStrategy); + } + + plan.commit(); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/AutoDeletionTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/AutoDeletionTests.java new file mode 100644 index 0000000000..e3e7fbb3a6 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/AutoDeletionTests.java @@ -0,0 +1,135 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling; + +import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; +import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.json.Json; +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AutoDeletionTests extends ProceduralSchedulingSetup { + private GoalInvocationId edslId; + private GoalInvocationId procedureId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + final String coexGoalDefinition = + """ + export default function myGoal() { + return Goal.CoexistenceGoal({ + forEach: ActivityExpression.ofType(ActivityTypes.BiteBanana), + activityTemplate: ActivityTemplates.GrowBanana({quantity: 1, growingDuration: Temporal.Duration.from({minutes:1})}), + startsAt:TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes : 5})) + }) + }"""; + + edslId = hasura.createSchedulingSpecGoal( + "Coexistence Scheduling Test Goal", + coexGoalDefinition, + "", + specId, + 0, + false + ); + + int procedureJarId = gateway.uploadJarFile("build/libs/ActivityAutoDeletionGoal.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 1, + false + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteSchedulingGoal(procedureId.goalId()); + hasura.deleteSchedulingGoal(edslId.goalId()); + } + + @Test + void createsOneActivityIfRunOnce() throws IOException { + final var args = Json + .createObjectBuilder() + .add("deleteAtBeginning", false) + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(1, activities.size()); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") + )); + } + + @Test + void createsTwoActivitiesSteadyState_JustBefore() throws IOException { + final var args = Json + .createObjectBuilder() + .add("deleteAtBeginning", false) + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + for (int i = 0; i < 3; i++) { + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(2, activities.size()); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") + )); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "GrowBanana") + )); + } + } + + @Test + void createsOneActivitySteadyState_AtBeginning() throws IOException { + final var args = Json + .createObjectBuilder() + .add("deleteAtBeginning", true) + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + for (int i = 0; i < 3; i++) { + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(1, activities.size()); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") + )); + } + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java index 71fb6aae76..6a8b8d7f04 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/BasicTests.java @@ -134,6 +134,14 @@ void executeEDSLAndProcedure() throws IOException { final var args = Json.createObjectBuilder().add("quantity", 4).build(); hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + final String recurrenceGoalDefinition = + """ + export default function myGoal() { + return Goal.ActivityRecurrenceGoal({ + activityTemplate: ActivityTemplates.PeelBanana({peelDirection: 'fromStem'}), + interval: Temporal.Duration.from({hours:1}) + })}"""; + hasura.createSchedulingSpecGoal( "Recurrence Scheduling Test Goal", recurrenceGoalDefinition, diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DeletionTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DeletionTests.java new file mode 100644 index 0000000000..5e38b57b19 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/DeletionTests.java @@ -0,0 +1,215 @@ +package gov.nasa.jpl.aerie.e2e.procedural.scheduling; + +import gov.nasa.jpl.aerie.e2e.types.GoalInvocationId; +import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.json.Json; +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DeletionTests extends ProceduralSchedulingSetup { + private GoalInvocationId procedureId; + + @BeforeEach + void localBeforeEach() throws IOException { + try (final var gateway = new GatewayRequests(playwright)) { + int procedureJarId = gateway.uploadJarFile("build/libs/ActivityDeletionGoal.jar"); + // Add Scheduling Procedure + procedureId = hasura.createSchedulingSpecProcedure( + "Test Scheduling Procedure", + procedureJarId, + specId, + 0 + ); + } + } + + @AfterEach + void localAfterEach() throws IOException { + hasura.deleteSchedulingGoal(procedureId.goalId()); + } + + @Test + void createsThreeActivities() throws IOException { + final var args = Json + .createObjectBuilder() + .add("whichToDelete", -1) + .add("anchorStrategy", "Error") + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(3, activities.size()); + + final AtomicReference id1 = new AtomicReference<>(); + final AtomicReference id2 = new AtomicReference<>(); + assertTrue(activities.stream().anyMatch( + it -> { + final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null); + if (result) id1.set(it.id()); + return result; + } + )); + + assertTrue(activities.stream().anyMatch( + it -> { + final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id1.get()); + if (result) id2.set(it.id()); + return result; + } + )); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id2.get()) + )); + } + + @Test + void deletesLast() throws IOException { + final var args = Json + .createObjectBuilder() + .add("whichToDelete", 2) + .add("anchorStrategy", "Error") + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(2, activities.size()); + + final AtomicReference id1 = new AtomicReference<>(); + assertTrue(activities.stream().anyMatch( + it -> { + final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null); + if (result) id1.set(it.id()); + return result; + } + )); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id1.get()) + )); + } + + @Test + void deletesMiddleCascade() throws IOException { + final var args = Json + .createObjectBuilder() + .add("whichToDelete", 1) + .add("anchorStrategy", "Cascade") + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(1, activities.size()); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null) + )); + } + + @Test + void deletesMiddleAnchorToParent() throws IOException { + final var args = Json + .createObjectBuilder() + .add("whichToDelete", 1) + .add("anchorStrategy", "ReAnchor") + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(2, activities.size()); + + final AtomicReference id1 = new AtomicReference<>(); + assertTrue(activities.stream().anyMatch( + it -> { + final var result = Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), null); + if (result) id1.set(it.id()); + return result; + } + )); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") + && Objects.equals(it.anchorId(), id1.get()) + && Objects.equals(it.startOffset(), "02:00:00") + )); + } + + @Test + void deletesFirstCascade() throws IOException { + final var args = Json + .createObjectBuilder() + .add("whichToDelete", 0) + .add("anchorStrategy", "Cascade") + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(0, activities.size()); + } + + @Test + void deletesFirstReAnchorToPlan() throws IOException { + final var args = Json + .createObjectBuilder() + .add("whichToDelete", 0) + .add("anchorStrategy", "ReAnchor") + .build(); + + hasura.updateSchedulingSpecGoalArguments(procedureId.invocationId(), args); + + hasura.awaitScheduling(specId); + + final var plan = hasura.getPlan(planId); + final var activities = plan.activityDirectives(); + + assertEquals(2, activities.size()); + + final AtomicReference id2 = new AtomicReference<>(); + assertTrue(activities.stream().anyMatch( + it -> { + final var result = Objects.equals(it.type(), "BiteBanana") + && Objects.equals(it.anchorId(), null) + && Objects.equals(it.startOffset(), "02:00:00"); + if (result) id2.set(it.id()); + return result; + } + )); + + assertTrue(activities.stream().anyMatch( + it -> Objects.equals(it.type(), "BiteBanana") && Objects.equals(it.anchorId(), id2.get()) + )); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java index c1ef5853b1..5ab2db69d8 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/procedural/scheduling/ProceduralSchedulingSetup.java @@ -26,13 +26,7 @@ public abstract class ProceduralSchedulingSetup { // Cross-Test Constants protected final String planStartTimestamp = "2023-01-01T00:00:00+00:00"; - protected final String recurrenceGoalDefinition = - """ - export default function myGoal() { - return Goal.ActivityRecurrenceGoal({ - activityTemplate: ActivityTemplates.PeelBanana({peelDirection: 'fromStem'}), - interval: Temporal.Duration.from({hours:1}) - })}"""; + @BeforeAll void beforeAll() { diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/Plan.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/Plan.java index 4fb0d3ee80..5fb5d74501 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/Plan.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/Plan.java @@ -12,7 +12,16 @@ public record Plan( int revision, List activityDirectives ) { - public record ActivityDirective(int id, int planId, String type, String startOffset, JsonObject arguments, String name) { + public record ActivityDirective( + int id, + int planId, + String type, + String startOffset, + JsonObject arguments, + String name, + Integer anchorId, + boolean anchoredToStart + ) { public static ActivityDirective fromJSON(JsonObject json){ return new ActivityDirective( json.getInt("id"), @@ -20,7 +29,9 @@ public static ActivityDirective fromJSON(JsonObject json){ json.getString("type"), json.getString("startOffset"), json.getJsonObject("arguments"), - json.getString("name") + json.getString("name"), + json.isNull("anchorId") ? null : json.getInt("anchorId"), + json.getBoolean("anchoredToStart") ); } } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index 8d07e3fa95..7c8c1ec9ce 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -398,6 +398,8 @@ query GetPlan($id: Int!) { startOffset: start_offset type name + anchorId: anchor_id + anchoredToStart: anchored_to_start } constraint_specification { constraint_id diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index 71b5dff661..e11241b0ec 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -710,12 +710,22 @@ public void updatePlanRevisionSchedulingSpec(int planId) throws IOException { makeRequest(GQL.UPDATE_SCHEDULING_SPECIFICATION_PLAN_REVISION, variables); } - public GoalInvocationId createSchedulingSpecProcedure( String name, int jarId, int specificationId, int priority + ) throws IOException { + return createSchedulingSpecProcedure(name, jarId, specificationId, priority, true); + } + + + public GoalInvocationId createSchedulingSpecProcedure( + String name, + int jarId, + int specificationId, + int priority, + boolean simulateAfter ) throws IOException { final var specGoalBuilder = Json.createObjectBuilder() .add("goal_metadata", @@ -733,7 +743,8 @@ public GoalInvocationId createSchedulingSpecProcedure( .add("uploaded_jar_id", jarId) ))))) .add("specification_id", specificationId) - .add("priority", priority); + .add("priority", priority) + .add("simulate_after", simulateAfter); final var variables = Json.createObjectBuilder().add("spec_goal", specGoalBuilder).build(); final var resp = makeRequest(GQL.CREATE_SCHEDULING_SPEC_GOAL, variables) .getJsonObject("insert_scheduling_specification_goals_one"); @@ -768,6 +779,18 @@ public GoalInvocationId createSchedulingSpecGoal( String description, int specificationId, int priority + ) throws IOException + { + return createSchedulingSpecGoal(name, definition, description, specificationId, priority, true); + } + + public GoalInvocationId createSchedulingSpecGoal( + String name, + String definition, + String description, + int specificationId, + int priority, + boolean simulateAfter ) throws IOException { final var specGoalBuilder = Json.createObjectBuilder() .add("goal_metadata", @@ -783,6 +806,7 @@ public GoalInvocationId createSchedulingSpecGoal( .add(Json.createObjectBuilder() .add("definition", definition)))))) .add("specification_id", specificationId) + .add("simulate_after", simulateAfter) .add("priority", priority); final var variables = Json.createObjectBuilder().add("spec_goal", specGoalBuilder).build(); final var resp = makeRequest(GQL.CREATE_SCHEDULING_SPEC_GOAL, variables) diff --git a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/GrowBananaActivity.java b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/GrowBananaActivity.java index 57489ccabb..5e6e2c8311 100644 --- a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/GrowBananaActivity.java +++ b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/GrowBananaActivity.java @@ -40,7 +40,7 @@ public boolean validateGrowingDuration() { @EffectModel @ControllableDuration(parameterName = "growingDuration") public void run(final Mission mission) { - final var rate = this.quantity() / (double) this.growingDuration().in(Duration.SECONDS); + final var rate = this.quantity() / (double) this.growingDuration().ratioOver(Duration.SECOND); mission.fruit.rate.add(rate); delay(this.growingDuration()); mission.fruit.rate.add(-rate); diff --git a/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedSimulationResults.kt b/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedSimulationResults.kt index 4b783efd21..84b795c095 100644 --- a/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedSimulationResults.kt +++ b/procedural/constraints/src/test/kotlin/gov/nasa/ammos/aerie/procedural/constraints/NotImplementedSimulationResults.kt @@ -1,6 +1,7 @@ package gov.nasa.ammos.aerie.procedural.constraints import gov.nasa.ammos.aerie.procedural.timeline.Interval +import gov.nasa.ammos.aerie.procedural.timeline.collections.Directives import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment @@ -15,4 +16,5 @@ open class NotImplementedSimulationResults: SimulationResults { deserializer: (List>) -> TL ): TL = TODO() override fun instances(type: String?, deserializer: (SerializedValue) -> A): Instances = TODO() + override fun inputDirectives(deserializer: (SerializedValue) -> A) = TODO() } diff --git a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/ActivityAutoDelete.kt b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/ActivityAutoDelete.kt new file mode 100644 index 0000000000..9ea4f79d9a --- /dev/null +++ b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/ActivityAutoDelete.kt @@ -0,0 +1,9 @@ +package gov.nasa.ammos.aerie.procedural.scheduling + +import gov.nasa.ammos.aerie.procedural.scheduling.plan.DeletedAnchorStrategy + +sealed interface ActivityAutoDelete { + data class AtBeginning(val anchorStrategy: DeletedAnchorStrategy, val simulateAfter: Boolean): ActivityAutoDelete + data class JustBefore(val anchorStrategy: DeletedAnchorStrategy): ActivityAutoDelete + data object No: ActivityAutoDelete +} diff --git a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/Goal.kt b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/Goal.kt index f4014d2cf0..4d6a0c39f1 100644 --- a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/Goal.kt +++ b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/Goal.kt @@ -1,9 +1,23 @@ package gov.nasa.ammos.aerie.procedural.scheduling import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan +import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan +import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults /** The interface that all scheduling rules must satisfy. */ interface Goal { + /** + * Whether the scheduler should delete this goal's past created activities. + * + * Default implementation returns [ActivityAutoDelete.No]. Override this method + * to specify otherwise and choose a strategy for deleted anchors. + * + * This method may be called multiple times during the scheduling run, and must return the + * same result every time. All calls to this method and [run] during a scheduling run + * will be performed on the same object instance. + */ + fun shouldDeletePastCreations(plan: Plan, simResults: SimulationResults?): ActivityAutoDelete = ActivityAutoDelete.No + /** * Run the rule. * diff --git a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/DeletedAnchorStrategy.kt b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/DeletedAnchorStrategy.kt new file mode 100644 index 0000000000..173ed876dd --- /dev/null +++ b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/DeletedAnchorStrategy.kt @@ -0,0 +1,27 @@ +package gov.nasa.ammos.aerie.procedural.scheduling.plan + +/** + * How to handle directives anchored to a deleted activity. + * + * If you intend to delete an activity that you believe has nothing anchored to it, + * using [Error] is recommended. This is the default. + */ +enum class DeletedAnchorStrategy { + /** Throw an error. */ Error, + /** Recursively delete everything in the anchor chain. */ Cascade, + + /** + * Attempt to delete the activity in-place without changing the start times + * of any activities anchored to it. + * + * Consider the anchor chain `A <- B <- C`, where `A` starts at an absolute time and + * `B` and `C` are anchored. + * - If `A` is deleted with [ReAnchor], `B` will be set to start at the absolute time `A.startTime + B.offset`. + * `C` will be unchanged. + * - If `B` is deleted with [ReAnchor], `C` will be anchored to `A` with a new offset equal to `B.offset + C.offset`. + * + * If an activity is anchored to the end of the deleted activity, the delete activity's duration is assumed to be 0, + * which may change the ultimate start time of the anchored activity. + */ + ReAnchor, +} diff --git a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/Edit.kt b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/Edit.kt index 2edcca9fb5..9e1fac6fdf 100644 --- a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/Edit.kt +++ b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/Edit.kt @@ -2,13 +2,28 @@ package gov.nasa.ammos.aerie.procedural.scheduling.plan import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Directive +import gov.nasa.jpl.aerie.types.ActivityDirectiveId /** * Edits that can be made to the plan. * - * Currently only creating new activities is supported. + * All edits are invertible. */ sealed interface Edit { + /** + * Returns the reverse operation. + * + * If both `E` and `E.inverse()` are applied, the plan is unchanged. + */ + fun inverse(): Edit + /** Create a new activity from a given directive. */ - data class Create(/***/ val directive: Directive): Edit + data class Create(/***/ val directive: Directive): Edit { + override fun inverse() = Delete(directive) + } + + /** Delete an activity, specified by directive id. */ + data class Delete(/***/ val directive: Directive): Edit { + override fun inverse() = Create(directive) + } } diff --git a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/EditablePlan.kt b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/EditablePlan.kt index 5a6e87b638..c86b78f19d 100644 --- a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/EditablePlan.kt +++ b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/plan/EditablePlan.kt @@ -3,6 +3,7 @@ package gov.nasa.ammos.aerie.procedural.scheduling.plan import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue import gov.nasa.ammos.aerie.procedural.scheduling.simulation.SimulateOptions import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Directive import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults @@ -33,6 +34,27 @@ interface EditablePlan: Plan { start )) + /** Delete an activity specified by directive id, and throw an error if any activities are anchored to it. */ + fun delete(id: ActivityDirectiveId) = delete(id, DeletedAnchorStrategy.Error) + + /** + * Delete an activity specified by directive id, with a strategy to handle activities that are anchored to it. + * + * If other anchored activities are affected, extra addition and deletion edits may be created. + */ + fun delete(id: ActivityDirectiveId, strategy: DeletedAnchorStrategy) + + /** Delete an activity and throw an error if any activities are anchored to it. */ + fun delete(directive: Directive) = delete(directive, DeletedAnchorStrategy.Error) + + /** + * Delete an activity with a strategy to handle activities that are anchored to it. + * + * If other anchored activities are affected, extra addition and deletion edits may be created. + */ + fun delete(directive: Directive, strategy: DeletedAnchorStrategy) + + /** Commit plan edits, making them final. */ fun commit() diff --git a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/utils/EasyEditablePlanDriver.kt b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/utils/EasyEditablePlanDriver.kt new file mode 100644 index 0000000000..fb03bf44a9 --- /dev/null +++ b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/utils/EasyEditablePlanDriver.kt @@ -0,0 +1,312 @@ +package gov.nasa.ammos.aerie.procedural.scheduling.utils + +import gov.nasa.ammos.aerie.procedural.scheduling.plan.* +import gov.nasa.ammos.aerie.procedural.scheduling.simulation.SimulateOptions +import gov.nasa.ammos.aerie.procedural.timeline.collections.Directives +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Directive +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart +import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan +import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults +import gov.nasa.jpl.aerie.types.ActivityDirectiveId +import java.lang.ref.WeakReference + +/** + * A default (but optional) driver for [EditablePlan] implementations that handles + * commits/rollbacks, staleness checking, and anchor deletion automatically. + * + * The [EditablePlan] interface requires the implementor to perform some fairly complex + * stateful operations, with a tangle of interdependent algorithmic guarantees. + * Most of those operations are standard among all implementations though, so this driver + * captures most of it in a reusable form. Just inherit from this class to make a valid + * [EditablePlan]. + * + * The subclass is still responsible for simulation and the basic context-free creation + * and deletion operations. See the *Contracts* section of each abstract method's doc comment. + */ +/* + * ## Staleness checking + * + * The editable plan instance keeps track of sim results that it has produced using weak references, and can dynamically + * update their staleness if the plan is changed after it was simulated. The process is this: + * + * 1. [InMemoryEditablePlan] has a set of weak references to simulation results objects that are currently up-to-date. + * I used weak references because if the user can't access it anymore, staleness doesn't matter and we might as well + * let it get gc'ed. + * 2. When the user gets simulation results, either through simulation or by getting the latest, it always checks for + * plan equality between the returned results and the current plan, even if we just simulated. If it is up-to-date, a + * weak ref is added to the set. + * 3. When an edit is made, the sim results in the current set are marked stale; then the set is reset to new reference + * to an empty set. + * 4. When a commit is made, the commit object takes *shared ownership* of the set. If a new simulation is run (step 2) + * the plan can still add to the set while it is still jointly owned by the commit. Then when an edit is made (step 3) + * the commit will become the sole owner of the set. + * 5. When changes are rolled back, any sim results currently in the plan's set are marked stale, the previous commit's + * sim results are marked not stale, then the plan will resume joint ownership of the previous commit's set. + * + * The joint ownership freaks me out a wee bit, but I think it's safe because the commits are only used to keep the + * previous sets from getting gc'ed in the event of a rollback. Only the plan object actually mutates the set. + */ +abstract class EasyEditablePlanDriver( + private val plan: Plan +): EditablePlan, Plan by plan { + /** + * Create a unique directive ID. + * + * *Contract:* + * - the implementor must return an ID that is distinct from any activity ID that was in the initial plan + * or that has been returned from this method before during the implementor's lifetime. + */ + protected abstract fun generateDirectiveId(): ActivityDirectiveId + + /** + * Create a directive in the plan. + * + * *Contracts*: + * - the driver will guarantee that the directive ID does not collide with any other directive currently in the plan. + * - the implementor must return the new directive in future calls to [Plan.directives], unless it is later deleted. + * - the implementor must include the directive in future input plans for simulation, unless it is later deleted. + */ + protected abstract fun createInternal(directive: Directive) + + /** + * Remove a directive from the plan, specified by ID. + */ + protected abstract fun deleteInternal(id: ActivityDirectiveId) + + /** + * Get the latest simulation results. + * + * *Contract:* + * - the implementor must return equivalent results objects if this method is called multiple times without + * updates. + * + * The implementor doesn't have to return the exact same *instance* each time if no updates are made (i.e. referential + * equality isn't required, only structural equality). + */ + protected abstract fun latestResultsInternal(): PerishableSimulationResults? + + /** + * Simulate the current plan. + * + * *Contracts:* + * - all prior creations and deletions must be reflected in the simulation run. + * - the results corresponding to this run must be returned from future calls to [latestResultsInternal] + * until the next time [simulateInternal] is called. + */ + protected abstract fun simulateInternal(options: SimulateOptions) + + /** + * Optional validation hook for new activities. + * + * The default implementation checks if the activity is within the bounds of the plan. The implementor can + * add additional checks by overriding this method and calling `super.validate(directive)`. Implementor + * should throw if the directive is invalid. + */ + protected open fun validate(directive: Directive) { + if (directive.startTime > duration()) { + throw Exception("New activity with id ${directive.id.id()} would start after the end of the plan") + } + if (directive.start is DirectiveStart.Absolute && directive.startTime.isNegative) { + throw Exception("New activity with id ${directive.id.id()} would start before the beginning of the plan") + } + } + + private data class Commit( + val diff: Set, + + /** + * A record of the simulation results objects that were up-to-date when the commit + * was created. + * + * This has SHARED OWNERSHIP with [EasyEditablePlanDriver]; the editable plan may add more to + * this list AFTER the commit is created. + */ + val upToDateSimResultsSet: MutableSet> + ) + + private var committedChanges = Commit(setOf(), mutableSetOf()) + private var uncommittedChanges = mutableListOf() + + /** Whether there are uncommitted changes. */ + val isDirty + get() = uncommittedChanges.isNotEmpty() + + /** The total reduced set of changes made to the plan. */ + val totalDiff: Set + get() = committedChanges.diff + + // Jointly owned set of up-to-date simulation results. See class-level comment for algorithm explanation. + private var upToDateSimResultsSet: MutableSet> = mutableSetOf() + + override fun latestResults(): SimulationResults? { + val internalResults = latestResultsInternal() + + // kotlin checks structural equality by default, not referential equality. + val isStale = internalResults?.inputDirectives()?.toSet() != directives().toSet() + + internalResults?.setStale(isStale) + + if (!isStale) upToDateSimResultsSet.add(WeakReference(internalResults)) + return internalResults + } + + override fun create(directive: NewDirective): ActivityDirectiveId { + class ParentSearchException(id: ActivityDirectiveId, size: Int): Exception("Expected one parent activity with id $id, found $size") + val id = generateDirectiveId() + val parent = when (val s = directive.start) { + is DirectiveStart.Anchor -> { + val parentList = directives() + .filter { it.id == s.parentId } + .collect(totalBounds()) + if (parentList.size != 1) throw ParentSearchException(s.parentId, parentList.size) + parentList.first() + } + is DirectiveStart.Absolute -> null + } + val resolved = directive.resolve(id, parent) + uncommittedChanges.add(Edit.Create(resolved)) + + validate(resolved) + + createInternal(resolved) + + for (simResults in upToDateSimResultsSet) { + simResults.get()?.setStale(true) + } + // create a new list instead of `.clear` because commit objects have the same reference + upToDateSimResultsSet = mutableSetOf() + + return id + } + + override fun delete(directive: Directive, strategy: DeletedAnchorStrategy) { + val directives = directives().cache() + + + val directivesToDelete: Set> + val directivesToCreate: Set> + + if (strategy == DeletedAnchorStrategy.Cascade) { + directivesToDelete = deleteCascadeRecursive(directive, directives).toSet() + directivesToCreate = mutableSetOf() + } else { + directivesToDelete = mutableSetOf(directive) + directivesToCreate = mutableSetOf() + for (d in directives) { + when (val childStart = d.start) { + is DirectiveStart.Anchor -> { + if (childStart.parentId == directive.id) { + when (strategy) { + DeletedAnchorStrategy.Error -> throw Exception("Cannot delete an activity that has anchors pointing to it without a ${DeletedAnchorStrategy::class.java.simpleName}") + DeletedAnchorStrategy.ReAnchor -> { + directivesToDelete.add(d) + val start = when (val parentStart = directive.start) { + is DirectiveStart.Absolute -> DirectiveStart.Absolute(parentStart.time + childStart.offset) + is DirectiveStart.Anchor -> DirectiveStart.Anchor( + parentStart.parentId, + parentStart.offset + childStart.offset, + parentStart.anchorPoint, + childStart.estimatedStart + ) + } + directivesToCreate.add(d.copy(start = start)) + } + else -> throw Error("internal error; unreachable") + } + } + } + else -> {} + } + } + } + + for (d in directivesToDelete) { + uncommittedChanges.add(Edit.Delete(d)) + deleteInternal(d.id) + } + for (d in directivesToCreate) { + uncommittedChanges.add(Edit.Create(d)) + createInternal(d) + } + + for (simResults in upToDateSimResultsSet) { + simResults.get()?.setStale(true) + } + + upToDateSimResultsSet = mutableSetOf() + } + + private fun deleteCascadeRecursive(directive: Directive, allDirectives: Directives): List> { + val recurse = allDirectives.collect().flatMap { d -> + when (val s = d.start) { + is DirectiveStart.Anchor -> { + if (s.parentId == directive.id) deleteCascadeRecursive(d, allDirectives) + else listOf() + } + else -> listOf() + } + } + return recurse + listOf(directive) + } + + override fun delete(id: ActivityDirectiveId, strategy: DeletedAnchorStrategy) { + val matchingDirectives = plan.directives().filter { it.id == id }.collect() + if (matchingDirectives.isEmpty()) throw Exception("attempted to delete activity by ID that does not exist: $id") + if (matchingDirectives.size > 1) throw Exception("multiple activities with ID found: $id") + + delete(matchingDirectives.first(), strategy) + } + + override fun commit() { + // Early return if there are no changes. This prevents multiple commits from sharing ownership of the set, + // because new sets are only created when edits are made. + // Probably unnecessary, but shared ownership freaks me out enough already. + if (uncommittedChanges.isEmpty()) return + + val newCommittedChanges = uncommittedChanges + val newTotalDiff = committedChanges.diff.toMutableSet() + + for (newChange in newCommittedChanges) { + val inverse = newChange.inverse() + if (newTotalDiff.contains(inverse)) { + newTotalDiff.remove(inverse) + } else { + newTotalDiff.add(newChange) + } + } + + uncommittedChanges = mutableListOf() + + // Create a commit that shares ownership of the simResults set. + committedChanges = Commit(newTotalDiff, upToDateSimResultsSet) + } + + override fun rollback(): List { + // Early return if there are no changes, to keep staleness accuracy + if (uncommittedChanges.isEmpty()) return emptyList() + + val result = uncommittedChanges + uncommittedChanges = mutableListOf() + for (edit in result) { + when (edit) { + is Edit.Create -> deleteInternal(edit.directive.id) + is Edit.Delete -> createInternal(edit.directive) + } + } + for (simResult in upToDateSimResultsSet) { + simResult.get()?.setStale(true) + } + for (simResult in committedChanges.upToDateSimResultsSet) { + simResult.get()?.setStale(false) + } + upToDateSimResultsSet = committedChanges.upToDateSimResultsSet + return result + } + + override fun simulate(options: SimulateOptions): SimulationResults { + simulateInternal(options) + return latestResults()!! + } + +} diff --git a/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/utils/PerishableSimulationResults.kt b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/utils/PerishableSimulationResults.kt new file mode 100644 index 0000000000..b60425270c --- /dev/null +++ b/procedural/scheduling/src/main/kotlin/gov/nasa/ammos/aerie/procedural/scheduling/utils/PerishableSimulationResults.kt @@ -0,0 +1,8 @@ +package gov.nasa.ammos.aerie.procedural.scheduling.utils + +import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults + +/** Simulation results whose staleness can be changed after creation. */ +interface PerishableSimulationResults: SimulationResults { + /***/ fun setStale(stale: Boolean) +} diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/payloads/activities/DirectiveStart.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/payloads/activities/DirectiveStart.kt index c799ab36a4..ef37385f51 100644 --- a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/payloads/activities/DirectiveStart.kt +++ b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/payloads/activities/DirectiveStart.kt @@ -44,5 +44,19 @@ sealed interface DirectiveStart { } override fun atNewTime(time: Duration) = Anchor(parentId, offset + time - estimatedStart, anchorPoint, time) + + // Override equality so that it doesn't check `estimatedStart`. Start estimate is not part of the source of truth. + override fun equals(other: Any?) = when (other) { + is Anchor -> parentId == other.parentId && offset == other.offset && anchorPoint == other.anchorPoint + else -> false + } + + // Override hashing so that it doesn't include `estimatedStart`. + override fun hashCode(): Int { + var result = parentId.hashCode() + result = 31 * result + offset.hashCode() + result = 31 * result + anchorPoint.hashCode() + return result + } } } diff --git a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt index 58896179b1..fdf86f5c7d 100644 --- a/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt +++ b/procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/plan/SimulationResults.kt @@ -2,10 +2,12 @@ package gov.nasa.ammos.aerie.procedural.timeline.plan import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue import gov.nasa.ammos.aerie.procedural.timeline.Interval +import gov.nasa.ammos.aerie.procedural.timeline.collections.Directives import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyInstance import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective /** An interface for querying plan information and simulation results. */ interface SimulationResults { @@ -34,4 +36,9 @@ interface SimulationResults { fun instances(type: String) = instances(type, AnyInstance.deserializer()) /** Queries all activity instances, deserializing them as [AnyInstance]. **/ fun instances() = instances(null, AnyInstance.deserializer()) + + /** The input directives that were used for this simulation. */ + fun inputDirectives(deserializer: (SerializedValue) -> A): Directives + /** The input directives that were used for this simulation, deserialized as [AnyDirective]. */ + fun inputDirectives() = inputDirectives(AnyDirective.deserializer()) } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java index 306aff1468..88293e1e9f 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Procedure.java @@ -1,5 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.goals; +import gov.nasa.ammos.aerie.procedural.scheduling.ActivityAutoDelete; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.DeletedAnchorStrategy; import gov.nasa.ammos.aerie.procedural.timeline.payloads.ExternalEvent; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; @@ -8,6 +10,7 @@ import gov.nasa.jpl.aerie.scheduler.DirectiveIdGenerator; import gov.nasa.jpl.aerie.scheduler.ProcedureLoader; import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; @@ -17,11 +20,13 @@ import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.ConflictSatisfaction; import gov.nasa.jpl.aerie.scheduler.solver.Evaluation; +import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; import static gov.nasa.jpl.aerie.scheduler.plan.InMemoryEditablePlan.toSchedulingActivity; @@ -30,16 +35,37 @@ public class Procedure extends Goal { private final Path jarPath; private final Map args; - public Procedure(final PlanningHorizon planningHorizon, Path jarPath, Map args, boolean simulateAfter) { + private gov.nasa.ammos.aerie.procedural.scheduling.Goal goal; + + private ActivityAutoDelete shouldDelete; + private final GoalId goalId; + + public Procedure( + final PlanningHorizon planningHorizon, + Path jarPath, + Map args, + boolean simulateAfter, + GoalId goalId + ) { this.simulateAfter = simulateAfter; this.planHorizon = planningHorizon; this.jarPath = jarPath; this.args = args; + this.goalId = goalId; } - public void run( + public void prepare() { + final ProcedureMapper procedureMapper; + try { + procedureMapper = ProcedureLoader.loadProcedure(jarPath); + } catch (ProcedureLoader.ProcedureLoadException e) { + throw new RuntimeException(e); + } + this.goal = procedureMapper.deserialize(SerializedValue.of(this.args)); + } + + public boolean deleteAtBeginning( final Problem problem, - final Evaluation eval, final Plan plan, final MissionModel missionModel, final Function lookupActivityType, @@ -47,13 +73,43 @@ public void run( final DirectiveIdGenerator idGenerator, Map> eventsByDerivationGroup ) { - final ProcedureMapper procedureMapper; - try { - procedureMapper = ProcedureLoader.loadProcedure(jarPath); - } catch (ProcedureLoader.ProcedureLoadException e) { - throw new RuntimeException(e); + final var planAdapter = new SchedulerToProcedurePlanAdapter( + plan, + planHorizon, + eventsByDerivationGroup, + problem.getDiscreteExternalProfiles(), + problem.getRealExternalProfiles() + ); + + final var editablePlan = new InMemoryEditablePlan( + missionModel, + idGenerator, + planAdapter, + simulationFacade, + lookupActivityType::apply + ); + + final var simResults = editablePlan.latestResults(); + + this.shouldDelete = this.goal.shouldDeletePastCreations(editablePlan, simResults); + + if (shouldDelete instanceof ActivityAutoDelete.AtBeginning ab) { + deletePastCreations(editablePlan, ab.getAnchorStrategy(), problem.sourceSchedulingGoals); + return ab.getSimulateAfter(); } + return false; + } + public void run( + final Problem problem, + final Evaluation eval, + final Plan plan, + final MissionModel missionModel, + final Function lookupActivityType, + final SimulationFacade simulationFacade, + final DirectiveIdGenerator idGenerator, + Map> eventsByDerivationGroup + ) { List newActivities = new ArrayList<>(); final var planAdapter = new SchedulerToProcedurePlanAdapter( @@ -72,16 +128,18 @@ public void run( lookupActivityType::apply ); - procedureMapper.deserialize(SerializedValue.of(this.args)).run(editablePlan); + if (shouldDelete instanceof ActivityAutoDelete.JustBefore jb) { + deletePastCreations(editablePlan, jb.getAnchorStrategy(), problem.sourceSchedulingGoals); + } - if (!editablePlan.getUncommittedChanges().isEmpty()) { + this.goal.run(editablePlan); + + if (editablePlan.isDirty()) { throw new IllegalStateException("procedural goal %s had changes that were not committed or rolled back".formatted(jarPath.getFileName())); } for (final var edit : editablePlan.getTotalDiff()) { if (edit instanceof Edit.Create c) { newActivities.add(toSchedulingActivity(c.getDirective(), lookupActivityType::apply, true)); - } else { - throw new IllegalStateException("Unexpected value: " + edit); } } @@ -91,4 +149,17 @@ public void run( } evaluation.setConflictSatisfaction(null, ConflictSatisfaction.SAT); } + + private void deletePastCreations( + final InMemoryEditablePlan plan, + final DeletedAnchorStrategy strategy, + final Map sourceSchedulingGoals + ) { + for (final var activity: plan.getAdapter().getActivities()) { + final var goalId = sourceSchedulingGoals.getOrDefault(activity.id(), null); + if (goalId != null && goalId.goalInvocationId().equals(this.goalId.goalInvocationId())) { + plan.delete(activity.id(), strategy); + } + } + } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalId.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/GoalId.java similarity index 86% rename from scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalId.java rename to scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/GoalId.java index ec4ab2b8a6..79d404e34a 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalId.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/GoalId.java @@ -1,4 +1,4 @@ -package gov.nasa.jpl.aerie.scheduler.server.models; +package gov.nasa.jpl.aerie.scheduler.model; import java.util.Optional; diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java index ae70e94449..0cd07c93b0 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java @@ -11,6 +11,7 @@ import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationData; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationResultsConverter; +import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import java.util.ArrayList; import java.util.Collection; @@ -60,6 +61,8 @@ public class Problem { */ private Optional initialSimulationResults; + public final Map sourceSchedulingGoals; + /** * container of all goals in the problem, indexed by name */ @@ -80,7 +83,9 @@ public Problem( MissionModel mission, PlanningHorizon planningHorizon, SimulationFacade simulationFacade, - SchedulerModel schedulerModel) { + SchedulerModel schedulerModel, + Map sourceSchedulingGoals + ) { this.missionModel = mission; this.schedulerModel = schedulerModel; this.initialPlan = new PlanInMemory(); @@ -96,6 +101,16 @@ public Problem( this.simulationFacade.addActivityTypes(this.getActivityTypes()); } this.initialSimulationResults = Optional.empty(); + this.sourceSchedulingGoals = sourceSchedulingGoals; + } + + public Problem( + MissionModel mission, + PlanningHorizon planningHorizon, + SimulationFacade simulationFacade, + SchedulerModel schedulerModel + ) { + this(mission, planningHorizon, simulationFacade, schedulerModel, new HashMap<>()); } public SimulationFacade getSimulationFacade(){ diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index a6629d3182..3dc5d15364 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -265,11 +265,35 @@ public void initializePlan() throws SimulationFacade.SimulationException, Schedu * * the output plan member is updated directly with the devised solution */ - private void solve() throws SchedulingInterruptedException{ + private void solve() throws SchedulingInterruptedException { //construct a priority sorted goal container final var goalQ = getGoalQueue(); assert goalQ != null; + // perform at-beginning auto deletions first + boolean simulateAfter = false; + for (final var goal: goalQ) { + if (goal instanceof Procedure p) { + simulateAfter = simulateAfter || p.deleteAtBeginning( + problem, + plan, + problem.getMissionModel(), + this.problem::getActivityType, + this.simulationFacade, + this.idGenerator, + this.problem.getEventsByDerivationGroup() + ); + } + } + + if (simulateAfter) { + try { + simulationFacade.simulateNoResults(plan, problem.getPlanningHorizon().getAerieHorizonDuration()); + } catch (SimulationFacade.SimulationException e) { + logger.error("Simulation error after auto deleting activities: ", e); + } + } + //process each goal independently in that order while (!goalQ.isEmpty()) { var goal = goalQ.remove(); @@ -299,7 +323,7 @@ private LinkedList getGoalQueue() { final var rawGoals = problem.getGoals(); assert rawGoals != null; - this.atLeastOneSimulateAfter = rawGoals.stream().filter(g -> g.simulateAfter).findFirst().isPresent(); + this.atLeastOneSimulateAfter = rawGoals.stream().anyMatch(g -> g.simulateAfter); //create queue container using comparator and pre-sized for all goals final var capacity = rawGoals.size(); diff --git a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/InMemoryEditablePlan.kt b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/InMemoryEditablePlan.kt index 049d7eb808..d45d119bb6 100644 --- a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/InMemoryEditablePlan.kt +++ b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/InMemoryEditablePlan.kt @@ -2,171 +2,54 @@ package gov.nasa.jpl.aerie.scheduler.plan import gov.nasa.jpl.aerie.merlin.driver.MissionModel import gov.nasa.jpl.aerie.merlin.protocol.types.Duration -import gov.nasa.ammos.aerie.procedural.scheduling.plan.Edit -import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan -import gov.nasa.ammos.aerie.procedural.scheduling.plan.NewDirective import gov.nasa.ammos.aerie.procedural.scheduling.simulation.SimulateOptions +import gov.nasa.ammos.aerie.procedural.scheduling.utils.EasyEditablePlanDriver +import gov.nasa.ammos.aerie.procedural.scheduling.utils.PerishableSimulationResults import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Directive import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart -import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan -import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType import gov.nasa.jpl.aerie.scheduler.DirectiveIdGenerator import gov.nasa.jpl.aerie.scheduler.model.* import gov.nasa.jpl.aerie.types.ActivityDirectiveId -import java.lang.ref.WeakReference -import java.time.Instant import kotlin.jvm.optionals.getOrNull -import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults as TimelineSimResults /* * An implementation of [EditablePlan] that stores the plan in memory for use in the internal scheduler. - * - * ## Staleness checking - * - * The editable plan instance keeps track of sim results that it has produced using weak references, and can dynamically - * update their staleness if the plan is changed after it was simulated. The process is this: - * - * 1. [InMemoryEditablePlan] has a set of weak references to simulation results objects that are currently up-to-date. - * I used weak references because if the user can't access it anymore, staleness doesn't matter and we might as well - * let it get gc'ed. - * 2. When the user gets simulation results, either through simulation or by getting the latest, it always checks for - * plan equality between the returned results and the current plan, even if we just simulated. If it is up-to-date, a - * weak ref is added to the set. - * 3. When an edit is made, the sim results in the current set are marked stale; then the set is reset to new reference - * to an empty set. - * 4. When a commit is made, the commit object takes *shared ownership* of the set. If a new simulation is run (step 2) - * the plan can still add to the set while it is still jointly owned by the commit. Then when an edit is made (step 3) - * the commit will become the sole owner of the set. - * 5. When changes are rolled back, any sim results currently in the plan's set are marked stale, the previous commit's - * sim results are marked not stale, then the plan will resume joint ownership of the previous commit's set. - * - * The joint ownership freaks me out a wee bit, but I think it's safe because the commits are only used to keep the - * previous sets from getting gc'ed in the event of a rollback. Only the plan object actually mutates the set. */ data class InMemoryEditablePlan( - private val missionModel: MissionModel<*>, - private var idGenerator: DirectiveIdGenerator, - private val plan: SchedulerToProcedurePlanAdapter, - private val simulationFacade: SimulationFacade, - private val lookupActivityType: (String) -> ActivityType -) : EditablePlan, Plan by plan { - - private data class Commit( - val diff: List, - - /** - * A record of the simulation results objects that were up-to-date when the commit - * was created. - * - * This has SHARED OWNERSHIP with [InMemoryEditablePlan]; the editable plan may add more to - * this list AFTER the commit is created. - */ - val upToDateSimResultsSet: MutableSet> - ) - - private var committedChanges = Commit(listOf(), mutableSetOf()) - var uncommittedChanges = mutableListOf() - private set - - val totalDiff: List - get() = committedChanges.diff - - // Jointly owned set of up-to-date simulation results. See class-level comment for algorithm explanation. - private var upToDateSimResultsSet: MutableSet> = mutableSetOf() - - override fun latestResults(): SimulationResults? { + private val missionModel: MissionModel<*>, + private var idGenerator: DirectiveIdGenerator, + val adapter: SchedulerToProcedurePlanAdapter, + private val simulationFacade: SimulationFacade, + private val lookupActivityType: (String) -> ActivityType +) : EasyEditablePlanDriver(adapter) { + + override fun generateDirectiveId(): ActivityDirectiveId = idGenerator.next() + override fun latestResultsInternal(): PerishableSimulationResults? { val merlinResults = simulationFacade.latestSimulationData.getOrNull() ?: return null - - // kotlin checks structural equality by default, not referential equality. - val isStale = merlinResults.plan.activities != plan.activities - - val results = MerlinToProcedureSimulationResultsAdapter(merlinResults.driverResults, isStale, plan) - if (!isStale) upToDateSimResultsSet.add(WeakReference(results)) - return results + return MerlinToProcedureSimulationResultsAdapter(merlinResults.driverResults, adapter.copy(schedulerPlan = adapter.duplicate())) } - override fun create(directive: NewDirective): ActivityDirectiveId { - class ParentSearchException(id: ActivityDirectiveId, size: Int): Exception("Expected one parent activity with id $id, found $size") - val id = idGenerator.next() - val parent = when (val s = directive.start) { - is DirectiveStart.Anchor -> { - val parentList = directives() - .filter { it.id == s.parentId } - .collect(totalBounds()) - if (parentList.size != 1) throw ParentSearchException(s.parentId, parentList.size) - parentList.first() - } - is DirectiveStart.Absolute -> null - } - val resolved = directive.resolve(id, parent) - uncommittedChanges.add(Edit.Create(resolved)) - resolved.validateArguments(lookupActivityType) - plan.add(resolved.toSchedulingActivity(lookupActivityType, true)) - - for (simResults in upToDateSimResultsSet) { - simResults.get()?.stale = true - } - // create a new list instead of `.clear` because commit objects have the same reference - upToDateSimResultsSet = mutableSetOf() - - return id + override fun createInternal(directive: Directive) { + adapter.add(directive.toSchedulingActivity(lookupActivityType, true)) } - override fun commit() { - // Early return if there are no changes. This prevents multiple commits from sharing ownership of the set, - // because new sets are only created when edits are made. - // Probably unnecessary, but shared ownership freaks me out enough already. - if (uncommittedChanges.isEmpty()) return - - val newCommittedChanges = uncommittedChanges - uncommittedChanges = mutableListOf() - - // Create a commit that shares ownership of the simResults set. - committedChanges = Commit(committedChanges.diff + newCommittedChanges, upToDateSimResultsSet) + override fun deleteInternal(id: ActivityDirectiveId) { + adapter.remove(adapter.activitiesById[id]) } - override fun rollback(): List { - // Early return if there are no changes, to keep staleness accuracy - if (uncommittedChanges.isEmpty()) return emptyList() - - val result = uncommittedChanges - uncommittedChanges = mutableListOf() - for (edit in result) { - when (edit) { - is Edit.Create -> { - plan.remove(edit.directive.toSchedulingActivity(lookupActivityType, true)) - } - } - } - for (simResult in upToDateSimResultsSet) { - simResult.get()?.stale = true - } - for (simResult in committedChanges.upToDateSimResultsSet) { - simResult.get()?.stale = false - } - upToDateSimResultsSet = committedChanges.upToDateSimResultsSet - return result + override fun simulateInternal(options: SimulateOptions) { + simulationFacade.simulateWithResults(adapter, options.pause.resolve(this)) } - override fun simulate(options: SimulateOptions): TimelineSimResults { - simulationFacade.simulateWithResults(plan, options.pause.resolve(this)) - return latestResults()!! + override fun validate(directive: Directive) { + super.validate(directive) + lookupActivityType(directive.type).specType.inputType.validateArguments(directive.inner.arguments) } - // These cannot be implemented with the by keyword, - // because directives() below needs a custom implementation. - override fun totalBounds() = plan.totalBounds() - override fun toRelative(abs: Instant) = plan.toRelative(abs) - override fun toAbsolute(rel: Duration) = plan.toAbsolute(rel) - companion object { - fun Directive.validateArguments(lookupActivityType: (String) -> ActivityType) { - lookupActivityType(type).specType.inputType.validateArguments(inner.arguments) - } - @JvmStatic fun Directive.toSchedulingActivity(lookupActivityType: (String) -> ActivityType, isNew: Boolean) = SchedulingActivity( id, lookupActivityType(type), diff --git a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt index fecd52fcce..a1add89f01 100644 --- a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt +++ b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/MerlinToProcedureSimulationResultsAdapter.kt @@ -1,12 +1,12 @@ package gov.nasa.jpl.aerie.scheduler.plan +import gov.nasa.ammos.aerie.procedural.scheduling.utils.PerishableSimulationResults import gov.nasa.ammos.aerie.procedural.timeline.Interval import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialSegmentOps import gov.nasa.ammos.aerie.procedural.timeline.payloads.Segment import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.Instance import gov.nasa.ammos.aerie.procedural.timeline.plan.Plan -import gov.nasa.ammos.aerie.procedural.timeline.plan.SimulationResults import gov.nasa.ammos.aerie.procedural.timeline.util.duration.rangeTo import gov.nasa.jpl.aerie.merlin.driver.engine.ProfileSegment import gov.nasa.jpl.aerie.merlin.protocol.types.Duration @@ -18,9 +18,15 @@ import kotlin.jvm.optionals.getOrNull class MerlinToProcedureSimulationResultsAdapter( private val results: gov.nasa.jpl.aerie.merlin.driver.SimulationResults, - var stale: Boolean, + + /** A copy of the plan that will not be mutated after creation. */ private val plan: Plan -): SimulationResults { +): PerishableSimulationResults { + + private var stale = false; + override fun setStale(stale: Boolean) { + this.stale = stale; + } override fun isStale() = stale @@ -119,4 +125,6 @@ class MerlinToProcedureSimulationResultsAdapter( } return Instances(instances) } + + override fun inputDirectives(deserializer: (SerializedValue) -> A) = plan.directives(null, deserializer) } diff --git a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/SchedulerToProcedurePlanAdapter.kt b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/SchedulerToProcedurePlanAdapter.kt index c23b5cbdbc..4a53dea205 100644 --- a/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/SchedulerToProcedurePlanAdapter.kt +++ b/scheduler-driver/src/main/kotlin/gov/nasa/jpl/aerie/scheduler/plan/SchedulerToProcedurePlanAdapter.kt @@ -48,7 +48,7 @@ data class SchedulerToProcedurePlanAdapter( result.add( Directive( deserializer(SerializedValue.of(activity.arguments)), - "Name unavailable", + activity.name, activity.id, activity.type.name, if (activity.anchorId == null) DirectiveStart.Absolute(activity.startOffset) diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/EditablePlanStalenessTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/EditablePlanTest.java similarity index 60% rename from scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/EditablePlanStalenessTest.java rename to scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/EditablePlanTest.java index 200368d2ee..d15ab6b1da 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/EditablePlanStalenessTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/EditablePlanTest.java @@ -12,17 +12,20 @@ import gov.nasa.jpl.aerie.scheduler.plan.SchedulerToProcedurePlanAdapter; import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; -public class EditablePlanStalenessTest { +public class EditablePlanTest { MissionModel missionModel; Problem problem; @@ -63,6 +66,47 @@ public void tearDown() { plan = null; } + @Test + public void activityCreation() { + plan.create( + "BiteBanana", + new DirectiveStart.Absolute(Duration.MINUTE), + Map.of("biteSize", SerializedValue.of(1)) + ); + + assertEquals(1, plan.directives().collect().size()); + assertEquals(Map.of("biteSize", SerializedValue.of(1)), plan.directives().collect().getFirst().inner.arguments); + } + + @Test + public void activityDeletion() { + final var id1 = plan.create( + "BiteBanana", + new DirectiveStart.Absolute(Duration.MINUTE), + Map.of("biteSize", SerializedValue.of(1)) + ); + final var id2 = plan.create( + "GrowBanana", + new DirectiveStart.Absolute(Duration.HOUR), + Map.of("growingDuration", SerializedValue.of(1), "quantity", SerializedValue.of(1)) + ); + + final Supplier> idSet = + () -> plan.directives().collect().stream().map($ -> $.id).collect(Collectors.toSet()); + + plan.commit(); + + assertEquals(Set.of(id1, id2), idSet.get()); + + plan.delete(id1); + + assertEquals(Set.of(id2), idSet.get()); + + plan.delete(id2); + + assertEquals(Set.of(), idSet.get()); + } + @Test public void simResultMarkedStale() { plan.create( @@ -87,6 +131,23 @@ public void simResultMarkedStale() { assertTrue(simResults.isStale()); } + @Test + public void simResultMarkedStaleAfterDelete() { + final var id = plan.create( + "BiteBanana", + new DirectiveStart.Absolute(Duration.MINUTE), + Map.of("biteSize", SerializedValue.of(1)) + ); + + final var simResults = plan.simulate(); + + assertFalse(simResults.isStale()); + + plan.delete(id); + + assertTrue(simResults.isStale()); + } + @Test public void simResultMarkedNotStaleAfterRollback_CommitThenSimulate() { plan.create( @@ -118,7 +179,7 @@ public void simResultMarkedNotStaleAfterRollback_CommitThenSimulate() { @Test public void simResultMarkedNotStaleAfterRollback_SimulateThenCommit() { - plan.create( + final var id = plan.create( "BiteBanana", new DirectiveStart.Absolute(Duration.MINUTE), Map.of("biteSize", SerializedValue.of(1)) @@ -129,19 +190,43 @@ public void simResultMarkedNotStaleAfterRollback_SimulateThenCommit() { assertFalse(simResults.isStale()); + plan.delete(id); + + assertTrue(simResults.isStale()); + + plan.rollback(); + + assertFalse(simResults.isStale()); + } + + @Test + void simulationInputDirectivesDontChange() { + plan.create( + "BiteBanana", + new DirectiveStart.Absolute(Duration.MINUTE), + Map.of("biteSize", SerializedValue.of(1)) + ); + + final var expectedDirectives = plan.directives(); + final var simResults = plan.simulate(); + plan.create( "GrowBanana", new DirectiveStart.Absolute(Duration.HOUR), Map.of( - "growingDuration", SerializedValue.of(10), + "growingDuration", SerializedValue.of(10000), "quantity", SerializedValue.of(1) ) ); - assertTrue(simResults.isStale()); + assertIterableEquals(expectedDirectives, simResults.inputDirectives()); + assertNotEquals(plan.directives(), simResults.inputDirectives()); - plan.rollback(); + plan.commit(); - assertFalse(simResults.isStale()); + assertIterableEquals(expectedDirectives, simResults.inputDirectives()); + assertNotEquals(plan.directives(), simResults.inputDirectives()); + + assertIterableEquals(plan.directives(), plan.simulate().inputDirectives()); } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/exceptions/NoSuchSchedulingGoalException.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/exceptions/NoSuchSchedulingGoalException.java index 421ae3705e..0e8b3b4ba5 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/exceptions/NoSuchSchedulingGoalException.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/exceptions/NoSuchSchedulingGoalException.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.server.exceptions; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; public final class NoSuchSchedulingGoalException extends Exception { public final GoalId goalId; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ResponseSerializers.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ResponseSerializers.java index 44302a40c2..af99dbd96c 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ResponseSerializers.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ResponseSerializers.java @@ -11,7 +11,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingCompilationError; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleAction; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalInvocationRecord.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalInvocationRecord.java index bb048ddbf8..026219b4dc 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalInvocationRecord.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalInvocationRecord.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.models; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import java.util.Map; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/SpecificationRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/SpecificationRepository.java index 8c3d192eea..91b7da7cf0 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/SpecificationRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/SpecificationRepository.java @@ -4,7 +4,7 @@ import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSchedulingGoalException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.SpecificationLoadException; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalType; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetCreatedActivitiesAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetCreatedActivitiesAction.java index e675a6c73a..823587b5a0 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetCreatedActivitiesAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetCreatedActivitiesAction.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import org.intellij.lang.annotations.Language; @@ -11,7 +11,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; /*package-local*/ final class GetCreatedActivitiesAction implements AutoCloseable { private static final @Language("SQL") String sql = """ diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetGoalSatisfactionAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetGoalSatisfactionAction.java index 8624da2af2..aba6e9b2fc 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetGoalSatisfactionAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetGoalSatisfactionAction.java @@ -5,7 +5,7 @@ import java.sql.SQLException; import java.util.HashMap; import java.util.Map; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import org.intellij.lang.annotations.Language; /*package-local*/ final class GetGoalSatisfactionAction implements AutoCloseable { diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSatisfyingActivitiesAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSatisfyingActivitiesAction.java index d52dfbdf47..407833c36f 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSatisfyingActivitiesAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSatisfyingActivitiesAction.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import org.intellij.lang.annotations.Language; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSchedulingGoalAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSchedulingGoalAction.java index 40c8b1ee0b..ac7868ffb0 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSchedulingGoalAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSchedulingGoalAction.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.GoalType; import org.intellij.lang.annotations.Language; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationGoalsAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationGoalsAction.java index e74941e538..557774fb94 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationGoalsAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationGoalsAction.java @@ -3,7 +3,7 @@ import gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidEntityException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalInvocationRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.GoalType; @@ -13,10 +13,8 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.text.ParseException; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import static gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers.parseJson; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GoalBuilder.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GoalBuilder.java index 5a9b6b6569..f8756e2c3b 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GoalBuilder.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GoalBuilder.java @@ -25,6 +25,7 @@ import gov.nasa.jpl.aerie.scheduler.goals.Procedure; import gov.nasa.jpl.aerie.scheduler.goals.RecurrenceGoal; import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.model.PersistentTimeAnchor; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; @@ -43,7 +44,9 @@ public static Goal goalOfGoalSpecifier( final Timestamp horizonStartTimestamp, final Timestamp horizonEndTimestamp, final Function lookupActivityType, - final boolean simulateAfter) { + final boolean simulateAfter, + final GoalId goalId + ) { final var planningHorizon = new PlanningHorizon( horizonStartTimestamp.toInstant(), horizonEndTimestamp.toInstant()); @@ -110,11 +113,16 @@ public static Goal goalOfGoalSpecifier( case SchedulingDSL.GoalSpecifier.GoalAnd g -> { var builder = new CompositeAndGoal.Builder(); for (final var subGoalSpecifier : g.goals()) { - builder = builder.and(goalOfGoalSpecifier(subGoalSpecifier, - horizonStartTimestamp, - horizonEndTimestamp, - lookupActivityType, - simulateAfter)); + builder = builder.and( + goalOfGoalSpecifier( + subGoalSpecifier, + horizonStartTimestamp, + horizonEndTimestamp, + lookupActivityType, + simulateAfter, + goalId + ) + ); } builder.simulateAfter(simulateAfter); builder.withinPlanHorizon(planningHorizon); @@ -126,11 +134,16 @@ public static Goal goalOfGoalSpecifier( case SchedulingDSL.GoalSpecifier.GoalOr g -> { var builder = new OptionGoal.Builder(); for (final var subGoalSpecifier : g.goals()) { - builder = builder.or(goalOfGoalSpecifier(subGoalSpecifier, - horizonStartTimestamp, - horizonEndTimestamp, - lookupActivityType, - simulateAfter)); + builder = builder.or( + goalOfGoalSpecifier( + subGoalSpecifier, + horizonStartTimestamp, + horizonEndTimestamp, + lookupActivityType, + simulateAfter, + goalId + ) + ); } builder.simulateAfter(simulateAfter); builder.withinPlanHorizon(planningHorizon); @@ -140,7 +153,7 @@ public static Goal goalOfGoalSpecifier( } case SchedulingDSL.GoalSpecifier.GoalApplyWhen g -> { - var goal = goalOfGoalSpecifier(g.goal(), horizonStartTimestamp, horizonEndTimestamp, lookupActivityType, simulateAfter); + var goal = goalOfGoalSpecifier(g.goal(), horizonStartTimestamp, horizonEndTimestamp, lookupActivityType, simulateAfter, goalId); goal.setTemporalContext(g.windows()); return goal; } @@ -165,7 +178,7 @@ public static Goal goalOfGoalSpecifier( } case SchedulingDSL.GoalSpecifier.Procedure g -> { - return new Procedure(planningHorizon, g.jarPath(), g.arguments(), simulateAfter); + return new Procedure(planningHorizon, g.jarPath(), g.arguments(), simulateAfter, goalId); } } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertCreatedActivitiesAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertCreatedActivitiesAction.java index 87e5205d95..aedd1ae523 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertCreatedActivitiesAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertCreatedActivitiesAction.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import org.intellij.lang.annotations.Language; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertGoalSatisfactionAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertGoalSatisfactionAction.java index 0a3083ea54..958ae9f8b3 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertGoalSatisfactionAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertGoalSatisfactionAction.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import org.intellij.lang.annotations.Language; import java.sql.Connection; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertSatisfyingActivitiesAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertSatisfyingActivitiesAction.java index 3fa7556f48..a00535839f 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertSatisfyingActivitiesAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertSatisfyingActivitiesAction.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import org.intellij.lang.annotations.Language; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java index 87bd7daaf0..d92b39868e 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java @@ -13,7 +13,7 @@ import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchRequestException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; import gov.nasa.jpl.aerie.scheduler.server.remotes.ResultsCellRepository; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleFailure; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java index 3338460c64..c43224a777 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java @@ -3,7 +3,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSchedulingGoalException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalType; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalInvocationRecord; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/UpdateSchedulingGoalParameterSchemaAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/UpdateSchedulingGoalParameterSchemaAction.java index 27dce53309..93039d3f9a 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/UpdateSchedulingGoalParameterSchemaAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/UpdateSchedulingGoalParameterSchemaAction.java @@ -2,7 +2,7 @@ import gov.nasa.jpl.aerie.merlin.driver.json.ValueSchemaJsonParser; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import org.intellij.lang.annotations.Language; import java.sql.Connection; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinDatabaseService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinDatabaseService.java index 347b013262..306d708901 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinDatabaseService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinDatabaseService.java @@ -36,7 +36,7 @@ import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanMetadata; @@ -88,6 +88,7 @@ import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.simulationArgumentsP; import static gov.nasa.jpl.aerie.scheduler.server.graphql.ProfileParsers.discreteValueSchemaTypeP; import static gov.nasa.jpl.aerie.scheduler.server.graphql.ProfileParsers.realValueSchemaTypeP; +import static java.util.Map.entry; /** * {@inheritDoc} @@ -421,6 +422,8 @@ public Map updatePlanActivityDirective final var ids = new HashMap(); //creation are done in batch as that's what the scheduler does the most final var toAdd = new ArrayList(); + final var toDelete = new ArrayList(); + final var toModify = new ArrayList(); for (final var activity : plan.getActivities()) { if(activity.getParentActivity().isPresent()) continue; // Skip generated activities if (!activity.isNew()) { @@ -440,8 +443,7 @@ public Map updatePlanActivityDirective activity.anchoredToStart() ); if (!activityDirectiveFromSchedulingDirective.equals(actFromInitialPlan.get())) { - throw new MerlinServiceException("The scheduler should not be updating activity instances"); - //updateActivityDirective(planId, schedulerActIntoMerlinAct, activityDirectiveId, activityToGoalId.get(activity)); + toModify.add(activity); } ids.put(activity.id(), activity.id()); } else { @@ -452,13 +454,14 @@ public Map updatePlanActivityDirective final var actsFromNewPlan = plan.getActivitiesById(); for (final var idInInitialPlan : initialPlan.getActivitiesById().keySet()) { if (!actsFromNewPlan.containsKey(idInInitialPlan)) { - throw new MerlinServiceException("The scheduler should not be deleting activity instances"); - //deleteActivityDirective(idActInInitialPlan.getValue()); + toDelete.add(idInInitialPlan); } } //Create ids.putAll(createActivityDirectives(planId, toAdd, activityToGoalId, schedulerModel)); + modifyActivityDirectives(planId, toModify); + deleteActivityDirectives(planId, toDelete); return ids; } @@ -554,7 +557,7 @@ public Map createAllPlanActivityDirect return createActivityDirectives(planId, plan.getActivitiesByTime(), activityToGoalId, schedulerModel); } - public Map createActivityDirectives( + private Map createActivityDirectives( final PlanId planId, final List orderedActivities, final Map activityToGoalId, @@ -580,14 +583,14 @@ mutation createAllPlanActivityDirectives($activities: [activity_directive_insert final var insertionObjects = Json.createArrayBuilder(); for (final var act : orderedActivities) { - var insertionObject = Json + final var insertionObject = Json .createObjectBuilder() .add("plan_id", planId.id()) .add("type", act.getType().getName()) .add("start_offset", act.startOffset().toString()) .add("anchored_to_start", act.anchoredToStart()); - if (act.name() != null) insertionObject = insertionObject.add("name", act.name()); + if (act.name() != null) insertionObject.add("name", act.name()); //add duration to parameters if controllable final var insertionObjectArguments = Json.createObjectBuilder(); @@ -600,6 +603,7 @@ mutation createAllPlanActivityDirectives($activities: [activity_directive_insert final var goalId = activityToGoalId.get(act); if (goalId != null) { insertionObject.add("source_scheduling_goal_id", goalId.id()); + goalId.goalInvocationId().ifPresent($ -> insertionObject.add("source_scheduling_goal_invocation_id", $)); } for (final var arg : act.arguments().entrySet()) { @@ -636,6 +640,66 @@ mutation createAllPlanActivityDirectives($activities: [activity_directive_insert return activityToDirectiveId; } + private void modifyActivityDirectives( + final PlanId planId, + final List activities + ) + throws IOException, NoSuchPlanException, MerlinServiceException + { + if (activities.isEmpty()) return; + ensurePlanExists(planId); + final var request = new StringBuilder(); + request.append("mutation updatePlanActivityDirectives("); + request.append(String.join( + ",", + activities.stream().map($ -> "$activity_%d: activity_directive_set_input!".formatted($.id().id())).toList() + )); + request.append(") {"); + final var arguments = Json.createObjectBuilder(); + for (final var act : activities) { + final var id = act.id().id(); + request.append(""" + update_%d: update_activity_directive_by_pk(pk_columns: {id: %d, plan_id: %d}, _set: $activity_%d) { + affected_rows + } + """.formatted(id, id, planId.id(), id)); + + final var activityObject = Json + .createObjectBuilder() + .add("start_offset", act.startOffset().toString()) + .add("anchored_to_start", act.anchoredToStart()) + .add("name", act.name()); + + final var insertionObjectArguments = Json.createObjectBuilder(); + for (final var arg : act.arguments().entrySet()) { + insertionObjectArguments.add(arg.getKey(), serializedValueP.unparse(arg.getValue())); + } + activityObject.add("arguments", insertionObjectArguments.build()); + arguments.add("activity_%d".formatted(id), activityObject); + } + postRequest(request.toString(), arguments.build()).orElseThrow(() -> new NoSuchPlanException(planId)); + } + + private void deleteActivityDirectives( + final PlanId planId, + final List ids + ) + throws IOException, NoSuchPlanException, MerlinServiceException + { + if (ids.isEmpty()) return; + ensurePlanExists(planId); + final var request = new StringBuilder(); + request.append("mutation deletePlanActivityDirectives {"); + for (final var id : ids) { + request.append(""" + delete_%d: delete_activity_directive_by_pk(id: %d, plan_id: %d) {affected_rows} + """.formatted(id.id(), id.id(), planId.id())); + } + request.append("}"); + postRequest(request.toString()).orElseThrow(() -> new NoSuchPlanException(planId)); + } + + @Override public MerlinDatabaseService.MissionModelTypes getMissionModelTypes(final PlanId planId) throws IOException, MerlinServiceException @@ -796,6 +860,35 @@ public Collection getResourceTypes(final PlanId planId) return allResourceTypes; } + @Override + @SuppressWarnings("unchecked") + public Map getActivityIdToGoalIdMap(final PlanId planId) + throws MerlinServiceException, IOException + { + final var request = """ + query { + activity_directive(where: {plan_id: {_eq: %d}}) { + id + source_scheduling_goal_id + source_scheduling_goal_invocation_id + } + """.formatted(planId.id()); + final JsonObject response = postRequest(request).get(); + final var data = response.getJsonObject("data"); + final List> results = data.getJsonArray("activity_directive").getValuesAs( + $ -> { + final var obj = $.asJsonObject(); + final var id = new ActivityDirectiveId(obj.getInt("id")); + if (obj.isNull("source_scheduling_goal_id")) return entry(id, null); + final var source_goal = obj.getInt("source_scheduling_goal_id"); + if (obj.isNull("source_scheduling_goal_invocation_id")) return entry(id, new GoalId(source_goal, -1, Optional.empty())); + final Long source_invocation = (long) obj.getInt("source_scheduling_goal_invocation_id"); + return entry(id, new GoalId(source_goal, -1, Optional.of(source_invocation))); + } + ); + return Map.ofEntries(results.toArray(new Map.Entry[0])); + } + public SimulationId getSimulationId(PlanId planId) throws MerlinServiceException, IOException { final var request = """ query { diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinDatabaseService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinDatabaseService.java index 18939f8d1f..d046427db8 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinDatabaseService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinDatabaseService.java @@ -15,7 +15,7 @@ import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanMetadata; @@ -114,6 +114,9 @@ Map> getExternalEvents(final PlanId planId, final In */ Collection getResourceTypes(final PlanId planId) throws IOException, MerlinServiceException, NoSuchPlanException; + + Map getActivityIdToGoalIdMap(final PlanId planId) + throws MerlinServiceException, IOException; } interface WriterRole { diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleResults.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleResults.java index 755ca3c313..b9bcd3344f 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleResults.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleResults.java @@ -3,7 +3,7 @@ import java.util.Collection; import java.util.Map; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.types.ActivityDirectiveId; /** diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java index 9fe6fad93e..d0a6dc27a4 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java @@ -5,7 +5,7 @@ import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSchedulingGoalException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.SpecificationLoadException; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalType; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java index 1347e2afac..ddcb1a01a5 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java @@ -50,7 +50,7 @@ import gov.nasa.jpl.aerie.scheduler.server.http.ResponseSerializers; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalInvocationRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.GoalType; @@ -141,12 +141,15 @@ public void schedule( planMetadata.modelConfiguration(), planMetadata.horizon().getStartInstant(), new MissionModelId(planMetadata.modelId())), - canceledListener); + canceledListener + ); + final var oldActivityIdToGoalId = merlinDatabaseService.getActivityIdToGoalIdMap(specification.planId()); final var problem = new Problem( schedulerMissionModel.missionModel(), planningHorizon, simulationFacade, - schedulerMissionModel.schedulerModel() + schedulerMissionModel.schedulerModel(), + oldActivityIdToGoalId ); final var externalProfiles = loadExternalProfiles(planMetadata.planId()); final var externalEventsByDerivationGroup = loadExternalEvents(planMetadata.planId(), planMetadata.horizon().getStartInstant()); @@ -191,49 +194,51 @@ public void schedule( final var orderedGoals = new ArrayList(); final var goals = new HashMap(); - final var compiledGoals = new ArrayList>(); - final var failedGoals = new ArrayList>>(); - for (final var goalRecord : specification.goalsByPriority()) { - switch (goalRecord.type()) { - case GoalType.EDSL edsl -> { - final var result = compileGoalDefinition( - merlinDatabaseService, - planMetadata.planId(), - edsl.source(), - schedulingDSLCompilationService, - externalProfiles.resourceTypes()); - if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Success r) { - compiledGoals.add(Pair.of(goalRecord, r.value())); - } else if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error r) { - failedGoals.add(Pair.of(goalRecord.id(), r.errors())); - } else { - throw new Error("Unhandled variant of %s: %s".formatted( - SchedulingDSLCompilationService.SchedulingDSLCompilationResult.class.getSimpleName(), - result)); - } - } - case GoalType.JAR jar -> { - compiledGoals.add(Pair.of(goalRecord, new SchedulingDSL.GoalSpecifier.Procedure(modelJarsDir.resolve(jar.path()), goalRecord.args()))); + final var compiledGoals = new ArrayList>(); + final var failedGoals = new ArrayList>>(); + for (final var goalRecord : specification.goalsByPriority()) { + switch (goalRecord.type()) { + case GoalType.EDSL edsl -> { + final var result = compileGoalDefinition( + merlinDatabaseService, + planMetadata.planId(), + edsl.source(), + schedulingDSLCompilationService, + externalProfiles.resourceTypes()); + if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Success r) { + compiledGoals.add(Pair.of(goalRecord, r.value())); + } else if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error r) { + failedGoals.add(Pair.of(goalRecord.id(), r.errors())); + } else { + throw new Error("Unhandled variant of %s: %s".formatted( + SchedulingDSLCompilationService.SchedulingDSLCompilationResult.class.getSimpleName(), + result)); } } + case GoalType.JAR jar -> { + compiledGoals.add(Pair.of(goalRecord, new SchedulingDSL.GoalSpecifier.Procedure(modelJarsDir.resolve(jar.path()), goalRecord.args()))); + } } - if (!failedGoals.isEmpty()) { - writer.failWith(b -> b - .type("SCHEDULING_GOALS_FAILED") - .message("Scheduling goal%s failed".formatted(failedGoals.size() > 1 ? "s" : "")) - .data(ResponseSerializers.serializeFailedGoals(failedGoals))); - return; - } - for (final var compiledGoal : compiledGoals) { - final var goal = GoalBuilder - .goalOfGoalSpecifier( - compiledGoal.getValue(), - specification.horizonStartTimestamp(), - specification.horizonEndTimestamp(), - problem::getActivityType, - compiledGoal.getKey().simulateAfter()); - orderedGoals.add(goal); - goals.put(goal, compiledGoal.getKey().id()); + } + if (!failedGoals.isEmpty()) { + writer.failWith(b -> b + .type("SCHEDULING_GOALS_FAILED") + .message("Scheduling goal%s failed".formatted(failedGoals.size() > 1 ? "s" : "")) + .data(ResponseSerializers.serializeFailedGoals(failedGoals))); + return; + } + for (final var compiledGoal : compiledGoals) { + final var goal = GoalBuilder + .goalOfGoalSpecifier( + compiledGoal.getValue(), + specification.horizonStartTimestamp(), + specification.horizonEndTimestamp(), + problem::getActivityType, + compiledGoal.getKey().simulateAfter(), + compiledGoal.getKey().id() + ); + orderedGoals.add(goal); + goals.put(goal, compiledGoal.getKey().id()); } problem.setGoals(orderedGoals); @@ -242,10 +247,10 @@ public void schedule( final var solutionPlan = scheduler.getNextSolution().orElseThrow( () -> new ResultsProtocolFailure("scheduler returned no solution")); - final var activityToGoalId = new HashMap(); + final var newActivityToGoalId = new HashMap(); for (final var entry : solutionPlan.getEvaluation().getGoalEvaluations().entrySet()) { for (final var activity : entry.getValue().getInsertedActivities()) { - activityToGoalId.put(activity, goals.get(entry.getKey())); + newActivityToGoalId.put(activity, goals.get(entry.getKey())); } } //store the solution plan back into merlin (and reconfirm no intervening mods!) @@ -255,7 +260,7 @@ public void schedule( planMetadata, loadedPlanComponents.merlinPlan(), solutionPlan, - activityToGoalId, + newActivityToGoalId, schedulerMissionModel.schedulerModel() ); diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinDatabaseService.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinDatabaseService.java index f79c1acf1d..d9dd3e9954 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinDatabaseService.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinDatabaseService.java @@ -13,7 +13,7 @@ import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivity; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanMetadata; @@ -173,6 +173,13 @@ public Collection getResourceTypes(final PlanId planId) return null; } + @Override + public Map getActivityIdToGoalIdMap(final PlanId planId) + throws MerlinServiceException, IOException + { + return Map.of(); + } + @Override public void clearPlanActivityDirectives(final PlanId planId) { diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationRepository.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationRepository.java index 9547dbc82a..1834ab16a2 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationRepository.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationRepository.java @@ -5,7 +5,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalType; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java index efd274cb7a..8198d1058d 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java @@ -29,6 +29,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.TimeUtility; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.model.PersistentTimeAnchor; import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; @@ -42,6 +43,7 @@ import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinDatabaseService; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinServiceException; +import gov.nasa.jpl.aerie.types.ActivityDirectiveId; import gov.nasa.jpl.aerie.types.MissionModelId; import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.AfterAll; @@ -122,6 +124,13 @@ public Map> getExternalEvents(final PlanId planId, f public Collection getResourceTypes(final PlanId planId) { return null; } + + @Override + public Map getActivityIdToGoalIdMap(final PlanId planId) + throws MerlinServiceException, IOException + { + return Map.of(); + } }; SchedulingDSLCompilationService schedulingDSLCompilationService; diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingEdslIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingEdslIntegrationTests.java index b7dda0af9a..51eebe46a4 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingEdslIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingEdslIntegrationTests.java @@ -42,7 +42,7 @@ import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionId; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionRecord; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionSource; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.model.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalInvocationRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId;