Deterministic simulation testing (FoundationDB, TigerBeetle VOPR, Antithesis)
Document status:
reviewed. Canonical source.
Summary
Deterministic simulation testing (DST) runs an entire system inside one single-threaded process in which time, scheduling, network, disk and randomness are simulated from one seeded pseudo-random number generator. A failure is reproduced by rerunning the same build with the same seed. FoundationDB introduced the practice with the Flow actor language and the Sim2 simulator; TigerBeetle’s VOPR adds swarm-randomised fault distributions, hash-chained state checkers and a liveness mode; Antithesis moves determinism into a hypervisor so unmodified binaries can be simulated. Swarm testing (Groce et al., ISSTA 2012) supplies the evidence that randomising which features each run enables improves defect discovery.
NUIF is not a distributed system, but its trial-and-error loop has the same nondeterminism sources: operation ordering, floating-point layout, font and image loading, adapter I/O and renderer scheduling. The DST recipe transfers as a design constraint on the headless engine: every source of nondeterminism sits behind an injectable interface and every run is replayable from (build, seed, fixture).
Evidence
- FoundationDB simulation is “a deterministic simulation of an entire FoundationDB cluster within a single-threaded process” and determinism “allows perfect repeatability of a simulated run”. https://apple.github.io/foundationdb/testing.html, §Simulation (mirrors
documentation/sphinx/source/testing.rst), retrieved 2026-08-29. - Simulated runs have roughly a 10:1 real-to-simulated time ratio and the project runs tens of thousands of simulations nightly. Same page, §Simulation.
- The simulated failure model includes network, machine and datacenter failures, reboots, degraded performance and “swizzle-clogging” (stopping connections in random sequence, then unclogging). Same page, §Simulation.
- Flow is an actor-based extension of C++ whose output feeds “our simulation tool, which conducts deterministic simulations of the entire system”. https://apple.github.io/foundationdb/flow.html, retrieved 2026-08-29. On the
mainbranchflow/README.mdnow describes cooperative scheduling over standard C++ coroutines. - The simulator is
class Sim2 final : public ISimulator, public INetworkConnections;runLoop()pops aTaskQueue<PromiseTask>and advances virtual time withdeterministicRandom()->random01();delay()schedules timers on virtual time and buggifies extra delay with probability 0.25.fdbrpc/sim2.cpp,mainbranch (lines ~1064–1075 and ~1379), retrieved 2026-08-29; therelease-7.1filefdbrpc/sim2.actor.cppnotes that time is modified only from the main thread. - Network and disk are simulated by
SimClogging,Sim2ConnandSimpleFile, with latency, disconnects and open delays drawn fromdeterministicRandom(). Same file, lines ~277–402 and ~654. - BUGGIFY sections activate with probability 0.25 and fire with probability 0.25;
buggify()returns true only if buggify is enabled for the file/line anddeterministicRandom()->random01() < probability.flow/include/flow/Buggify.h,mainbranch, lines 52–53, 92–101, retrieved 2026-08-29. fdbserveraccepts-r simulation,-f TESTFILE,-s SEED(“Random seed.”),-b [on,off](buggify, default off),-fi [on,off]and-R/--restarting.fdbserver/fdbserver.cpp,mainbranch, usage text lines ~609–630, retrieved 2026-08-29. The wiki page “How to reproduce a restart test failure” showsfdbserver -r simulation -f <test> --seed 523887594 --buggify on.- The 2014 Strange Loop abstract states that disks, network links and machines are “replaced in testing with software” so that “the exact same series of events can be replayed”. https://www.thestrangeloop.com/2014/testing-distributed-systems-w-slash-deterministic-simulation.html, retrieved 2026-08-29. Talk video https://www.youtube.com/watch?v=4fFDFbi3toc (no transcript retrievable); secondary notes at https://alex-ii.github.io/notes/2018/04/29/distributed_systems_with_deterministic_simulation.html record the interface swap
INetwork -> SimNetwork,IAsyncFile -> SimFileand the single-thread requirement. - Wilson (Antithesis blog, 2024-02-13) states that a “fully-deterministic event-based network simulation” was written before the database, run as a single-threaded process with one RNG and rerun “with the same random seed”. https://antithesis.com/blog/is_something_bugging_you/, retrieved 2026-08-29.
- Antithesis distinguishes FoundationDB-style DST, where “all nondeterministic components are pluggable”, from running unmodified software inside a deterministic hypervisor; the controlled sources are clocks, thread interleaving and system randomness. https://antithesis.com/docs/resources/deterministic_simulation_testing/, retrieved 2026-08-29.
- The Antithesis hypervisor runs each instance on one physical core, virtualises time and routes I/O through a VMCALL channel; reproducibility enables time-travel debugging. https://antithesis.com/blog/deterministic_hypervisor/ (2024-03-20), retrieved 2026-08-29.
- A “Sometimes” assertion asserts that a state is reached in at least one run; a never-hit sometimes assertion indicates an unreachable state or weak testing. https://antithesis.com/docs/best_practices/sometimes_assertions/, retrieved 2026-08-29.
- TIGER_STYLE requires an average of at least two assertions per function, pair assertions on different code paths, and assertions of both positive and negative space; it states that assertions “downgrade catastrophic correctness bugs into liveness bugs” and are “a force multiplier for discovering bugs by fuzzing”.
docs/TIGER_STYLE.md,mainbranch, §Safety (lines ~105–150), retrieved 2026-08-29. The phrase “assertions as oracles” does not appear in the document. - VOPR uses a random seed to tune fault-injection parameters; “the seed and Git commit hash can be used to replay back the exact simulation”; storage checkers verify data files byte-for-byte across caught-up replicas.
docs/internals/vopr.md,mainbranch, lines ~9–39 and §Assertions and Checkers, retrieved 2026-08-29. - Replay command:
./zig/zig build vopr -- 123“produces a fully deterministic, reproducible outcome”.docs/internals/HACKING.md,mainbranch, §Simulation (lines ~48–60), retrieved 2026-08-29. src/vopr.zig(main, 1805 lines): default seedstd.crypto.random.int(u64),var prng = stdx.PRNG.from_seed(seed),options_swarm(&prng)randomises replica/client counts, packet loss, partition mode, storage fault probabilities and crash probabilities; failure message “you can reproduce this failure with seed={}”; safety mode thentransition_to_liveness_mode(core)withfatal(.liveness, "no state convergence: ...")on timeout. Lines ~83–84, 127–157, 263–265, 349–350, 374ff, 802–808, 888, retrieved 2026-08-29.- Testing doubles:
src/testing/packet_simulator.zig(delay, loss, replay, partition modesnone,uniform_size,uniform_partition,isolate_single, clogging);src/testing/storage.zig(“In-memory storage, with simulated faults and latency”, read/write fault and misdirect probabilities,ClusterFaultAtlasguaranteeing one valid copy);src/testing/time.zig(TimeSimwith tick-based monotonic and drifting realtime clocks);src/testing/cluster/state_checker.zig(hash-chain assertions such asassert(header_b.?.parent == checksum_a)).mainbranch, retrieved 2026-08-29. - Liveness mode: pick a core quorum, heal its partitions, freeze non-core faults, require convergence within a timeout. https://tigerbeetle.com/blog/2023-07-06-simulation-testing-for-liveness/, retrieved 2026-08-29. VOPR’s default mode swarm-randomises the fault distributions themselves. https://tigerbeetle.com/blog/2025-11-28-tale-of-four-fuzzers/ and https://tigerbeetle.com/blog/2025-04-23-swarm-testing-data-structures/, retrieved 2026-08-29.
- Swarm testing: a “swarm” of random configurations, “each of which omits some features”, found 42% more distinct compiler crashes in a week (104 vs 73 for the default Csmith configuration); features can suppress interesting behaviour and compete for space in a test. Groce, Zhang, Eide, Chen, Regehr, ISSTA 2012, DOI 10.1145/2338965.2336763, Abstract and §1 (https://users.cs.utah.edu/~regehr/papers/swarm12.pdf, retrieved 2026-08-29).
- Rust equivalents: turmoil runs multiple hosts “within a single thread” with a seeded RNG and injects latency, drops, partitions and torn writes (https://github.com/tokio-rs/turmoil README,
main); madsim requires “All I/O-related interfaces must be mocked”, providesRuntime::with_seed,MADSIM_TEST_SEEDandMADSIM_TEST_CHECK_DETERMINISM(https://github.com/madsim-rs/madsim README, docs.rs 0.2.34). Retrieved 2026-08-29.
Mechanism
Recipe, with attribution:
- Single-threaded scheduler over virtual time. All concurrency is cooperative; a task queue ordered by virtual timestamp is drained in one thread (FoundationDB
Sim2::runLoop; TigerBeetleTimeSim.tick(); turmoil). - One seeded PRNG. Every random choice, including simulated latency, fault firing and workload generation, is drawn from a generator initialised from the CLI seed (
deterministicRandom()with-s SEED;stdx.PRNG.from_seed(seed)). - Nondeterminism behind injectable interfaces. Network, disk, clock and randomness are traits with a production and a simulated implementation (
ISimulator,INetworkConnections,IAsyncFile;packet_simulator.zig,storage.zig,time.zig; madsim mocks). Antithesis relocates this boundary to the hypervisor. - Fault injection at two levels: environment faults (partition, loss, crash, misdirected write, clock drift) and in-code probabilistic hooks (
BUGGIFY, 0.25 × 0.25). - Replay by
(commit, seed). The failure report prints the seed; the same binary and seed reproduce the run (fdbserver -r simulation -s,zig build vopr -- <seed>). - Oracles are invariants, not expected outputs: dense assertions (TIGER_STYLE), state checkers with hash chaining, byte-identical storage across replicas, convergence within a liveness timeout, and reachability (“sometimes”) assertions.
- Swarm-randomised configurations: each seed also selects which features and fault classes are enabled and their probabilities (Groce 2012; VOPR
options_swarm). - Volume: many short simulated runs per night, with time compression relative to wall-clock.
run(seed, build):
prng = Prng::from_seed(seed)
config = swarm_config(&prng) # which features/faults are on, and their rates
env = SimEnv { clock: VirtualClock, io: SimIo(prng, config), rng: prng }
sys = System::new(&env) # all I/O through env traits
model = ReferenceModel::new()
while env.clock.now() < config.ticks_max:
env.step() # drain one virtual-time task; may fire faults
if let Some(op) = workload.next(&prng, &model):
sys.apply(op); model.apply(op)
check_invariants(&sys, &model) # assert, never log-and-continue
assert_convergence(&sys, &model) # liveness phase
report { seed, commit, config, coverage, sometimes_hits }
Invariants: no wall-clock, thread or OS entropy reaches the system under test; any two runs with equal (build, seed) produce identical traces; every failure is emitted with the seed needed to reproduce it.
NUIF relevance
Borrow
- Make
(implementation version, capability profile, fixture, seed)the replay key of every conformance run, matching the report fields already required inconformance/PLAN.md(FoundationDB-s SEED; VOPR seed plus commit hash). - Put every nondeterminism source of the headless engine behind traits with simulated implementations: font and image loading, adapter file I/O, renderer scheduling, timestamps in provenance records (FoundationDB interface swap; madsim mocking rule).
- Adopt assertion density and pair assertions in
nuif-core,nuif-protocolandnuif-layoutso that invariants (stable IDs, containment, acyclic references,Extensionsunchanged by unrelated operations) fail inside the loop rather than in later comparison (TIGER_STYLE §Safety). - Swarm-randomise the operation mix, layout families and adapter set per seed instead of fixing one generator distribution (Groce 2012; VOPR
options_swarm). - Add reachability (“sometimes”) assertions for rare paths such as move-into-instance, extension preservation through an unaware intermediate, and lossy adapter fallbacks (Antithesis).
Adapt
- NUIF has no network or clock to virtualise; the analogue of environment faults is adapter loss (unsupported feature, approximated value), corrupted or truncated inputs, and resource-limit hits from
spec/11-security.md. Fault injection should target those. - The liveness phase becomes a convergence phase: after fault injection stops, canonical hashes across the round-trip path must converge, and operation replay from the same base must yield the same hash (
conformance/fixtures/v0-responsive-card/README.md). - Time compression is irrelevant; the equivalent budget is operations per second through the CLI/API contract of
spec/12-cli-api-and-automation.md.
Reject
- A deterministic hypervisor is unnecessary: NUIF controls its own process and can achieve determinism at the interface level.
- BUGGIFY-style probabilistic hooks inside production code paths conflict with a library that must be embeddable; fault hooks belong in the simulated trait implementations only.
Open questions
- Which floating-point paths in layout and rasterisation are deterministic across CPU architectures, and must the seed key include target triple and font rasteriser version?
- Should browser-based differential oracles be excluded from seeded runs, given that a browser cannot be made deterministic from NUIF’s side?
- How are seeds and swarm configurations recorded in the report so that a coverage-guided scheduler can prioritise seeds without breaking replayability?