Runner
Entry point for asynchronous simulation execution.
Runner wraps ksl.simulation.Model.simulate's synchronous replication loop in a coroutine, exposes lifecycle progress as a RunEvent flow, and ensures that RunAttachmentIfc instances are attached before the experiment begins and detached after it ends regardless of outcome.
Basic usage
val model = Model("MM1")
// ... configure model ...
model.numberOfReplications = 30
model.lengthOfReplication = 20_000.0
val runner = Runner()
val handle = runner.submit(RunRequest.SingleRun(model))
launch { handle.events.collect { println(it) } }
when (val r = handle.result.await()) {
is RunResult.Completed -> println("Done: ${r.summary}")
is RunResult.Cancelled -> println("Cancelled: ${r.reason}")
is RunResult.Failed -> println("Error: ${r.error}")
}How the replication loop works
Rather than calling model.simulate() (which blocks the calling thread for the entire experiment), Runner drives the replication loop manually using the public API:
model.initializeReplications() // sets beginExecutionTime, triggers BEFORE_EXPERIMENT
while (hasNextReplication && !isDone):
ensureActive() // cooperative cancellation check
emit ReplicationStarted
model.runNextReplication() // one full replication, synchronous
emit ReplicationEnded
model.endSimulation() // triggers endIterations → AFTER_EXPERIMENT
emit RunCompletedThis approach lets Runner:
emit RunEvent.ReplicationStarted / RunEvent.ReplicationEnded between steps
check for cooperative cancellation between replications
honour
model.autoCSVReports(mirrored fromsimulate())
RunAttachmentIfc instances attached via RunRequest.SingleRun.attachments call model.attachModelElementObserver(...) in their RunAttachmentIfc.onAttach implementations. Those observers then fire synchronously on the simulation thread at fine-grained lifecycle boundaries inside each runNextReplication() call (beforeReplication, warmUp, afterReplication, etc.), giving attachments full access to within-replication lifecycle events.
Cancellation
RunHandle.cancel cancels the coroutine's Job. Cancellation is cooperative between replications: the replication currently executing will complete; the loop then checks ensureActive() before starting the next replication, finds the job cancelled, and emits RunEvent.RunCancelled.
stopReplication() vs endSimulation()
These two methods operate at different levels of the KSL execution hierarchy and must not be confused:
ModelElement.stopReplication()— signals the Executive's inner event loop to halt the current replication. Call this from within model code (event handlers, process steps) when you want to end a replication early.Model.endSimulation()— signals the outer iterative-process loop to stop scheduling further replications. This flag is only checked between replications, never during one. Calling it from inside an event handler while the Executive is running has no effect on the current replication and will cause an infinite-horizon model to hang indefinitely.
Runner calls endSimulation() in two places where it is safe: after the normal replication loop exits (no replication is running) and in the cancellation handler (cancellation is cooperative and only fires between replications at ensureActive()).
Thread safety
All simulation work runs on a single thread within SimulationDispatcher.default. RunEvent emissions are routed through RunLifecycle, which owns terminal event/result completion and suppresses progress after a terminal outcome.