Skip to content

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 typed mar.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.network helpers such as setLossiness(...), setLatency(...), setClogs(...), and setPartitionDynamics(...).
  • runCase / expectPass / expectFuzz / expectFailure for scenario runs.
  • Trace events for sends, drops, deliveries, accepts, commits, and checks.
  • A named mar.StateCheck that inspects structured harness state.
  • Rejection of conflicting same-version proposals.
  • A parity test that initializes the same Replicas type 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.