A deterministic simulation testing framework.
OpenDST explores the state-space of a system under test (SUT) to find bugs. It relies on a pseudo-random number generator to create test scenarios at runtime. A test is made of three things:
- Invariants -- properties the system must maintain in every situation. OpenDST will try to defeat them.
- Test workload -- creates traffic/actions against the SUT (e.g. HTTP requests).
- Fault injection -- conditions injected by OpenDST to break invariants (e.g. network partitions, thread interleaving).
A test is made of one or more applications (the SUT) and one or more test workloads. The
opendst-maven-plugin packages everything into a single self-contained JAR:
java -jar simulation-opendst.jarThe JAR is self-sufficient -- it contains the SUT, all its dependencies, and the entire OpenDST machinery needed to run the simulation.
A deployment.yaml defines the services to simulate:
services:
server:
class: com.example.App$Server
ip: 10.0.0.1
args: ["8080"]
client:
class: com.example.App$Client
ip: 10.0.0.2<plugin>
<groupId>com.pingidentity.opendst</groupId>
<artifactId>opendst-maven-plugin</artifactId>
<version>${latest.version}</version>
<configuration>
<descriptor>${project.basedir}/deployment.yaml</descriptor>
</configuration>
</plugin>This produces appname-opendst.jar.
The Maven plugin produces a JAR with this layout:
simulation-opendst.jar
Bootstrap.class # Main-Class entry point (no dependencies)
build-config.json # Baked CLI/JVM settings from the Maven build
deployment.yaml # Enriched deployment descriptor
system/
opendst-agent.jar # Shaded Java agent (ByteBuddy, jackson-jr)
opendst-runner.jar # Orchestrator + child entry point
opendst-sdk.jar # SDK API (Assert, Signals, TraceAuditor)
opendst-patch.jar # Non-final VirtualThread + SimulatorThread
jackson-*.jar # Jackson databind + YAML
picocli.jar # CLI framework
apps/
<service-a>/WEB-INF/ # Instrumented SUT classes + dependencies
<service-b>/WEB-INF/ # (one directory per service)
Running the JAR starts a parent process (the runner) which repeatedly spawns short-lived child processes (the simulations). Each child runs one simulation iteration with a different execution plan; the parent collects results and steers future runs.
Parent JVM (opendst-runner) Child JVM (opendst-agent)
+---------------------------------+ +---------------------------------+
| Bootstrap | | -javaagent:opendst-agent.jar |
| extracts JAR to disk | stdin | |
| loads system/*.jar | ---------> | OpenDSTExecutor |
| invokes BuildRunner | (JSON | parses deployment.yaml |
| | plan) | creates classloader per |
| BuildRunner (CLI via picocli) | | service from apps/ |
| reads deployment descriptor | stdout | calls Simulator.startNode() |
| manages Orchestrator | <--------- | |
| | (JSON | Simulator |
| Orchestrator | signals) | deterministic scheduler |
| generates execution plans | | intercepted time/net/threads |
| coverage-guided exploration | stderr | assertion evaluation |
| | <--------- | state hashing |
| TestExecutor | (human | |
| spawns child JVM | logs) | SimulatorAgent |
| feeds plan via stdin | | bytecode rewriting |
| parses signals from stdout | | JDK method interception |
| | | |
| ReportGenerator | | |
| aggregates results | | |
| writes report.json | | |
+---------------------------------+ +---------------------------------+
Communication protocol:
- stdin (parent to child): JSON execution plan -- seed values, segment boundaries, fault config.
- stdout (child to parent): structured JSON signals -- assertion verdicts, guidance data, lifecycle events (started, stopped, non-determinism detected).
- stderr (child to parent): human-readable log lines for console display.
Bootstrapextracts the JAR contents to a temporary directory, builds aURLClassLoaderfromsystem/*.jar, and reflectively invokesBuildRunner.main().BuildRunnerparses CLI arguments, scans assertion bytecode in the SUT, creates anOrchestratorand aTestExecutor.- For each iteration, the
Orchestratorproduces aPlan(random seed + segments + fault config).TestExecutorspawns a child JVM, writes the plan to its stdin, and collects signals from its stdout until the child exits. - After all iterations,
ReportGeneratorwritesreport.jsonwith per-assertion pass/fail counts and shortest-path examples.
- The child JVM starts with
-javaagent:opendst-agent.jarand--patch-module java.base=opendst-patch.jar. The agent rewrites JDK classes (threads, sockets, time,System.exit, ...) to route through deterministic interceptors. The patch module injectsSimulatorThread(aVirtualThreadsubclass) intojava.base, enablingThreadsubclasses in the SUT to run as virtual threads under simulation. Experimental — this may be reverted if it causes more issues than it solves. OpenDSTExecutor.main()reads the execution plan from stdin, parsesdeployment.yaml, and for each service creates aURLClassLoaderpointing at itsapps/<service>/WEB-INF/directory (parented to the platform classloader for isolation).Simulator.runSimulation()starts the deterministic scheduler. Each service'smain()method is invoked on a virtual thread. All threads run cooperatively on a single carrier thread, scheduled by a priority queue driven by the plan's random seed.- The simulation runs until the plan's iteration budget is exhausted or all threads are blocked.
A final
stoppedsignal with the run's state hash is emitted, and the process exits.
opendst-sdk Compile-only API for SUT code (Assert, Signals, TraceAuditor)
^
|
opendst-common Shared types used by both the agent and the maven plugin
^ (deployment descriptors, assertion metadata, build config,
| call-site transforms). Shaded into dependent modules.
|
opendst-agent Shaded Java agent -- simulation engine, bytecode rewriting,
^ deterministic interceptors. Minimal dependencies (jackson-jr,
| ByteBuddy). Runs in the child JVM.
|
opendst-runner Orchestration + child entry point. Runs in both JVMs:
^ - Parent side: Bootstrap, BuildRunner, Orchestrator,
| TestExecutor, ReportGenerator
| - Child side: OpenDSTExecutor (parses deployment.yaml,
| starts classloader-isolated nodes)
| Heavier dependencies (Jackson databind + YAML, picocli).
|
opendst-maven-plugin Build-time only. Instruments SUT bytecode, resolves
dependencies, packages the self-contained JAR.
Also generates opendst-patch.jar (non-final VirtualThread
+ SimulatorThread compiled from source via javac).
Why is OpenDSTExecutor in opendst-runner and not in opendst-agent? It needs Jackson
databind + YAML to parse deployment.yaml. The agent is a shaded JAR with only jackson-jr
(lightweight). Adding full Jackson to the agent would bloat it and risk classpath conflicts with
instrumented application code.