| name | quakey |
| description | Use when writing, adapting, or debugging code that uses the Quakey deterministic simulation testing framework. Covers simulation harness setup, adapting application code to run inside Quakey, fault injection, invariant checking, and build configuration. |
Overview
Quakey is a C framework for testing distributed systems by running multiple simulated nodes in a single process with fully controlled, deterministic I/O. Given the same seed, every simulation run produces identical results, making rare bugs reproducible.
- Multiple virtual nodes ("hosts") run cooperatively inside one OS process — no threads.
- All I/O (sockets, files, clocks, memory) is replaced with in-process mocks via preprocessor macros.
- Time is discrete and event-driven: it only advances when all hosts are blocked.
- A seeded PRNG controls all randomness (network delays, fault injection decisions).
- Faults (node crashes, network partitions) are injected deterministically.
Files
quakey/
include/quakey.h — public API and mock macro definitions
src/quakey.c — scheduler, event loop, mock network/time
src/mockfs.h — mock filesystem header
src/mockfs.c — in-memory filesystem implementation
Core API
int quakey_init(Quakey **quakey, QuakeyUInt64 seed);
void quakey_free(Quakey *quakey);
QuakeyNode quakey_spawn(Quakey *quakey, QuakeySpawn config, char *arg);
int quakey_schedule_one(Quakey *quakey);
QuakeyUInt64 quakey_current_time(Quakey *quakey);
int quakey_num_hosts(Quakey *quakey);
void *quakey_host_state(Quakey *quakey, int idx);
int quakey_host_is_dead(Quakey *quakey, int idx);
const char *quakey_host_name(Quakey *quakey, int idx);
void *quakey_node_state(QuakeyNode node);
void quakey_set_max_crashes(Quakey *quakey, int max_crashes);
void quakey_network_partitioning(Quakey *quakey, bool enabled);
QuakeyUInt64 quakey_random(void);
void quakey_signal(char *name);
int quakey_get_signal(Quakey *quakey, QuakeySignal *signal);
void quakey_enter_host(QuakeyNode node);
void quakey_leave_host(void);
QuakeySpawn config
typedef struct {
char *name;
int state_size;
QuakeyInitFunc init_func;
QuakeyTickFunc tick_func;
QuakeyFreeFunc free_func;
char **addrs;
int num_addrs;
int disk_size;
} QuakeySpawn;
Callback signatures
int my_init(void *state, int argc, char **argv,
void **ctxs, struct pollfd *pdata, int pcap,
int *pnum, int *timeout);
int my_tick(void *state, void **ctxs,
struct pollfd *pdata, int pcap, int *pnum, int *timeout);
int my_free(void *state);
pdata/pcap/pnum — poll array. The host fills it with fds and events to wait on.
timeout — milliseconds until next tick; -1 = wait forever, 0 = immediate.
ctxs — per-fd context pointers (parallel array to pdata, managed by application).
Enabling mocks
Define QUAKEY_ENABLE_MOCKS before including the header in any translation unit that should use mocked system calls:
#define QUAKEY_ENABLE_MOCKS
#include <quakey.h>
This replaces socket, open, read, write, malloc, clock_gettime, etc. with mock implementations via #define. Application code calls the standard names; macros redirect them transparently.
Do not define QUAKEY_ENABLE_MOCKS in the test harness file — only in the application source files under test.
Use QUAKEY_SIGNAL(name) (not quakey_signal() directly) inside application code so it compiles cleanly in both simulation and production builds.
Persistent vs volatile state
- Disk (
disk_size bytes): survives node crashes. Backed by MockFS.
- Memory (
state_size bytes + anything malloc'd): wiped on crash.
- Sockets/connections: closed and cleaned up on crash.
After a crash, init_func is called again with the same args. Applications can read disk to reconstruct in-memory state, exercising the recovery path.
Fault injection
| Fault | Behaviour |
|---|
| Node crash | All sockets closed, in-memory state wiped, disk preserved. Node restarts after 1–10 s simulated delay. |
| Network partition | Pairs of hosts that cannot communicate. Partitions drift gradually (one link added/removed every 100–500 ms). |
| Network delay | All connect/send operations delayed 0–100 ms. |
Enable probabilistic fault injection at compile time with -DFAULT_INJECTION.
Build
Compile application files with -DQUAKEY_ENABLE_MOCKS, link against quakey/src/quakey.c and quakey/src/mockfs.c:
my_server.o: my_server.c
$(CC) -Iquakey/include -DQUAKEY_ENABLE_MOCKS -c -o $@ $<
test_main.o: test_main.c
$(CC) -Iquakey/include -c -o $@ $<
test: test_main.o my_server.o quakey/src/quakey.c quakey/src/mockfs.c
$(CC) -o $@ $^
Workflow: adapting application code to run in Quakey
-
Guard mock includes — wrap the Quakey include so production builds are unaffected:
#ifdef SIMULATION
#define QUAKEY_ENABLE_MOCKS
#endif
#include <quakey.h>
-
Replace blocking I/O with poll-based I/O — Quakey schedules one tick at a time; any blocking call (blocking read, sleep, etc.) will stall the entire simulation. All waiting must go through the poll array.
-
Implement the three callbacks:
init_func — open sockets/files, populate the initial poll array, set timeout.
tick_func — handle ready events, update the poll array, return.
free_func — release resources (called on quakey_free, not on crash).
-
Use QUAKEY_SIGNAL instead of quakey_signal directly so code compiles in both simulation and production modes.
-
Parse node identity from arg — quakey_spawn passes a single string; use it to convey addresses, peer lists, node IDs, or any other per-node config.
Workflow: writing a simulation test harness
- Include
<quakey.h> without QUAKEY_ENABLE_MOCKS in the harness file.
- Call
quakey_init with a seed. Accept the seed as a CLI argument so any failing run can be reproduced exactly.
- Spawn all nodes before starting the loop. Spawn order affects scheduling order.
- Run the loop:
while (quakey_current_time(q) < time_limit_ns) {
quakey_schedule_one(q);
check_invariants(q);
drain_signals(q);
}
- Check invariants outside
quakey_schedule_one — reading another host's state from inside a tick_func is unsafe.
- Use
quakey_host_state(q, idx) returning NULL as the signal that a node is currently dead/restarting; skip it in invariant checks.
Workflow: invariant checking
- Maintain a shadow log in the harness: as entries become committed on any node, append them to the shadow. Verify that later commits from other nodes agree with the shadow prefix.
- Separate per-node safety checks (local consistency) from cross-node agreement checks (distributed correctness).
- Use
quakey_enter_host / quakey_leave_host if invariant checking needs to call mock filesystem functions to inspect a node's disk state.
Workflow: debugging a failing simulation
- Note the seed printed at startup (or passed via CLI).
- Re-run with the same seed — the failure will reproduce identically.
- Add
QUAKEY_SIGNAL("checkpoint") calls in the application at key points to narrow down where things go wrong.
- Reduce the time limit to stop the simulation closer to the failure.
- Add assertions directly in
tick_func for local invariants that should never be violated.
- Compile without
FAULT_INJECTION to rule out crash/partition interaction.
Common mistakes
- Calling
quakey_random() from harness context between spawns: safe. Calling it from multiple hosts in non-deterministic order: breaks reproducibility. Keep random calls inside tick_func or harness, not in both.
- Forgetting
quakey_enter_host: mock calls from harness context without entering a host will crash.
- Blocking in
tick_func: any real sleep, blocking recv, or blocking read stalls the whole simulation.
- Sharing pointers between hosts: each host has its own address space conceptually; in practice they share the process heap, but writing to another host's state from
tick_func is a data race against the scheduler.
- Using real
time() or rand(): bypasses determinism. Use quakey_current_time and quakey_random instead.