From d77cb64391074af271b3493ae4027462481ec8e9 Mon Sep 17 00:00:00 2001 From: Paul Rosemurgy Date: Thu, 4 Aug 2022 14:30:37 -0700 Subject: [PATCH 1/5] Remove redundant use of `final` for `record` --- .../aerie/merlin/server/services/CreateSimulationMessage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CreateSimulationMessage.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CreateSimulationMessage.java index f066b6b256..33b39c2d5e 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CreateSimulationMessage.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CreateSimulationMessage.java @@ -10,7 +10,7 @@ import java.util.Map; import java.util.Objects; -public final record CreateSimulationMessage( +public record CreateSimulationMessage( String missionModelId, Instant startTime, Duration samplingDuration, From aeb8cef22b2cb3faf361dce77d53b0a8f7ca5285 Mon Sep 17 00:00:00 2001 From: Paul Rosemurgy Date: Thu, 4 Aug 2022 14:31:28 -0700 Subject: [PATCH 2/5] Add plan start to mission model `instantiate()` --- .../aerie/banananation/SimulationUtility.java | 6 ++--- .../foomissionmodel/SimulateMapSchedule.java | 11 ++++----- .../merlin/driver/MissionModelLoader.java | 23 ++++++++++++------- .../generator/MissionModelGenerator.java | 5 ++++ .../protocol/model/MissionModelFactory.java | 3 ++- .../services/LocalMissionModelService.java | 18 +++++++++++---- .../server/models/MissionModelTest.java | 5 ++-- .../services/SynchronousSchedulerAgent.java | 2 +- .../services/SchedulingIntegrationTests.java | 2 ++ .../aerie/scheduler/SimulationUtility.java | 8 +++---- 10 files changed, 53 insertions(+), 30 deletions(-) diff --git a/examples/banananation/src/test/java/gov/nasa/jpl/aerie/banananation/SimulationUtility.java b/examples/banananation/src/test/java/gov/nasa/jpl/aerie/banananation/SimulationUtility.java index d73e9209db..77a369c060 100644 --- a/examples/banananation/src/test/java/gov/nasa/jpl/aerie/banananation/SimulationUtility.java +++ b/examples/banananation/src/test/java/gov/nasa/jpl/aerie/banananation/SimulationUtility.java @@ -17,11 +17,11 @@ import java.util.Map; public final class SimulationUtility { - private static MissionModel makeMissionModel(final MissionModelBuilder builder, final Configuration config) { + private static MissionModel makeMissionModel(final MissionModelBuilder builder, final Instant planStart, final Configuration config) { final var factory = new GeneratedMissionModelFactory(); final var registry = DirectiveTypeRegistry.extract(factory); // TODO: [AERIE-1516] Teardown the model to release any system resources (e.g. threads). - final var model = factory.instantiate(registry.registry(), config, builder); + final var model = factory.instantiate(registry.registry(), planStart, config, builder); return builder.build(model, factory.getConfigurationType(), registry); } @@ -29,8 +29,8 @@ private static MissionModel makeMissionModel(final MissionModelBuilder builde simulate(final Map> schedule, final Duration simulationDuration) { final var dataPath = Path.of(SimulationUtility.class.getResource("data/lorem_ipsum.txt").getPath()); final var config = new Configuration(Configuration.DEFAULT_PLANT_COUNT, Configuration.DEFAULT_PRODUCER, dataPath); - final var missionModel = makeMissionModel(new MissionModelBuilder(), config); final var startTime = Instant.now(); + final var missionModel = makeMissionModel(new MissionModelBuilder(), Instant.EPOCH, config); return SimulationDriver.simulate( missionModel, diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulateMapSchedule.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulateMapSchedule.java index 83172ce57a..e0096e0c2e 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulateMapSchedule.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulateMapSchedule.java @@ -29,23 +29,22 @@ public static void main(final String[] args) { } private static MissionModel> - makeMissionModel(final MissionModelBuilder builder, final Configuration config) { + makeMissionModel(final MissionModelBuilder builder, final Instant planStart, final Configuration config) { final var factory = new GeneratedMissionModelFactory(); final var registry = DirectiveTypeRegistry.extract(factory); - final var model = factory.instantiate(registry.registry(), config, builder); + final var model = factory.instantiate(registry.registry(), planStart, config, builder); return builder.build(model, factory.getConfigurationType(), registry); } private static void simulateWithMapSchedule() { final var config = new Configuration(); - final var missionModel = makeMissionModel(new MissionModelBuilder(), config); + final var startTime = Instant.now(); + final var simulationDuration = duration(25, SECONDS); + final var missionModel = makeMissionModel(new MissionModelBuilder(), Instant.EPOCH, config); try { final var schedule = loadSchedule(); - final var startTime = Instant.now(); - final var simulationDuration = duration(25, SECONDS); - final var simulationResults = SimulationDriver.simulate( missionModel, schedule, diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelLoader.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelLoader.java index 50984ef241..3a77ceae38 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelLoader.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelLoader.java @@ -15,38 +15,45 @@ import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.time.Instant; import java.util.jar.JarFile; public final class MissionModelLoader { public static MissionModelFactory loadMissionModelFactory(final Path path, final String name, final String version) - throws MissionModelLoadException + throws MissionModelLoadException { final var service = loadMissionModelProvider(path, name, version); return service.getFactory(); } - public static MissionModel loadMissionModel(final SerializedValue missionModelConfig, final Path path, final String name, final String version) - throws MissionModelLoadException + public static MissionModel loadMissionModel( + final Instant planStart, + final SerializedValue missionModelConfig, + final Path path, + final String name, + final String version) + throws MissionModelLoadException { final var service = loadMissionModelProvider(path, name, version); final var factory = service.getFactory(); final var builder = new MissionModelBuilder(); - return loadMissionModel(missionModelConfig, factory, builder); + return loadMissionModel(planStart, missionModelConfig, factory, builder); } private static MissionModel loadMissionModel( + final Instant planStart, final SerializedValue missionModelConfig, final MissionModelFactory factory, - final MissionModelBuilder builder - ) { + final MissionModelBuilder builder) + { try { final var serializedConfigMap = missionModelConfig.asMap().orElseThrow(ConfigurationType.UnconstructableConfigurationException::new); final var config = factory.getConfigurationType().instantiate(serializedConfigMap); final var registry = DirectiveTypeRegistry.extract(factory); - final var model = factory.instantiate(registry.registry(), config, builder); + final var model = factory.instantiate(registry.registry(), planStart, config, builder); return builder.build(model, factory.getConfigurationType(), registry); } catch (final ConfigurationType.UnconstructableConfigurationException | InvalidArgumentsException ex) { throw new MissionModelInstantiationException(ex); @@ -54,7 +61,7 @@ MissionModel loadMissionModel( } public static MerlinPlugin loadMissionModelProvider(final Path path, final String name, final String version) - throws MissionModelLoadException + throws MissionModelLoadException { // Look for a MerlinMissionModel implementor in the mission model. For correctness, we're assuming there's // only one matching MerlinMissionModel in any given mission model. diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java index e6d3c8327b..3bbe70aaa9 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java @@ -40,6 +40,7 @@ import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -192,6 +193,10 @@ public JavaFile generateMissionModelFactory(final MissionModelRecord missionMode missionModel.getTypesName(), "registry", Modifier.FINAL) + .addParameter( + ClassName.get(Instant.class), + "simulationStart", + Modifier.FINAL) .addParameter( missionModel.modelConfigurationType .map($ -> ClassName.get($.declaration())) diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/MissionModelFactory.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/MissionModelFactory.java index 448aa05253..dad6f59ae2 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/MissionModelFactory.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/MissionModelFactory.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.merlin.protocol.model; +import java.time.Instant; import gov.nasa.jpl.aerie.merlin.protocol.driver.DirectiveTypeRegistrar; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; @@ -8,5 +9,5 @@ public interface MissionModelFactory { ConfigurationType getConfigurationType(); - Model instantiate(Registry registry, Config configuration, Initializer builder); + Model instantiate(Registry registry, Instant planStart, Config configuration, Initializer builder); } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index 10b7e7973e..b28566b32f 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory; import java.nio.file.Path; +import java.time.Instant; import java.util.List; import java.util.Map; @@ -189,7 +190,7 @@ public SimulationResults runSimulation(final CreateSimulationMessage message) } // TODO: [AERIE-1516] Teardown the mission model after use to release any system resources (e.g. threads). - return loadConfiguredMissionModel(message.missionModelId(), SerializedValue.of(config)) + return loadConfiguredMissionModel(message.missionModelId(), message.startTime(), SerializedValue.of(config)) .simulate(message.activityInstances(), message.samplingDuration(), message.startTime()); } @@ -246,7 +247,7 @@ private MissionModelFacade.Unconfigured loadUnconfiguredMissionModel(final St private MissionModelFacade loadConfiguredMissionModel(final String missionModelId) throws NoSuchMissionModelException, MissionModelLoadException { - return loadConfiguredMissionModel(missionModelId, SerializedValue.of(Map.of())); + return loadConfiguredMissionModel(missionModelId, Instant.EPOCH, SerializedValue.of(Map.of())); } /** @@ -259,13 +260,20 @@ private MissionModelFacade loadConfiguredMissionModel(final String missionModelI * it contains may not abide by the expected contract at load time. * @throws NoSuchMissionModelException If no mission model is known by the given ID. */ - private MissionModelFacade loadConfiguredMissionModel(final String missionModelId, final SerializedValue configuration) + private MissionModelFacade loadConfiguredMissionModel( + final String missionModelId, + final Instant planStart, + final SerializedValue configuration) throws NoSuchMissionModelException, MissionModelLoadException { try { final var missionModelJar = this.missionModelRepository.getMissionModel(missionModelId); - final var missionModel = - MissionModelLoader.loadMissionModel(configuration, missionModelDataPath.resolve(missionModelJar.path), missionModelJar.name, missionModelJar.version); + final var missionModel = MissionModelLoader.loadMissionModel( + planStart, + configuration, + missionModelDataPath.resolve(missionModelJar.path), + missionModelJar.name, + missionModelJar.version); return new MissionModelFacade(missionModel); } catch (final MissionModelRepository.NoSuchMissionModelException ex) { throw new NoSuchMissionModelException(missionModelId, ex); diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/models/MissionModelTest.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/models/MissionModelTest.java index 9024570795..621c4acfd6 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/models/MissionModelTest.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/models/MissionModelTest.java @@ -6,7 +6,7 @@ import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.protocol.model.TaskSpecType; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InvalidArgumentsException; import gov.nasa.jpl.aerie.merlin.protocol.types.Parameter; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; @@ -16,6 +16,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,7 +39,7 @@ public void initialize() { private static MissionModelFacade makeMissionModel(final MissionModelBuilder builder, final Configuration config) { final var factory = new GeneratedMissionModelFactory(); final var registry = DirectiveTypeRegistry.extract(factory); - final var model = factory.instantiate(registry.registry(), config, builder); + final var model = factory.instantiate(registry.registry(), Instant.EPOCH, config, builder); return new MissionModelFacade(builder.build(model, factory.getConfigurationType(), registry)); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SynchronousSchedulerAgent.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SynchronousSchedulerAgent.java index 792362bd8e..ee70de1ad5 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SynchronousSchedulerAgent.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SynchronousSchedulerAgent.java @@ -304,7 +304,7 @@ private SchedulerMissionModel loadMissionModel(final PlanMetadata plan) { final var missionConfig = SerializedValue.of(plan.modelConfiguration()); final var modelJarPath = modelJarsDir.resolve(plan.modelPath()); return new SchedulerMissionModel( - MissionModelLoader.loadMissionModel(missionConfig, modelJarPath, plan.modelName(), plan.modelVersion()), + MissionModelLoader.loadMissionModel(plan.horizon().getStartInstant(), missionConfig, modelJarPath, plan.modelName(), plan.modelVersion()), loadSchedulerModelProvider(modelJarPath, plan.modelName(), plan.modelVersion()).getSchedulerModel()); } catch (MissionModelLoader.MissionModelLoadException | SchedulerModelLoadException e) { throw new ResultsProtocolFailure(e); diff --git a/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulingIntegrationTests.java b/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulingIntegrationTests.java index 463cce0655..f105a04a27 100644 --- a/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulingIntegrationTests.java +++ b/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulingIntegrationTests.java @@ -22,6 +22,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -895,6 +896,7 @@ static MissionModelService.MissionModelTypes loadMissionModelTypesFromJar( throws MissionModelLoader.MissionModelLoadException { final var missionModel = MissionModelLoader.loadMissionModel( + Instant.EPOCH, SerializedValue.of(configuration), Path.of(jarPath), "", diff --git a/scheduler/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java b/scheduler/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java index c0a321f3cf..00f0362670 100644 --- a/scheduler/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java +++ b/scheduler/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java @@ -10,23 +10,24 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import java.nio.file.Path; +import java.time.Instant; public final class SimulationUtility { private static MissionModel makeMissionModel(final MissionModelBuilder builder, final Configuration config) { final var factory = new gov.nasa.jpl.aerie.banananation.generated.GeneratedMissionModelFactory(); final var registry = DirectiveTypeRegistry.extract(factory); - final var model = factory.instantiate(registry.registry(), config, builder); + final var model = factory.instantiate(registry.registry(), Instant.EPOCH, config, builder); return builder.build(model, factory.getConfigurationType(), registry); } public static MissionModel> getFooMissionModel() { - final var conf = new gov.nasa.jpl.aerie.foomissionmodel.Configuration(); + final var config = new gov.nasa.jpl.aerie.foomissionmodel.Configuration(); final var factory = new gov.nasa.jpl.aerie.foomissionmodel.generated.GeneratedMissionModelFactory(); final var registry = DirectiveTypeRegistry.extract(factory); final var builder = new MissionModelBuilder(); - final var model = factory.instantiate(registry.registry(), conf, builder); + final var model = factory.instantiate(registry.registry(), Instant.EPOCH, config, builder); return builder.build(model, factory.getConfigurationType(), registry); } @@ -42,5 +43,4 @@ public static MissionModel getBananaMissionModel(){ public static SchedulerModel getBananaSchedulerModel(){ return new gov.nasa.jpl.aerie.banananation.generated.GeneratedSchedulerModel(); } - } From 36828614aa8d93177268d975f1d7515f280c8678 Mon Sep 17 00:00:00 2001 From: Paul Rosemurgy Date: Thu, 4 Aug 2022 14:31:28 -0700 Subject: [PATCH 3/5] Pass plan start to mission model if expected --- .../merlin/processor/MissionModelParser.java | 95 +++++++++++++++---- .../generator/MissionModelGenerator.java | 41 +++++--- .../metamodel/MissionModelRecord.java | 3 + 3 files changed, 111 insertions(+), 28 deletions(-) diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java index 834148cd19..f05c0e056d 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.merlin.processor; import com.squareup.javapoet.ClassName; +import gov.nasa.jpl.aerie.merlin.framework.Registrar; import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; import gov.nasa.jpl.aerie.merlin.framework.annotations.Export; import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel; @@ -30,6 +31,7 @@ import javax.lang.model.util.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Repeatable; +import java.time.Instant; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -48,23 +50,73 @@ public MissionModelRecord parseMissionModel(final PackageElement missionModelElement) throws InvalidMissionModelException { - final var topLevelModel = this.getMissionModelModel(missionModelElement); - final var modelConfigurationType = this.getMissionModelConfigurationType(missionModelElement); - final var activityTypes = new ArrayList(); - final var typeRules = new ArrayList(); + final var topLevelModel = this.getMissionModel(missionModelElement); + final var typeRules = new ArrayList(); for (final var factory : this.getMissionModelMapperClasses(missionModelElement)) { typeRules.addAll(this.parseValueMappers(factory)); } + final var activityTypes = new ArrayList(); for (final var activityTypeElement : this.getMissionModelActivityTypes(missionModelElement)) { activityTypes.add(this.parseActivityType(missionModelElement, activityTypeElement)); } - return new MissionModelRecord(missionModelElement, topLevelModel, modelConfigurationType, typeRules, activityTypes); + return new MissionModelRecord( + missionModelElement, + topLevelModel.type, + topLevelModel.expectsPlanStart, + topLevelModel.configurationType, + typeRules, + activityTypes); + } + + private record MissionModelTypeRecord( + TypeElement type, + boolean expectsPlanStart, + Optional configurationType) { } + + private MissionModelTypeRecord getMissionModel(final PackageElement missionModelElement) + throws InvalidMissionModelException + { + final var configurationType = this.getMissionModelConfigurationType(missionModelElement); + final var modelTypeElement = this.getMissionModelTypeElement(missionModelElement); + + final var parameters = this.getMissionModelConstructor(modelTypeElement).getParameters().stream() + .map(p -> ClassName.get(p.asType())) + .toList(); + final var nParameters = parameters.size(); + final var foundPlanStart = parameters.contains(ClassName.get(Instant.class)); + + // Ensure model only has one constructor, one of: + // - (Registrar, Instant, Configuration) + // - (Registrar, Configuration) + // - (Registrar, Instant) + // - (Registrar) + final var expectedParameters = configurationType + // Configuration expected + .map(configType -> foundPlanStart && nParameters > 2 ? // If plan start `Instant` truly is expected + List.of(ClassName.get(Registrar.class), ClassName.get(Instant.class), ClassName.get(configType.declaration())) : + List.of(ClassName.get(Registrar.class), ClassName.get(configType.declaration()))) + // No configuration expected + .orElseGet(() -> foundPlanStart && nParameters > 1 ? // If plan start `Instant` truly is expected + List.of(ClassName.get(Registrar.class), ClassName.get(Instant.class)) : + List.of(ClassName.get(Registrar.class))); + + if (!parameters.equals(expectedParameters)) { + throw new InvalidMissionModelException( + "The top-level model's constructor is expected to accept %s, found: %s" + .formatted(expectedParameters.toString(), parameters.toString()), + missionModelElement); + } + + // TODO: Consider enrolling the given model in a dependency injection framework, + // such that the Cursor can be injected like any other constructor argument, + // and indeed such that other arguments can flexibly be supported. + return new MissionModelTypeRecord(modelTypeElement, foundPlanStart, configurationType); } - private TypeElement getMissionModelModel(final PackageElement missionModelElement) + private TypeElement getMissionModelTypeElement(final PackageElement missionModelElement) throws InvalidMissionModelException { final var annotationMirror = this @@ -73,23 +125,34 @@ private TypeElement getMissionModelModel(final PackageElement missionModelElemen "The missionModel package is somehow missing an @MissionModel annotation", missionModelElement)); - final var modelAttribute = - getAnnotationAttribute(annotationMirror, "model").orElseThrow(); - if (!(modelAttribute.getValue() instanceof DeclaredType)) { + final var modelAttribute = getAnnotationAttribute(annotationMirror, "model").orElseThrow(); + if (!(modelAttribute.getValue() instanceof final DeclaredType model)) { throw new InvalidMissionModelException( "The top-level model is not yet defined", missionModelElement, annotationMirror, modelAttribute); } + return (TypeElement) model.asElement(); + } - // TODO: Check that the given model conforms to the expected protocol. - // * Has a (1,1) constructor that takes a Registrar. - // It doesn't actually need to subclass Model. - // TODO: Consider enrolling the given model in a dependency injection framework, - // such that the Cursor can be injected like any other constructor argument, - // and indeed such that other arguments can flexibly be supported. - return (TypeElement) ((DeclaredType) modelAttribute.getValue()).asElement(); + private ExecutableElement getMissionModelConstructor(final TypeElement missionModelTypeElement) + throws InvalidMissionModelException + { + final var constructors = missionModelTypeElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR && e.getModifiers().contains(Modifier.PUBLIC)) + .toList(); + if (constructors.isEmpty()) { + throw new InvalidMissionModelException( + "The top-level model declares no public constructor", + missionModelTypeElement); + } + if (constructors.size() > 1) { + throw new InvalidMissionModelException( + "The top-level model declares more than one public constructor", + missionModelTypeElement); + } + return (ExecutableElement) constructors.get(0); } private Optional getMissionModelConfigurationType(final PackageElement missionModelElement) diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java index 3bbe70aaa9..4ab18d0cbf 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java @@ -41,7 +41,6 @@ import javax.lang.model.util.Types; import javax.tools.Diagnostic; import java.time.Instant; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -195,7 +194,7 @@ public JavaFile generateMissionModelFactory(final MissionModelRecord missionMode Modifier.FINAL) .addParameter( ClassName.get(Instant.class), - "simulationStart", + "planStart", Modifier.FINAL) .addParameter( missionModel.modelConfigurationType @@ -233,16 +232,7 @@ public JavaFile generateMissionModelFactory(final MissionModelRecord missionMode gov.nasa.jpl.aerie.merlin.framework.InitializationContext.class, "executor", "builder", - (missionModel.modelConfigurationType.isPresent()) - ? CodeBlock.of( - "new $T($L, $L)", - ClassName.get(missionModel.topLevelModel), - "registrar", - "configuration") - : CodeBlock.of( - "new $T($L))", - ClassName.get(missionModel.topLevelModel), - "registrar")) + generateMissionModelInstantiation(missionModel)) .addStatement( "return new $T<>($L, $L, $L)", gov.nasa.jpl.aerie.merlin.framework.RootModel.class, @@ -258,6 +248,33 @@ public JavaFile generateMissionModelFactory(final MissionModelRecord missionMode .build(); } + private static CodeBlock generateMissionModelInstantiation(final MissionModelRecord missionModel) { + final var modelClassName = ClassName.get(missionModel.topLevelModel); + return missionModel.modelConfigurationType + .map(configType -> missionModel.expectsPlanStart ? + CodeBlock.of( + "new $T($L, $L, $L)", + modelClassName, + "registrar", + "planStart", + "configuration") : + CodeBlock.of( + "new $T($L, $L)", + modelClassName, + "registrar", + "configuration")) + .orElseGet(() -> missionModel.expectsPlanStart ? + CodeBlock.of( + "new $T($L, $L)", + modelClassName, + "registrar", + "planStart") : + CodeBlock.of( + "new $T($L)", + modelClassName, + "registrar")); + } + /** Generate `GeneratedSchedulerModel` class. */ public JavaFile generateSchedulerModel(final MissionModelRecord missionModel) { final var typeName = missionModel.getSchedulerModelName(); diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/MissionModelRecord.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/MissionModelRecord.java index 646add7167..999a1023dd 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/MissionModelRecord.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/MissionModelRecord.java @@ -13,17 +13,20 @@ public final class MissionModelRecord { public final TypeElement topLevelModel; public final List typeRules; public final List activityTypes; + public final boolean expectsPlanStart; public final Optional modelConfigurationType; public MissionModelRecord( final PackageElement $package, final TypeElement topLevelModel, + final boolean expectsPlanStart, final Optional modelConfigurationType, final List typeRules, final List activityTypes) { this.$package = Objects.requireNonNull($package); this.topLevelModel = Objects.requireNonNull(topLevelModel); + this.expectsPlanStart = expectsPlanStart; this.modelConfigurationType = Objects.requireNonNull(modelConfigurationType); this.typeRules = Objects.requireNonNull(typeRules); this.activityTypes = Objects.requireNonNull(activityTypes); From b5240cf59f6c0d59ef2f0d27eeca74ad88b01266 Mon Sep 17 00:00:00 2001 From: Paul Rosemurgy Date: Mon, 15 Aug 2022 13:37:02 -0700 Subject: [PATCH 4/5] Add plan start to `foomissionmodel` --- .../java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java | 5 ++++- .../nasa/jpl/aerie/foomissionmodel/FooActivityTest.java | 3 ++- .../aerie/foomissionmodel/MissionConfigurationTest.java | 7 +++++-- .../jpl/aerie/merlin/server/models/MissionModelTest.java | 1 - 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java index 97d49f29bf..24719ba866 100644 --- a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java +++ b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java @@ -25,6 +25,7 @@ public final class Mission { // Need to generalize RealDynamics to nonlinear polynomials. public final Register foo = Register.forImmutable(0.0); + public final Register startingAfterUnixEpoch; public final Accumulator data = new Accumulator(); public final Accumulator source = new Accumulator(100.0, 1.0); public final Accumulator sink; @@ -37,9 +38,10 @@ public final class Mission { public final Clock utcClock = new Clock(Instant.parse("2023-08-18T00:00:00.00Z")); private final Registrar cachedRegistrar; // Normally bad practice, only stored to demonstrate built/unbuilt check - public Mission(final Registrar registrar, final Configuration config) { + public Mission(final Registrar registrar, final Instant planStart, final Configuration config) { this.cachedRegistrar = registrar; + this.startingAfterUnixEpoch = Register.forImmutable(planStart.compareTo(Instant.EPOCH) > 0); this.sink = new Accumulator(0.0, config.sinkRate); spawn(this::test); @@ -52,6 +54,7 @@ public Mission(final Registrar registrar, final Configuration config) { // (This binding layer should also be the one responsible for feeding in constructor parameters.) registrar.discrete("/foo", this.foo, new DoubleValueMapper()); registrar.discrete("/foo/conflicted", this.foo::isConflicted, new BooleanValueMapper()); + registrar.discrete("/foo/starting_after_unix_epoch", startingAfterUnixEpoch, new BooleanValueMapper()); registrar.real("/batterySoC", this.source.minus(this.sink)); registrar.real("/data", this.data); registrar.real("/data/rate", this.data.rate); diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooActivityTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooActivityTest.java index 91be526367..26f393fcb0 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooActivityTest.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooActivityTest.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.foomissionmodel; +import java.time.Instant; import gov.nasa.jpl.aerie.foomissionmodel.activities.FooActivity; import gov.nasa.jpl.aerie.foomissionmodel.generated.ActivityTypes; import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; @@ -32,7 +33,7 @@ public final class FooActivityTest { // The `Registrar` does not need to be declared as a parameter, but will be injected if declared. public FooActivityTest(final MerlinTestContext ctx) { // Model configuration can be provided directly, just as for a normal Java class constructor. - this.model = new Mission(ctx.registrar(), new Configuration()); + this.model = new Mission(ctx.registrar(), Instant.EPOCH, new Configuration()); // Activities must be registered explicitly in order to be used in testing. // The generated `ActivityTypes` helper class loads all declared activities, diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/MissionConfigurationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/MissionConfigurationTest.java index 17a7e22587..c31db90ab6 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/MissionConfigurationTest.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/MissionConfigurationTest.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.foomissionmodel; +import java.time.Instant; import gov.nasa.jpl.aerie.foomissionmodel.activities.FooActivity; import gov.nasa.jpl.aerie.foomissionmodel.generated.ActivityTypes; import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; @@ -26,12 +27,13 @@ public final class MissionConfigurationTest { private final Mission model; public Test1(final MerlinTestContext ctx) { - this.model = new Mission(ctx.registrar(), new Configuration()); + this.model = new Mission(ctx.registrar(), Instant.EPOCH, new Configuration()); ctx.use(model, ActivityTypes::register); } @Test public void test() { + assertThat(model.startingAfterUnixEpoch.get()).isEqualTo(false); spawn(new FooActivity()); delay(1, Duration.SECOND); assertThat(model.sink.get()).isCloseTo(0.5, within(1e-9)); @@ -47,12 +49,13 @@ public void test() { private final Mission model; public Test2(final MerlinTestContext ctx) { - this.model = new Mission(ctx.registrar(), new Configuration(2.0)); + this.model = new Mission(ctx.registrar(), Instant.EPOCH.plusSeconds(1), new Configuration(2.0)); ctx.use(model, ActivityTypes::register); } @Test public void test() { + assertThat(model.startingAfterUnixEpoch.get()).isEqualTo(true); spawn(new FooActivity()); delay(1, Duration.SECOND); assertThat(model.sink.get()).isCloseTo(2.0, within(1e-9)); diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/models/MissionModelTest.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/models/MissionModelTest.java index 621c4acfd6..532dbce347 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/models/MissionModelTest.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/models/MissionModelTest.java @@ -6,7 +6,6 @@ import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InvalidArgumentsException; import gov.nasa.jpl.aerie.merlin.protocol.types.Parameter; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; From 6c98d18ed581da77760e3fcfde890f469229b1da Mon Sep 17 00:00:00 2001 From: Paul Rosemurgy Date: Thu, 25 Aug 2022 12:34:34 -0700 Subject: [PATCH 5/5] Add `UNTRUE_PLAN_START` env var --- deployment/Environment.md | 80 ++++++++++--------- deployment/README.md | 30 ++++--- deployment/docker-compose.yml | 2 + docker-compose.yml | 3 + e2e-tests/docker-compose-test.yml | 3 + .../aerie/merlin/server/AerieAppDriver.java | 25 +++--- .../server/config/AppConfiguration.java | 5 +- .../jpl/aerie/merlin/server/config/Store.java | 4 +- .../services/LocalMissionModelService.java | 7 +- .../jpl/aerie/merlin/server/DevAppDriver.java | 3 +- .../merlin/worker/MerlinWorkerAppDriver.java | 19 +++-- .../merlin/worker/WorkerAppConfiguration.java | 5 +- 12 files changed, 110 insertions(+), 76 deletions(-) diff --git a/deployment/Environment.md b/deployment/Environment.md index c8c3ce60ff..a2b67e8503 100644 --- a/deployment/Environment.md +++ b/deployment/Environment.md @@ -19,10 +19,10 @@ This document provides detailed information about environment variables for each | `MERLIN_GRAPHQL_URL` | URI of the Aerie GraphQL API | `string` | http://hasura:8080/v1/graphql | | `COMMANDING_SERVER_PORT` | Port the server listens on | `number` | 27184 | | `COMMANDING_DB` | Name of Commanding Postgres database | `string` | aerie_commanding | -| `COMMANDING_DB_SERVER` | Hostname of Postgres instance | `string` | | -| `COMMANDING_DB_PASSWORD` | Password of Postgres instance | `string` | | +| `COMMANDING_DB_SERVER` | Hostname of Postgres instance | `string` | | +| `COMMANDING_DB_PASSWORD` | Password of Postgres instance | `string` | | | `COMMANDING_DB_PORT` | Port of Postgres instance | `number` | 5432 | -| `COMMANDING_DB_USER` | User of Postgres instance | `string` | | +| `COMMANDING_DB_USER` | User of Postgres instance | `string` | | | `COMMANDING_LOCAL_STORE` | Local storage file storage in the container | `string` | /usr/src/app/commanding_file_store | ## Aerie Gateway @@ -36,54 +36,56 @@ This document provides detailed information about environment variables for each | `POSTGRES_AERIE_MERLIN_DB` | Name of Merlin Postgres database. | `string` | aerie_merlin | | `POSTGRES_AERIE_SCHEDULER_DB` | Name of scheduler Postgres database. | `string` | aerie_scheduler | | `POSTGRES_HOST` | Hostname of Postgres instance. | `string` | localhost | -| `POSTGRES_PASSWORD` | Password of Postgres instance. | `string` | | +| `POSTGRES_PASSWORD` | Password of Postgres instance. | `string` | | | `POSTGRES_PORT` | Port of Postgres instance. | `number` | 5432 | -| `POSTGRES_USER` | User of Postgres instance. | `string` | | +| `POSTGRES_USER` | User of Postgres instance. | `string` | | | `RATE_LIMITER_FILES_MAX` | Max requests allowed every 15 minutes to file endpoints | `number` | 1000 | | `RATE_LIMITER_LOGIN_MAX` | Max requests allowed every 15 minutes to login endpoints | `number` | 1000 | ## Aerie Merlin -| Name | Description | Type | Default | -| -------------------- | --------------------------------------------------------- | -------- | ------------------------------- | -| `JAVA_OPTS` | Configuration for Merlin's logging level and output file | `string` | log level: warn. output: stderr | -| `MERLIN_PORT` | Port number for the Merlin server | `number` | 27183 | -| `MERLIN_LOCAL_STORE` | Local storage for Merlin in the container | `string` | /usr/src/app/merlin_file_store | -| `MERLIN_DB_SERVER` | The DB instance that Merlin will connect with | `string` | | -| `MERLIN_DB_PORT` | The DB instance port number that Merlin will connect with | `number` | 5432 | -| `MERLIN_DB_USER` | Username of the DB instance | `string` | | -| `MERLIN_DB_PASSWORD` | Password of the DB instance | `string` | | -| `MERLIN_DB` | The DB for Merlin. | `string` | aerie_merlin | +| Name | Description | Type | Default | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------- | +| `JAVA_OPTS` | Configuration for Merlin's logging level and output file | `string` | log level: warn. output: stderr | +| `MERLIN_PORT` | Port number for the Merlin server | `number` | 27183 | +| `MERLIN_LOCAL_STORE` | Local storage for Merlin in the container | `string` | /usr/src/app/merlin_file_store | +| `MERLIN_DB_SERVER` | The DB instance that Merlin will connect with | `string` | | +| `MERLIN_DB_PORT` | The DB instance port number that Merlin will connect with | `number` | 5432 | +| `MERLIN_DB_USER` | Username of the DB instance | `string` | | +| `MERLIN_DB_PASSWORD` | Password of the DB instance | `string` | | +| `MERLIN_DB` | The DB for Merlin. | `string` | aerie_merlin | +| `UNTRUE_PLAN_START` | Temporary solution to provide plan start time to models, should be set to a time that models will not fail to initialize on | `string` | | ## Aerie Merlin Worker -| Name | Description | Type | Default | -| --------------------------- | --------------------------------------------------------- | -------- | -------------------------------------------- | -| `JAVA_OPTS` | Configuration for Merlin's logging level and output file | `string` | log level: warn. output: stderr | -| `MERLIN_WORKER_LOCAL_STORE` | The local storage as for the Merlin container | `string` | /usr/src/app/merlin_file_store | -| `MERLIN_WORKER_DB_SERVER` | The DB instance that Merlin will connect with | `string` | (this must the same as the Merlin container) | -| `MERLIN_WORKER_DB_PORT` | The DB instance port number that Merlin will connect with | `number` | (this must the same as the Merlin container) | -| `MERLIN_WORKER_DB_USER` | Username of the DB instance | `string` | (this must the same as the Merlin container) | -| `MERLIN_WORKER_DB_PASSWORD` | Password of the DB instance | `string` | (this must the same as the Merlin container) | -| `MERLIN_WORKER_DB` | The DB for Merlin. | `string` | (this must the same as the Merlin container) | +| Name | Description | Type | Default | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------- | +| `JAVA_OPTS` | Configuration for Merlin's logging level and output file | `string` | log level: warn. output: stderr | +| `MERLIN_WORKER_LOCAL_STORE` | The local storage as for the Merlin container | `string` | /usr/src/app/merlin_file_store | +| `MERLIN_WORKER_DB_SERVER` | The DB instance that Merlin will connect with | `string` | (this must the same as the Merlin container) | +| `MERLIN_WORKER_DB_PORT` | The DB instance port number that Merlin will connect with | `number` | (this must the same as the Merlin container) | +| `MERLIN_WORKER_DB_USER` | Username of the DB instance | `string` | (this must the same as the Merlin container) | +| `MERLIN_WORKER_DB_PASSWORD` | Password of the DB instance | `string` | (this must the same as the Merlin container) | +| `MERLIN_WORKER_DB` | The DB for Merlin. | `string` | (this must the same as the Merlin container) | +| `UNTRUE_PLAN_START` | Temporary solution to provide plan start time to models, should be set to a time that models will not fail to initialize on | `string` | | ## Aerie Scheduler -| Name | Description | Type | Default | -| ----------------------- | --------------------------------------------------------------------- | -------- |---------------------------------------------------| -| `JAVA_OPTS` | Configuration for the scheduler's logging level and output file | `string` | log level: warn. output: stderr | -| `MERLIN_GRAPHQL_URL` | URI of the Merlin graphql interface to call | `string` | http://hasura:8080/v1/graphql | -| `MERLIN_LOCAL_STORE` | Local storage for Merlin in the container (for backdoor jar access) | `string` | /usr/src/app/merlin_file_store | -| `SCHEDULER_DB` | The DB for scheduler | `string` | aerie_scheduler | -| `SCHEDULER_DB_PASSWORD` | Password of the DB instance | `string` | | -| `SCHEDULER_DB_PORT` | The DB instance port number that scheduler will connect with | `number` | 5432 | -| `SCHEDULER_DB_SERVER` | The DB instance that scheduler will connect with | `string` | | -| `SCHEDULER_DB_USER` | Username of the DB instance | `string` | | -| `SCHEDULER_LOCAL_STORE` | Local storage for scheduler in the container | `string` | /usr/src/app/scheduler_file_store | -| `SCHEDULER_LOGGING` | Whether or not you want Javalin to log server information | `string` | true | -| `SCHEDULER_OUTPUT_MODE` | how scheduler output is sent back to aerie | `string` | UpdateInputPlanWithNewActivities | -| `SCHEDULER_PORT` | Port number for the scheduler server | `number` | 27185 | -| `SCHEDULER_RULES_JAR` | Jar file to load scheduling rules from (until user input to database) | `string` | /usr/src/app/merlin_file_store/scheduler_rules.jar | +| Name | Description | Type | Default | +| ----------------------- | --------------------------------------------------------------------- | -------- | --------------------------------------------------- | +| `JAVA_OPTS` | Configuration for the scheduler's logging level and output file | `string` | log level: warn. output: stderr | +| `MERLIN_GRAPHQL_URL` | URI of the Merlin graphql interface to call | `string` | http://hasura:8080/v1/graphql | +| `MERLIN_LOCAL_STORE` | Local storage for Merlin in the container (for backdoor jar access) | `string` | /usr/src/app/merlin_file_store | +| `SCHEDULER_DB` | The DB for scheduler | `string` | aerie_scheduler | +| `SCHEDULER_DB_PASSWORD` | Password of the DB instance | `string` | | +| `SCHEDULER_DB_PORT` | The DB instance port number that scheduler will connect with | `number` | 5432 | +| `SCHEDULER_DB_SERVER` | The DB instance that scheduler will connect with | `string` | | +| `SCHEDULER_DB_USER` | Username of the DB instance | `string` | | +| `SCHEDULER_LOCAL_STORE` | Local storage for scheduler in the container | `string` | /usr/src/app/scheduler_file_store | +| `SCHEDULER_LOGGING` | Whether or not you want Javalin to log server information | `string` | true | +| `SCHEDULER_OUTPUT_MODE` | how scheduler output is sent back to aerie | `string` | UpdateInputPlanWithNewActivities | +| `SCHEDULER_PORT` | Port number for the scheduler server | `number` | 27185 | +| `SCHEDULER_RULES_JAR` | Jar file to load scheduling rules from (until user input to database) | `string` | /usr/src/app/merlin_file_store/scheduler_rules.jar | ## Aerie UI diff --git a/deployment/README.md b/deployment/README.md index cb14a85924..35a48ca42b 100755 --- a/deployment/README.md +++ b/deployment/README.md @@ -17,19 +17,23 @@ Before you can deploy Aerie, you must install and configure the following produc ## Setting the Environment Variables -Each container has environment variables that can be used to fine-tune your deployment. See the [environment variable documentation](./Environment.md) for the complete set of variables. See the example [docker-compose.yml](./docker-compose.yml) file for examples on how to set the environment variables. - -Inside the [.env](./.env) there is a collection of variables that **must** be set in order to deploy Aerie using the default [init-aerie.sh](./postgres-init-db/init-aerie.sh) and [docker-compose.yml](./docker-compose.yml). They are, as follows: - -| Name | Description | -|-----------------------|------------------------------------------------------------------------------| -| REPOSITORY_DOCKER_URL | The URL used to fetch images of Aerie packages. | -| DOCKER_TAG | The version of the Aerie images to fetch. | -| AERIE_USERNAME | The username used for Aerie services when they access the Postgres database. | -| AERIE_PASSWORD | The password used for Aerie services when they access the Postgres database. | -| POSTGRES_DB | The name of the Postgres database. | -| POSTGRES_USER | The username of the superuser for the Postgres database. | -| POSTGRES_PASSWORD | The password of the superuser for the Postgres database. | +Each container has environment variables that can be used to fine-tune your deployment. +See the [environment variable documentation](./Environment.md) for the complete set of variables. +See the example [docker-compose.yml](./docker-compose.yml) file for examples on how to set the environment variables. + +Inside the [.env](./.env) there is a collection of variables that **must** be set in order to deploy Aerie using the default [init-aerie.sh](./postgres-init-db/init-aerie.sh) and [docker-compose.yml](./docker-compose.yml). +They are, as follows: + +| Name | Description | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `REPOSITORY_DOCKER_URL` | The URL used to fetch images of Aerie packages. | +| `DOCKER_TAG` | The version of the Aerie images to fetch. | +| `AERIE_USERNAME` | The username used for Aerie services when they access the Postgres database. | +| `AERIE_PASSWORD` | The password used for Aerie services when they access the Postgres database. | +| `POSTGRES_DB` | The name of the Postgres database. | +| `POSTGRES_USER` | The username of the superuser for the Postgres database. | +| `POSTGRES_PASSWORD` | The password of the superuser for the Postgres database. | +| `UNTRUE_PLAN_START` | Temporary solution to provide plan start time to models, should be set to a time that models will not fail to initialize on. | ### **At minimum, you _must_ assign values to the environment variables already present in [.env](./.env) in order to deploy Aerie**. diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 5f5d24358b..615c7fed19 100755 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -53,6 +53,7 @@ services: JAVA_OPTS: > -Dorg.slf4j.simpleLogger.defaultLogLevel=WARN -Dorg.slf4j.simpleLogger.logFile=System.err + UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" image: "${REPOSITORY_DOCKER_URL}/aerie-merlin:${DOCKER_TAG}" ports: ["27183:27183"] restart: always @@ -72,6 +73,7 @@ services: -Dorg.slf4j.simpleLogger.defaultLogLevel=INFO -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=WARN -Dorg.slf4j.simpleLogger.logFile=System.err + UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" image: "${REPOSITORY_DOCKER_URL}/aerie-merlin-worker:${DOCKER_TAG}" restart: always volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 36a3997211..6c9612e6c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,6 +64,7 @@ services: -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err + UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" image: aerie_merlin ports: ["27183:27183", "5005:5005"] restart: always @@ -131,6 +132,7 @@ services: -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err + UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" ports: ["5007:5005", "27187:8080"] restart: always volumes: @@ -153,6 +155,7 @@ services: -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err + UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" ports: ["5008:5005", "27188:8080"] restart: always volumes: diff --git a/e2e-tests/docker-compose-test.yml b/e2e-tests/docker-compose-test.yml index 5789e94a90..ccc3b17ea1 100644 --- a/e2e-tests/docker-compose-test.yml +++ b/e2e-tests/docker-compose-test.yml @@ -65,6 +65,7 @@ services: -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err + UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" image: aerie_merlin ports: ['27183:27183', '5005:5005'] restart: always @@ -132,6 +133,7 @@ services: -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err + UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" ports: ['5007:5005', '27187:8080'] restart: always volumes: @@ -154,6 +156,7 @@ services: -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err + UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" ports: ['5008:5005', '27188:8080'] restart: always volumes: diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java index 9e4c934ce3..540d0860b0 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java @@ -40,15 +40,19 @@ import java.io.IOException; import java.nio.file.Path; +import java.time.Instant; public final class AerieAppDriver { - public static void main(final String[] args) throws InterruptedException { + public static void main(final String[] args) { // Fetch application configuration properties. final var configuration = loadConfiguration(); final var stores = loadStores(configuration); - final var missionModelController = new LocalMissionModelService(configuration.merlinFileStore(), stores.missionModels()); + final var missionModelController = new LocalMissionModelService( + configuration.merlinFileStore(), + stores.missionModels(), + configuration.untruePlanStart()); final var typescriptCodeGenerationService = new TypescriptCodeGenerationServiceAdapter(missionModelController); @@ -138,7 +142,7 @@ private static Stores loadStores(final AppConfiguration config) { } } - private static final String getEnv(final String key, final String fallback){ + private static String getEnv(final String key, final String fallback) { final var env = System.getenv(key); return env == null ? fallback : env; } @@ -146,14 +150,15 @@ private static final String getEnv(final String key, final String fallback){ private static AppConfiguration loadConfiguration() { final var logger = LoggerFactory.getLogger(AerieAppDriver.class); return new AppConfiguration( - Integer.parseInt(getEnv("MERLIN_PORT","27183")), + Integer.parseInt(getEnv("MERLIN_PORT", "27183")), logger.isDebugEnabled(), - Path.of(getEnv("MERLIN_LOCAL_STORE","/usr/src/app/merlin_file_store")), - new PostgresStore(getEnv("MERLIN_DB_SERVER","postgres"), - getEnv("MERLIN_DB_USER",""), - Integer.parseInt(getEnv("MERLIN_DB_PORT","5432")), - getEnv("MERLIN_DB_PASSWORD",""), - getEnv("MERLIN_DB","aerie_merlin")) + Path.of(getEnv("MERLIN_LOCAL_STORE", "/usr/src/app/merlin_file_store")), + new PostgresStore(getEnv("MERLIN_DB_SERVER", "postgres"), + getEnv("MERLIN_DB_USER", ""), + Integer.parseInt(getEnv("MERLIN_DB_PORT", "5432")), + getEnv("MERLIN_DB_PASSWORD", ""), + getEnv("MERLIN_DB", "aerie_merlin")), + Instant.parse(getEnv("UNTRUE_PLAN_START", "")) ); } } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/config/AppConfiguration.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/config/AppConfiguration.java index ff8a2f34b1..63036d9b94 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/config/AppConfiguration.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/config/AppConfiguration.java @@ -1,16 +1,19 @@ package gov.nasa.jpl.aerie.merlin.server.config; import java.nio.file.Path; +import java.time.Instant; import java.util.Objects; public record AppConfiguration ( int httpPort, boolean enableJavalinDevLogging, Path merlinFileStore, - Store store + Store store, + Instant untruePlanStart ) { public AppConfiguration { Objects.requireNonNull(merlinFileStore); Objects.requireNonNull(store); + Objects.requireNonNull(untruePlanStart); } } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/config/Store.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/config/Store.java index 97d8bb6ad2..9e32802598 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/config/Store.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/config/Store.java @@ -1,5 +1,5 @@ package gov.nasa.jpl.aerie.merlin.server.config; -public /*sealed*/ interface Store - /*permits PostgresStore*/ +public sealed interface Store + permits PostgresStore, InMemoryStore {} diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index b28566b32f..c0c4009f25 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -34,13 +34,16 @@ public final class LocalMissionModelService implements MissionModelService { private final Path missionModelDataPath; private final MissionModelRepository missionModelRepository; + private final Instant untruePlanStart; public LocalMissionModelService( final Path missionModelDataPath, - final MissionModelRepository missionModelRepository + final MissionModelRepository missionModelRepository, + final Instant untruePlanStart ) { this.missionModelDataPath = missionModelDataPath; this.missionModelRepository = missionModelRepository; + this.untruePlanStart = untruePlanStart; } @Override @@ -247,7 +250,7 @@ private MissionModelFacade.Unconfigured loadUnconfiguredMissionModel(final St private MissionModelFacade loadConfiguredMissionModel(final String missionModelId) throws NoSuchMissionModelException, MissionModelLoadException { - return loadConfiguredMissionModel(missionModelId, Instant.EPOCH, SerializedValue.of(Map.of())); + return loadConfiguredMissionModel(missionModelId, untruePlanStart, SerializedValue.of(Map.of())); } /** diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/DevAppDriver.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/DevAppDriver.java index 43932b9794..b2471cd9e7 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/DevAppDriver.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/DevAppDriver.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.time.Instant; public final class DevAppDriver { private static final int HTTP_PORT = 27183; @@ -24,7 +25,7 @@ public final class DevAppDriver { public static void main(final String[] args) { // Assemble the core non-web object graph. final var fixtures = new Fixtures(); - final var missionModelController = new LocalMissionModelService(Path.of("/dev/null"), new InMemoryMissionModelRepository()); + final var missionModelController = new LocalMissionModelService(Path.of("/dev/null"), new InMemoryMissionModelRepository(), Instant.EPOCH); final var planController = new LocalPlanService(fixtures.planRepository); final var typescriptCodeGenerationService = new TypescriptCodeGenerationServiceAdapter(missionModelController); diff --git a/merlin-worker/src/main/java/gov/nasa/jpl/aerie/merlin/worker/MerlinWorkerAppDriver.java b/merlin-worker/src/main/java/gov/nasa/jpl/aerie/merlin/worker/MerlinWorkerAppDriver.java index ea3e19f27a..02f8c23e24 100644 --- a/merlin-worker/src/main/java/gov/nasa/jpl/aerie/merlin/worker/MerlinWorkerAppDriver.java +++ b/merlin-worker/src/main/java/gov/nasa/jpl/aerie/merlin/worker/MerlinWorkerAppDriver.java @@ -19,6 +19,7 @@ import io.javalin.Javalin; import java.nio.file.Path; +import java.time.Instant; import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -49,7 +50,10 @@ public static void main(String[] args) throws Exception { new PostgresMissionModelRepository(hikariDataSource), new PostgresResultsCellRepository(hikariDataSource)); - final var missionModelController = new LocalMissionModelService(configuration.merlinFileStore(), stores.missionModels()); + final var missionModelController = new LocalMissionModelService( + configuration.merlinFileStore(), + stores.missionModels(), + configuration.untruePlanStart()); final var planController = new LocalPlanService(stores.plans()); final var simulationAgent = new SynchronousSimulationAgent(planController, missionModelController); @@ -91,12 +95,13 @@ private static String getEnv(final String key, final String fallback){ private static WorkerAppConfiguration loadConfiguration() { return new WorkerAppConfiguration( - Path.of(getEnv("MERLIN_WORKER_LOCAL_STORE","/usr/src/app/merlin_file_store")), - new PostgresStore(getEnv("MERLIN_WORKER_DB_SERVER","postgres"), - getEnv("MERLIN_WORKER_DB_USER",""), - Integer.parseInt(getEnv("MERLIN_WORKER_DB_PORT","5432")), - getEnv("MERLIN_WORKER_DB_PASSWORD",""), - getEnv("MERLIN_WORKER_DB","aerie_merlin")) + Path.of(getEnv("MERLIN_WORKER_LOCAL_STORE", "/usr/src/app/merlin_file_store")), + new PostgresStore(getEnv("MERLIN_WORKER_DB_SERVER", "postgres"), + getEnv("MERLIN_WORKER_DB_USER", ""), + Integer.parseInt(getEnv("MERLIN_WORKER_DB_PORT", "5432")), + getEnv("MERLIN_WORKER_DB_PASSWORD", ""), + getEnv("MERLIN_WORKER_DB", "aerie_merlin")), + Instant.parse(getEnv("UNTRUE_PLAN_START", "")) ); } } diff --git a/merlin-worker/src/main/java/gov/nasa/jpl/aerie/merlin/worker/WorkerAppConfiguration.java b/merlin-worker/src/main/java/gov/nasa/jpl/aerie/merlin/worker/WorkerAppConfiguration.java index b55e1ed2ef..2025a4feee 100644 --- a/merlin-worker/src/main/java/gov/nasa/jpl/aerie/merlin/worker/WorkerAppConfiguration.java +++ b/merlin-worker/src/main/java/gov/nasa/jpl/aerie/merlin/worker/WorkerAppConfiguration.java @@ -3,14 +3,17 @@ import gov.nasa.jpl.aerie.merlin.server.config.Store; import java.nio.file.Path; +import java.time.Instant; import java.util.Objects; public record WorkerAppConfiguration( Path merlinFileStore, - Store store + Store store, + Instant untruePlanStart ) { public WorkerAppConfiguration { Objects.requireNonNull(merlinFileStore); Objects.requireNonNull(store); + Objects.requireNonNull(untruePlanStart); } }