From ad3381005b421766a29cdc791abea0b53ef2121f Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 7 Mar 2024 15:39:13 -0800 Subject: [PATCH 1/4] Add optional `force` parameter to Simulate action - Update parser in MerlinBindings to reflect this change --- deployment/hasura/metadata/actions.graphql | 2 +- .../jpl/aerie/merlin/server/http/HasuraParsers.java | 12 ++++++++++++ .../jpl/aerie/merlin/server/http/MerlinBindings.java | 3 ++- .../jpl/aerie/merlin/server/models/HasuraAction.java | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/deployment/hasura/metadata/actions.graphql b/deployment/hasura/metadata/actions.graphql index 436134dabc..4ea491ca87 100644 --- a/deployment/hasura/metadata/actions.graphql +++ b/deployment/hasura/metadata/actions.graphql @@ -97,7 +97,7 @@ type Query { } type Query { - simulate(planId: Int!): MerlinSimulationResponse + simulate(planId: Int!, force: Boolean): MerlinSimulationResponse } type Query { diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java index f88e507f40..cb3aa1bcea 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java @@ -7,6 +7,7 @@ import java.util.Optional; +import static gov.nasa.jpl.aerie.json.BasicParsers.boolP; import static gov.nasa.jpl.aerie.json.BasicParsers.listP; import static gov.nasa.jpl.aerie.json.BasicParsers.longP; import static gov.nasa.jpl.aerie.json.BasicParsers.mapP; @@ -54,6 +55,17 @@ private static JsonParser> hasura .field("planId", planIdP) .map(HasuraAction.PlanInput::new, HasuraAction.PlanInput::planId)); + public static final JsonParser> hasuraSimulateActionP + = hasuraActionF( + productP + .field("planId", planIdP) + .optionalField("force", nullableP(boolP)) + .map( + untuple((planId, force) -> new HasuraAction.SimulateInput(planId, force.flatMap($ -> $))), + $ -> tuple($.planId(), Optional.of($.force())) + ) + ); + public static final JsonParser> hasuraConstraintsViolationsActionP = hasuraActionF( productP diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java index 9b4575e8ce..4b6bcc9a2f 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java @@ -35,6 +35,7 @@ import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraActivityBulkActionP; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraConstraintsCodeAction; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraConstraintsViolationsActionP; +import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraSimulateActionP; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraUploadExternalDatasetActionP; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraMissionModelActionP; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraMissionModelArgumentsActionP; @@ -175,7 +176,7 @@ private void getResourceTypes(final Context ctx) { private void getSimulationResults(final Context ctx) { try { - final var body = parseJson(ctx.body(), hasuraPlanActionP); + final var body = parseJson(ctx.body(), hasuraSimulateActionP); final var planId = body.input().planId(); this.checkPermissions(Action.simulate, body.session(), planId); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java index 860e034be2..a35d29d106 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java @@ -15,6 +15,7 @@ public sealed interface Input { } public record MissionModelInput(String missionModelId) implements Input { } public record PlanInput(PlanId planId) implements Input { } + public record SimulateInput(PlanId planId, Optional force) implements Input {} public record ConstraintViolationsInput(PlanId planId, Optional simulationDatasetId) implements Input { } public record ActivityInput(String missionModelId, String activityTypeName, From 3da63cb33aebcc429bb6ab6808442c75082e0342 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 7 Mar 2024 16:12:22 -0800 Subject: [PATCH 2/4] Pass `force resim` to SimulationService --- .../nasa/jpl/aerie/merlin/server/http/MerlinBindings.java | 3 ++- .../merlin/server/services/CachedSimulationService.java | 7 ++++++- .../merlin/server/services/GetSimulationResultsAction.java | 4 ++-- .../aerie/merlin/server/services/SimulationService.java | 2 +- .../merlin/server/services/UncachedSimulationService.java | 4 ++-- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java index 4b6bcc9a2f..1d45f90867 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java @@ -178,10 +178,11 @@ private void getSimulationResults(final Context ctx) { try { final var body = parseJson(ctx.body(), hasuraSimulateActionP); final var planId = body.input().planId(); + final var force = body.input().force().orElse(false); this.checkPermissions(Action.simulate, body.session(), planId); - final var response = this.simulationAction.run(planId, body.session()); + final var response = this.simulationAction.run(planId, force, body.session()); ctx.result(ResponseSerializers.serializeSimulationResultsResponse(response).toString()); } catch (final InvalidEntityException ex) { ctx.status(400).result(ResponseSerializers.serializeInvalidEntityException(ex).toString()); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java index 98f0849a28..b9ba6a9469 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java @@ -14,7 +14,12 @@ public record CachedSimulationService ( ) implements SimulationService { @Override - public ResultsProtocol.State getSimulationResults(final PlanId planId, final RevisionData revisionData, final String requestedBy) { + public ResultsProtocol.State getSimulationResults( + final PlanId planId, + final boolean forceResim, + final RevisionData revisionData, + final String requestedBy) + { final var cell$ = this.store.lookup(planId); if (cell$.isPresent()) { return cell$.get().get(); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/GetSimulationResultsAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/GetSimulationResultsAction.java index bd3852d190..abbb3c6611 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/GetSimulationResultsAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/GetSimulationResultsAction.java @@ -35,11 +35,11 @@ public GetSimulationResultsAction( this.simulationService = Objects.requireNonNull(simulationService); } - public Response run(final PlanId planId, final HasuraAction.Session session) + public Response run(final PlanId planId, final boolean forceResim, final HasuraAction.Session session) throws NoSuchPlanException, MissionModelService.NoSuchMissionModelException { final var revisionData = this.planService.getPlanRevisionData(planId); - final var response = this.simulationService.getSimulationResults(planId, revisionData, session.hasuraUserId()); + final var response = this.simulationService.getSimulationResults(planId, forceResim, revisionData, session.hasuraUserId()); if (response instanceof ResultsProtocol.State.Pending r) { return new Response.Pending(r.simulationDatasetId()); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/SimulationService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/SimulationService.java index d8b8de2a20..cd733c982a 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/SimulationService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/SimulationService.java @@ -9,7 +9,7 @@ import java.util.Optional; public interface SimulationService { - ResultsProtocol.State getSimulationResults(PlanId planId, RevisionData revisionData, final String requestedBy); + ResultsProtocol.State getSimulationResults(PlanId planId, final boolean forceResim, RevisionData revisionData, final String requestedBy); Optional get(PlanId planId, RevisionData revisionData); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/UncachedSimulationService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/UncachedSimulationService.java index 7ece2efab2..1350e35aa0 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/UncachedSimulationService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/UncachedSimulationService.java @@ -14,7 +14,7 @@ public record UncachedSimulationService ( ) implements SimulationService { @Override - public ResultsProtocol.State getSimulationResults(final PlanId planId, final RevisionData revisionData, final String requestedBy) { + public ResultsProtocol.State getSimulationResults(final PlanId planId, final boolean forceResim, final RevisionData revisionData, final String requestedBy) { if (!(revisionData instanceof InMemoryRevisionData inMemoryRevisionData)) { throw new Error("UncachedSimulationService only accepts InMemoryRevisionData"); } @@ -40,7 +40,7 @@ public ResultsProtocol.State getSimulationResults(final PlanId planId, final Rev @Override public Optional get(final PlanId planId, final RevisionData revisionData) { return Optional.ofNullable( - getSimulationResults(planId, revisionData, null) instanceof ResultsProtocol.State.Success s ? + getSimulationResults(planId, false, revisionData, null) instanceof ResultsProtocol.State.Success s ? s.results() : null); } From c6c1516c0b60fe9b720ddd6f96103e578f46e601 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 7 Mar 2024 16:12:49 -0800 Subject: [PATCH 3/4] Add `forceAllocate` to ResultsCellRepository --- .../InMemoryResultsCellRepository.java | 5 +++ .../server/remotes/ResultsCellRepository.java | 1 + .../PostgresResultsCellRepository.java | 14 ++++++++ ...SimulationConfigurationRevisionAction.java | 35 +++++++++++++++++++ .../services/CachedSimulationService.java | 16 +++++---- 5 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/UpdateSimulationConfigurationRevisionAction.java diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/InMemoryResultsCellRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/InMemoryResultsCellRepository.java index 93402fa850..dd9e6a2773 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/InMemoryResultsCellRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/InMemoryResultsCellRepository.java @@ -46,6 +46,11 @@ public ResultsProtocol.OwnerRole allocate(final PlanId planId, final String requ } } + @Override + public ResultsProtocol.OwnerRole forceAllocate(PlanId planId, String requestedBy) { + return allocate(planId, requestedBy); + } + @Override public Optional claim(final PlanId planId, final Long datasetId) { return Optional.empty(); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ResultsCellRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ResultsCellRepository.java index df80e37b0b..8484efe00b 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ResultsCellRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ResultsCellRepository.java @@ -9,6 +9,7 @@ public interface ResultsCellRepository { ResultsProtocol.OwnerRole allocate(PlanId planId, String requestedBy); + ResultsProtocol.OwnerRole forceAllocate(PlanId planId, String requestedBy); Optional claim(PlanId planId, Long datasetId); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresResultsCellRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresResultsCellRepository.java index 7be407e4f6..15437d6526 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresResultsCellRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresResultsCellRepository.java @@ -88,6 +88,20 @@ public ResultsProtocol.OwnerRole allocate(final PlanId planId, final String requ } } + /** + * Forcibly allocate a simulation by updating the Simulation Configuration's revision + */ + @Override + public ResultsProtocol.OwnerRole forceAllocate(PlanId planId, String requestedBy) { + try (final var connection = this.dataSource.getConnection(); + final var updateSimConfig = new UpdateSimulationConfigurationRevisionAction(connection)) { + updateSimConfig.apply(planId.id()); + } catch (final SQLException ex) { + throw new DatabaseException("Failed to allocation simulation cell", ex); + } + return allocate(planId, requestedBy); + } + /** * Claim a simulation * diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/UpdateSimulationConfigurationRevisionAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/UpdateSimulationConfigurationRevisionAction.java new file mode 100644 index 0000000000..7bd18150dc --- /dev/null +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/UpdateSimulationConfigurationRevisionAction.java @@ -0,0 +1,35 @@ +package gov.nasa.jpl.aerie.merlin.server.remotes.postgres; + +import org.intellij.lang.annotations.Language; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/*package-local*/ final class UpdateSimulationConfigurationRevisionAction implements AutoCloseable { + private final @Language("SQL") String sql = """ + update simulation + set revision = revision + where plan_id = ?; + """; + + private final PreparedStatement statement; + + public UpdateSimulationConfigurationRevisionAction(final Connection connection) throws SQLException { + this.statement = connection.prepareStatement(sql); + } + + public void apply(final long planId) + throws SQLException + { + this.statement.setLong(1, planId); + final var count = this.statement.executeUpdate(); + if (count > 1) throw new Error("More than one row affected by sim config update by unique key. Is the database corrupted?"); + if (count == 0) throw new SQLException("No simulation configuration exists for plan "+planId); + } + + @Override + public void close() throws SQLException { + this.statement.close(); + } +} diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java index b9ba6a9469..01fd795d22 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java @@ -20,16 +20,20 @@ public ResultsProtocol.State getSimulationResults( final RevisionData revisionData, final String requestedBy) { + // If force resimulation is enabled, allocate a new cell regardless of whether there was already a valid cell + if (forceResim) { + return this.store.forceAllocate(planId, requestedBy).get(); + } + final var cell$ = this.store.lookup(planId); if (cell$.isPresent()) { return cell$.get().get(); - } else { - // Allocate a fresh cell. - final var cell = this.store.allocate(planId, requestedBy); - - // Return the current value of the reader; if it's incomplete, the caller can check it again later. - return cell.get(); } + + // Allocate a fresh cell. + final var cell = this.store.allocate(planId, requestedBy); + // Return the current value of the reader; if it's incomplete, the caller can check it again later. + return cell.get(); } @Override From b2ee901605a6f5d7dc850e17f407bd08cc36703b Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 11 Mar 2024 09:56:27 -0700 Subject: [PATCH 4/4] Add new e2eTests --- .../gov/nasa/jpl/aerie/e2e/BindingsTests.java | 55 +++++++++++++++--- .../nasa/jpl/aerie/e2e/SimulationTests.java | 46 +++++++++++++++ .../e2e/types/SimulationConfiguration.java | 25 ++++++++ .../gov/nasa/jpl/aerie/e2e/utils/GQL.java | 20 +++++++ .../jpl/aerie/e2e/utils/HasuraRequests.java | 58 +++++++++++++++++++ 5 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/SimulationConfiguration.java diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/BindingsTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/BindingsTests.java index 571b9a24fc..7d652eedc5 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/BindingsTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/BindingsTests.java @@ -21,6 +21,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import javax.json.Json; import javax.json.JsonArray; @@ -30,8 +33,10 @@ import java.io.StringReader; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Named.named; /** * Test the Action Bindings for the Merlin and Scheduler Servers @@ -156,16 +161,17 @@ void invalidPlanId() { // Returns a 404 if the PlanId is invalid // message is "no such plan" final String data = Json.createObjectBuilder() - .add("action", Json.createObjectBuilder().add("name", "simulate")) - .add("input", Json.createObjectBuilder().add("planId", -1)) - .add("request_query", "") - .add("session_variables", admin.getSession()) - .build() - .toString(); + .add("action", Json.createObjectBuilder().add("name", "simulate")) + .add("input", Json.createObjectBuilder().add("planId", -1)) + .add("request_query", "") + .add("session_variables", admin.getSession()) + .build() + .toString(); final var response = request.post("/getSimulationResults", RequestOptions.create().setData(data)); assertEquals(404, response.status()); assertEquals("no such plan", getBody(response).getString("message")); } + @Test void unauthorized() { // Returns a 403 if Unauthorized @@ -178,10 +184,12 @@ void unauthorized() { .toString(); final var response = request.post("/getSimulationResults", RequestOptions.create().setData(data)); assertEquals(403, response.status()); - assertEquals("User '"+nonOwner.name()+"' with role 'user' cannot perform 'simulate' because they are not " - + "a 'PLAN_OWNER_COLLABORATOR' for plan with id '"+planId+"'", - getBody(response).getString("message")); + assertEquals( + "User '" + nonOwner.name() + "' with role 'user' cannot perform 'simulate' because they are not " + + "a 'PLAN_OWNER_COLLABORATOR' for plan with id '" + planId + "'", + getBody(response).getString("message")); } + @Test void valid() throws InterruptedException { // Returns a 200 otherwise @@ -199,6 +207,35 @@ void valid() throws InterruptedException { // Delay 1s to allow any workers to finish with the request Thread.sleep(1000); } + + static Stream forceArgs() { + return Stream.of( + Arguments.arguments(named("valid, force is NULL", JsonValue.NULL)), + Arguments.arguments(named("valid, force is TRUE", JsonValue.TRUE)), + Arguments.arguments(named("valid, force is FALSE", JsonValue.FALSE)) + ); + } + + @ParameterizedTest + @MethodSource("forceArgs") + void validWithForce(JsonValue force) throws InterruptedException { + // Returns a 200 otherwise + // "status" is not "failed" + final String data = Json.createObjectBuilder() + .add("action", Json.createObjectBuilder().add("name", "simulate")) + .add( + "input", + Json.createObjectBuilder().add("planId", planId).add("force", force)) + .add("request_query", "") + .add("session_variables", admin.getSession()) + .build() + .toString(); + final var response = request.post("/getSimulationResults", RequestOptions.create().setData(data)); + assertEquals(200, response.status()); + assertNotEquals("failed", getBody(response).getString("status")); + // Delay 1s to allow any workers to finish with the request + Thread.sleep(1000); + } } @Nested diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java index fb19099fd9..28a482e199 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java @@ -319,4 +319,50 @@ void cancelingSimReturnsPartialResults() throws IOException { assertEquals(startedActivities, results.activities().size()); } } + + @Nested + class ForceResimulation { + private int simulationDatasetId; + @BeforeEach + void beforeEach() throws IOException { + simulationDatasetId = hasura.awaitSimulation(planId).simDatasetId(); + } + @Test + void noResimWhenNull() throws IOException { + assertEquals(simulationDatasetId, hasura.awaitSimulation(planId, null).simDatasetId()); + } + + @Test + void noResimWhenFalse() throws IOException { + assertEquals(simulationDatasetId, hasura.awaitSimulation(planId, false).simDatasetId()); + } + + @Test + void noResimWhenAbsent() throws IOException { + assertEquals(simulationDatasetId, hasura.awaitSimulation(planId).simDatasetId()); + } + + @Test + void resimOnlyUpdatesConfigRevision() throws IOException { + final int planRevision = hasura.getPlanRevision(planId); + final var simConfig = hasura.getSimConfig(planId); + + // Assert forcibly resimming returned a new simulation dataset + final var newSimDatasetId = hasura.awaitSimulation(planId, true).simDatasetId(); + assertNotEquals(simulationDatasetId, newSimDatasetId); + + // Assert that the plan revision is unchanged + assertEquals(planRevision, hasura.getPlanRevision(planId)); + + // Assert that the simulation configuration has only had its revision updated + final var newSimConfig = hasura.getSimConfig(planId); + assertNotEquals(simConfig.revision(), newSimConfig.revision()); + assertEquals(simConfig.id(), newSimConfig.id()); + assertEquals(simConfig.planId(), newSimConfig.planId()); + assertEquals(simConfig.simulationTemplateId(), newSimConfig.simulationTemplateId()); + assertEquals(simConfig.arguments(), newSimConfig.arguments()); + assertEquals(simConfig.simulationStartTime(), newSimConfig.simulationStartTime()); + assertEquals(simConfig.simulationEndTime(), newSimConfig.simulationEndTime()); + } + } } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/SimulationConfiguration.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/SimulationConfiguration.java new file mode 100644 index 0000000000..3c305faccf --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/SimulationConfiguration.java @@ -0,0 +1,25 @@ +package gov.nasa.jpl.aerie.e2e.types; + +import javax.json.JsonObject; +import java.util.Optional; + +public record SimulationConfiguration( + int id, + int revision, + int planId, + Optional simulationTemplateId, + JsonObject arguments, + String simulationStartTime, + String simulationEndTime +) { + public static SimulationConfiguration fromJSON(JsonObject json) { + return new SimulationConfiguration( + json.getInt("id"), + json.getInt("revision"), + json.getInt("plan_id"), + json.isNull("simulation_template_id") ? Optional.empty() : Optional.of(json.getInt("simulation_template_id")), + json.getJsonObject("arguments"), + json.getString("simulation_start_time"), + json.getString("simulation_end_time")); + } +} 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 68554dedff..a2dda9d2d1 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 @@ -384,6 +384,18 @@ query GetSchedulingRequest($specificationId: Int!, $specificationRev: Int!) { status } }"""), + GET_SIMULATION_CONFIGURATION(""" + query GetSimConfig($planId: Int!) { + sim_config: simulation(where: {plan_id: {_eq:$planId}}) { + id + revision + plan_id + simulation_template_id + arguments + simulation_start_time + simulation_end_time + } + }"""), GET_SIMULATION_DATASET(""" query GetSimulationDataset($id: Int!) { simulationDataset: simulation_dataset_by_pk(id: $id) { @@ -501,6 +513,14 @@ query Simulate($plan_id: Int!) { simulationDatasetId } }"""), + SIMULATE_FORCE(""" + query SimulateForce($plan_id: Int!, $force: Boolean) { + simulate(planId: $plan_id, force: $force){ + status + reason + simulationDatasetId + } + }"""), UPDATE_ACTIVITY_DIRECTIVE_ARGUMENTS(""" mutation updateActivityDirectiveArguments($id: Int!, $plan_id: Int!, $arguments: jsonb!) { updateActivityDirectiveArguments: update_activity_directive_by_pk( 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 d854205198..6d66ea2333 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 @@ -260,6 +260,16 @@ private SimulationResponse simulate(int planId) throws IOException { return SimulationResponse.fromJSON(makeRequest(GQL.SIMULATE, variables).getJsonObject("simulate")); } + private SimulationResponse simulateForce(int planId, Boolean force) throws IOException { + final var variables = Json.createObjectBuilder().add("plan_id", planId); + if (force == null) { + variables.add("force", JsonValue.NULL); + } else { + variables.add("force", force); + } + return SimulationResponse.fromJSON(makeRequest(GQL.SIMULATE_FORCE, variables.build()).getJsonObject("simulate")); + } + private SimulationDataset cancelSimulation(int simDatasetId, int timeout) throws IOException { final var variables = Json.createObjectBuilder().add("id", simDatasetId).build(); makeRequest(GQL.CANCEL_SIMULATION, variables); @@ -307,6 +317,47 @@ public SimulationResponse awaitSimulation(int planId, int timeout) throws IOExce throw new TimeoutError("Simulation timed out after " + timeout + " seconds"); } + /** + * Simulate the specified plan, potentially forcibly, with a timeout of 30 seconds + * @param planId the plan to simulate + * @param force whether to forcibly resimulate in the event of an existing dataset. + */ + public SimulationResponse awaitSimulation(int planId, Boolean force) throws IOException { + return awaitSimulation(planId, force, 30); + } + + /** + * Simulate the specified plan, potentially forcibly, with a set timeout + * @param planId the plan to simulate + * @param force whether to forcibly resimulate in the event of an existing dataset. + * @param timeout the length of the timeout, in seconds + */ + public SimulationResponse awaitSimulation(int planId, Boolean force, int timeout) throws IOException { + for (int i = 0; i < timeout; ++i) { + final SimulationResponse response; + // Only use force on the initial request to avoid an infinite loop of making new sim requests + if (i == 0) { + response = simulateForce(planId, force); + } else { + response = simulate(planId); + } + switch (response.status()) { + case "pending", "incomplete" -> { + try { + Thread.sleep(1000); // 1s + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + case "complete" -> { + return response; + } + default -> fail("Simulation returned bad status " + response.status() + " with reason " + response.reason()); + } + } + throw new TimeoutError("Simulation timed out after " + timeout + " seconds"); + } + /** * Start and immediately cancel a simulation with a timeout of 30 seconds * @param planId the plan to simulate @@ -351,6 +402,13 @@ public int getSimulationId(int planId) throws IOException { return makeRequest(GQL.GET_SIMULATION_ID, variables).getJsonArray("simulation").getJsonObject(0).getInt("id"); } + public SimulationConfiguration getSimConfig(int planId) throws IOException { + final var variables = Json.createObjectBuilder().add("planId", planId).build(); + final var simConfig = makeRequest(GQL.GET_SIMULATION_CONFIGURATION, variables).getJsonArray("sim_config"); + assertEquals(1, simConfig.size()); + return SimulationConfiguration.fromJSON(simConfig.getJsonObject(0)); + } + public int insertAndAssociateSimTemplate(int modelId, String description, JsonObject arguments, int simConfigId) throws IOException {