Repository: nasa-ammos/aerie
Issue: Negative delay moves simulation time backward

Labeling this as a bug - but maybe it's a feature...? 😅

## Description
Type-wise, everything is fine - delay is defined to work on Duration, and that includes negative durations. However, I think this violates other invariants of the system - such as the invariant that the start offsets of resource profile segments are monotonically increasing.

`TaskStatus.Delayed` is perfectly happy to accept a negative duration:
https://github.com/NASA-AMMOS/aerie/blob/ad2e9c4130a09afea401daf15b3d0dbf7ccd801b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java#L10

The SimulationEngine is perfectly happy to schedule that task to be resumed in the past:
https://github.com/NASA-AMMOS/aerie/blob/ad2e9c4130a09afea401daf15b3d0dbf7ccd801b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java#L236-L238

This of course puts that task at the front of the job queue, so the next call to `extractNextJobs` contains this task, as well as the time (in the past) at which this task is scheduled.

The "delta" computation in the simulation driver will compute a negative delta and add it to the timeline: https://github.com/NASA-AMMOS/aerie/blob/ad2e9c4130a09afea401daf15b3d0dbf7ccd801b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java#L73-L75

Interestingly - the timeline keeps a perfect record of all this - if time travel were somehow semantically acceptable, the timeline would capture it faithfully (particularly - it faithfully captures the direction of causality even when time steps backwards).

## Possible solutions
1. We can throw an error and tank simulation as soon as a negative delay is requested
2. We can clamp negative durations to ZERO - and perhaps issue a warning to alert the client that we've done this
3. (Far-fetched) We can try to give a reasonable semantics to time travel


Changed files:
merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java

diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
index b45646aa3..de252a9c7 100644
--- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
+++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
@@ -27,7 +27,6 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.commons.lang3.tuple.Triple;
 
-import java.lang.reflect.InvocationTargetException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -100,6 +99,8 @@ public final class SimulationEngine implements AutoCloseable {
 
   /** Schedule a new task to be performed at the given time. */
   public <Return> TaskId scheduleTask(final Duration startTime, final TaskFactory<Return> state) {
+    if (startTime.isNegative()) throw new IllegalArgumentException("Cannot schedule a task before the start time of the simulation");
+
     final var task = TaskId.generate();
     this.tasks.put(task, new ExecutionState.InProgress<>(startTime, state.create(this.executor)));
     this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(startTime));
@@ -234,6 +235,8 @@ public final class SimulationEngine implements AutoCloseable {
       this.tasks.put(task, progress.completedAt(currentTime, children));
       this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(currentTime));
     } else if (status instanceof TaskStatus.Delayed<Return> s) {
+      if (s.delay().isNegative()) throw new IllegalArgumentException("Cannot schedule a task in the past");
+
       this.tasks.put(task, progress.continueWith(s.continuation()));
       this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(currentTime.plus(s.delay())));
     } else if (status instanceof TaskStatus.CallingTask<Return> s) {