Examples
Examples are small enough to read quickly, but they exercise the real Marionette APIs. Each example should become useful input for deterministic replay tests. The example set is intentionally small while the API is experimental.
The examples module root is examples/root.zig.
Add new examples there so zig build test picks them up without hard-coding
each example in build.zig.
Retry Queue
Source: examples/retry_queue.zig
The retry queue is the README-facing bug demo. It models a single leased job:
- Worker 1 leases the job.
- The lease times out.
- Worker 2 leases the same job.
- A late completion from worker 1 arrives after worker 2 owns the lease.
The correct scenario rejects the stale completion and then accepts worker 2's
completion. The deliberately buggy scenario accepts both completions, and a
named mar.StateCheck catches the duplicate completion:
var report = try retry_queue.runBuggyScenarioReport(allocator, 0xC0FFEE);
defer report.deinit();
The useful trace shape is:
queue.lease job=7 worker=1 deadline_ns=5000000
queue.timeout job=7 worker=1
queue.lease job=7 worker=2
queue.complete job=7 worker=1 accepted=true reason=stale_ack_bug completions=1
queue.complete job=7 worker=2 accepted=true reason=current_lease completions=2
queue.invariant_violation job=7 completions=2
This example does not require disk or a real scheduler. It shows the smaller Phase 0 loop Marionette is proving first: seeded choices, simulated time, trace-visible behavior, and a named checker that preserves the failure context.
KV Store
Source: examples/kv_store.zig
The KV store is the first disk-backed example. It is intentionally tiny: a
fixed-size append-only WAL where each sector is one record with a magic value,
key, value, and checksum. The store itself is production-shaped: it accepts
std.Io, a root std.Io.Dir, and a narrow mar.Recorder, then uses
std.Io.File positional read/write and sync. The harness still owns
Marionette's Control authority for disk faults, crash, restart, and scripted
corruption.
The correct scenario:
- Writes and syncs one committed record.
- Writes a second unsynced record.
- Crashes the disk so the unsynced record is lost.
- Restarts the disk.
- Injects scripted corruption into the second sector.
- Recovers by scanning records and validating checksums.
- Checks that the synced record is recovered exactly once and the unsynced record is rejected.
Run it with:
zig build run-example -- kv-store --seed 12648430 --summary
The deliberately buggy scenario accepts any record with the right magic value, ignoring the checksum. A torn write leaves enough bytes for the magic and key to look plausible, and the named checker catches that the unsynced record was recovered:
zig build run-example -- kv-store-bug --seed 12648430 --expect-failure
Useful trace events include:
disk.crash_write op=2 path=kv.wal offset=16 len=16 result=torn
kv.recover.record offset=16 key=2 value=0 mode=buggy_accept_magic_only
kv.invariant_violation reason=unsynced_record_recovered
Idempotency Bug
Source: examples/idempotency_bug.zig
The idempotency bug is a small seed-sensitive replay demo. It models two account-local deposits. The service has a subtle bug: it dedupes request IDs globally, even though request IDs are only required to be unique per account.
Most seeds choose distinct request IDs and pass. Seeds that reuse the same request ID across two accounts suppress the second deposit, and the checker catches the lost update.
Run a passing seed:
zig build run-example -- idempotency-bug --seed 12648430 --summary
Replay a failing seed:
zig build run-example -- idempotency-bug --seed 13 --expect-failure
Use --trace with the failing seed to print the same failure trace each time.
Useful trace events include:
buggify hook=reuse_request_id_across_accounts
idempotency.requests alice_id=... bob_id=... reused=true
idempotency.deposit account=bob ... accepted=false reason=global_duplicate
idempotency.invariant_violation
Replicated Register
Source: examples/replicated_register.zig
The replicated register is the first VOPR-inspired showcase. It is not a real consensus protocol and does not copy TigerBeetle internals. It demonstrates the portable shapes Marionette needs:
- A small cluster model with three replicas.
- Seeded message drops and delivery latency.
world.simulate(.{ .network = ... })producing typedmar.Endpoint(MessagePayload)node endpoints backed by fixed-topology per-link queues ordered by(deliver_at, packet_id).- A partition scenario that drops queued packets through directed link filters.
- Runtime network fault configuration through focused
control.networkhelpers such assetLossiness(...),setLatency(...),setClogs(...), andsetPartitionDynamics(...). runCase/expectPass/expectFuzz/expectFailurefor scenario runs.- Trace events for sends, drops, deliveries, accepts, commits, and checks.
- A named
mar.StateCheckthat inspects structured harness state. - Rejection of conflicting same-version proposals.
- A parity test that initializes the same
Replicastype with simulated and production-shaped network handles.
The normal scenario writes one value to a quorum, commits it, and checks that committed replicas agree and that committed values were accepted by a quorum:
const trace = try replicated_register.runScenario(allocator, 0xC0FFEE);
defer allocator.free(trace);
The trace starts with the run name and records network fault configuration as explicit control events, so the seed is not the only context available when a failure is replayed.
The example also includes a deliberately buggy scenario used by tests to prove the checker path catches divergent committed state:
var report = try replicated_register.runBuggyScenarioReport(allocator, 0xC0FFEE);
defer report.deinit();
The partition scenario isolates one replica from the client and majority, then heals the network and replays the same value so the previously isolated replica commits too:
const trace = try replicated_register.runPartitionScenario(allocator, 0xC0FFEE);
defer allocator.free(trace);
There is also a same-version conflict scenario used by tests to prove the register rejects conflicting values instead of overwriting accepted state.
This is intentionally tiny. Its job is to make the future scheduler, network, and invariant APIs concrete enough to critique before they become core library surface.
Toy DB
Source: examples/toy_sql_db.zig
The toy database is a tiny protocol-adapter example. Its wire format is just a
one-byte tag plus an optional little-endian i64. Its purpose is not SQL
coverage; it shows how a user-owned protocol can declare codecs for
mar.CodecTransport so database code sees typed Request and Response
values instead of Marionette vtables, byte buffers, or message-pool handles.
The scenario drives a client and server over simulated byte endpoints, so the same adapter shape can later sit on production byte endpoints.
Durable Broadcast
Source: examples/durable_broadcast.zig
Durable broadcast is the first example that combines disk and network in one harness. It models a service that writes one operation to a local WAL, syncs it, then broadcasts the operation to three replicas and waits for a quorum of acknowledgements.
The example is deliberately narrow: one fixed-size WAL record, one operation, and scripted crash/restart. The roadmap tracks follow-ups for extracting the duplicated WAL framing helper, adding a probabilistic buggy fuzz/search variant, splitting happy-path and crash-recovery scenarios, and growing this into a multi-record recovery case.
The checker asserts the cross-subsystem invariant:
- if a quorum acknowledged an operation, that operation must be recoverable from local durable storage after crash/restart;
- if any replica accepted an operation, it must match the recovered durable operation.
Run the correct scenario:
zig build run-example -- durable-broadcast --seed 12648430 --summary
The deliberately buggy scenario broadcasts before syncing. The replicas can acknowledge the operation, then a crash loses the pending WAL write. The checker catches that the network-visible operation was not durable:
zig build run-example -- durable-broadcast-bug --seed 12648430 --expect-failure
Useful trace events include:
durable.broadcast.quorum op=1 value=99 acks=3
disk.crash_write op=0 path=durable_broadcast.wal offset=0 len=24 result=lost
durable.invariant_violation reason=quorum_without_durable
Example Rules
- Keep examples focused and readable.
- Prefer one clear service behavior over a broad feature tour.
- Route time and randomness through Marionette interfaces.
- Return or expose traces so tests can compare replay behavior.
- Avoid
std.time, unseeded randomness, threads, filesystem, and network calls in simulated example code.