SimulationWorkflowExecutionBackend.java

package org.hammer.audio.workflow.execution;

import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.hammer.audio.workflow.execution.WorkflowRunModels.Control;
import org.hammer.audio.workflow.execution.WorkflowRunModels.Input;
import org.hammer.audio.workflow.execution.WorkflowRunModels.Mode;
import org.hammer.audio.workflow.execution.WorkflowRunModels.Result;
import org.hammer.audio.workflow.execution.WorkflowRunModels.Violation;

/** Dry-run backend that exercises lifecycle and plan ordering without audio computation. */
public final class SimulationWorkflowExecutionBackend
    implements WorkflowRunModels.ExecutionBackend {

  private final Clock clock;

  /** Creates a simulation backend using UTC wall-clock time. */
  public SimulationWorkflowExecutionBackend() {
    this(Clock.systemUTC());
  }

  SimulationWorkflowExecutionBackend(Clock clock) {
    this.clock = Objects.requireNonNull(clock, "clock");
  }

  @Override
  public Mode mode() {
    return Mode.SIMULATION;
  }

  @Override
  public List<Violation> validate(Input input) {
    Objects.requireNonNull(input, "input");
    return List.of();
  }

  @Override
  public Result execute(Input input, Control control) {
    Objects.requireNonNull(input, "input");
    Objects.requireNonNull(control, "control");
    Instant startedAt = clock.instant();
    ExecutionContext context =
        new ExecutionContext(input.runId() + ":execution", input.plan(), startedAt);
    List<String> nodeIds = input.plan().orderedNodeIds();
    for (int index = 0; index < nodeIds.size(); index++) {
      String nodeId = nodeIds.get(index);
      if (control.cancellationRequested()) {
        cancelRemaining(context, nodeIds, index);
        break;
      }
      context.updateNodeStatus(nodeId, ExecutionStatus.RUNNING);
      context.updateNodeStatus(nodeId, ExecutionStatus.COMPLETED);
      int progress = nodeIds.isEmpty() ? 100 : Math.multiplyExact(index + 1, 100) / nodeIds.size();
      control.progress(progress, "Simulated " + nodeId);
    }
    if (nodeIds.isEmpty()) {
      control.progress(100, "Simulation completed");
    }
    ExecutionResult executionResult = context.toResult(clock.instant());
    ReproducibilityBundle bundle =
        new ReproducibilityBundle(input.snapshot(), executionResult, input.commitId(), null);
    return new Result(bundle, Map.of("backendMode", Mode.SIMULATION.name()));
  }

  private static void cancelRemaining(
      ExecutionContext context, List<String> nodeIds, int firstRemainingIndex) {
    for (int index = firstRemainingIndex; index < nodeIds.size(); index++) {
      context.updateNodeStatus(nodeIds.get(index), ExecutionStatus.CANCELLED);
    }
  }
}