|
ravel
Deterministic simulation testing for C++. Seed a bug, replay it exact.
|
A one-page mental model. Read it once and the rest of the documentation will make more sense, because everything else is a consequence of the ideas here.
Your code runs inside the box, and everything it can observe (the time, other tasks, the network, the disk, random numbers) is produced by ravel. Nothing from outside gets in, so a run is a pure function of its seed.
co_await.co_await is where a task hands control back: sleep, yield, channel.receive(), a disk operation. The task is parked until whatever it waits for happens.time_limit ends it.)Result.The interesting bugs live between steps 2 and 3. A co_await is a point where another task may run, and the scheduler tries different orders on different seeds. Two operations with no co_await between them cannot be interleaved; put one in (co_await sim.scheduler().yield()) if you want ravel to try reordering around it.
Three things need randomness, and all of them draw from the same recorded source:
sim.rng(), for workloads and jitter.Every draw is a bounded whole number, and 0 is always the simplest outcome: the task that has waited longest, no fault, the shortest delay. The list of everything a run drew is its list of choices. Here is one, printed, replayed and changed:
The list 1 0 0 1 1 1 1 0 0 0 6 is the run: replaying it gives the identical run (same digest) without any seed. An empty list means every draw takes its default, 0, which is the plainest run there is, here one that does not hit the bug. Anything in between is another run. That is what makes shrinking possible.
Four words, four jobs:
| What it is | What you use it for | |
|---|---|---|
| seed | a number that determines the whole run | naming a run: "seed 6 fails". Convenient, but its meaning can change when ravel is upgraded |
| choices | the list of every draw the run made | reproducing a run exactly; the shrunk list is your saved reproducer and stays valid across ravel upgrades |
| trace | one line per event: who ran when, which message was lost, which disk write completed | reading what happened; see debugging.md |
| digest | a 64-bit fingerprint of the trace | asking "are these two runs identical?" without comparing them event by event |
A failing run starts as a long list of choices, most of which have nothing to do with the bug. ravel looks for a shorter, simpler list that still fails the same way:
Each candidate list is replayed. If it still fails with the same message, it becomes the new best; if not, it is discarded. It stops when nothing simpler works. Because every list is a valid run (numbers too large are clamped, missing ones are 0), ravel can edit freely, and because runs are deterministic, the answer never depends on luck or on how many threads did the replaying.
The nonzero numbers left over are the ingredients of the bug. In the tutorial, 0 0 0 1 reads "deliver the request, lose the reply": that is the whole bug.
Channel: a one-way virtual network link between two named endpoints, with a FaultSpec (loss, delay, reordering). Choice: one recorded random draw. A run is its list of choices. Determinism: same seed, same run, on every machine. The foundation of everything. Digest: a fingerprint of a run's trace; equal digests mean identical runs. Disk: a virtual filesystem with a write-back cache, sync, rename, sync_dir, and crashes that lose or tear what was not synced. Invariant: a property that must hold when the run ends. Replay: run a saved list of choices instead of a seed. Reproducer: the shrunk choices of a failure, saved as a .choices file. Run: one execution of your system under one seed (or one list of choices). Scheduler: what picks which ready task runs next. Seed: a number that determines a whole run. Setup: your function that builds the system for one run. Shrinking: finding the simplest run that still fails the same way. Simulation: the object that owns a run's clock, randomness, scheduler, channels and disks. Sweep: running many seeds of the same setup. Task: a C++20 coroutine that is part of your system. Trace: the record of a run, one event per line. Virtual time: simulated time in ticks; it only moves when every task is waiting.