libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_sim.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License.
4 *
5 * src/inc/xtc_sim.h
6 * Deterministic Simulation Testing (DST) seams.
7 *
8 * Phase 0 is the seeded PRNG tree: a single root seed splits into
9 * per-stream sub-streams so a draw at one decision site (steal
10 * victim, placement, lock victim, fault toggle) does not perturb
11 * another site's sequence -- the FoundationDB discipline that keeps
12 * a replay stable when a new draw site is added.
13 *
14 * When sim is INACTIVE (the production default) __xtc_sim_active()
15 * is 0 and the runtime's existing ad-hoc randomness is used
16 * unchanged; the call sites consult __xtc_sim_active() and only draw
17 * from the seeded tree under sim. This module compiles into every
18 * build but is dormant unless a sim run activates it.
19 */
20
21#ifndef XTC_SIM_H
22#define XTC_SIM_H
23
24#include <stdint.h>
25
26/* Well-known PRNG streams. Each scheduler-relevant random draw site
27 * owns a stream so adding a draw in one site cannot shift another. */
28enum xtc_sim_stream {
29 XTC_SIM_RNG_SCHED = 0, /* which runnable loop advances next */
30 XTC_SIM_RNG_STEAL = 1, /* work-stealing victim selection */
31 XTC_SIM_RNG_PLACE = 2, /* round-robin proc/task placement */
32 XTC_SIM_RNG_LOCKVIC = 3, /* lock-manager deadlock victim */
33 XTC_SIM_RNG_IO = 4, /* simulated I/O latency / completion order */
34 XTC_SIM_RNG_FAULT = 5, /* fault-injection toggles */
35 XTC_SIM_RNG_APP = 6, /* application/test draws */
36 XTC_SIM_RNG_BUGGIFY = 7, /* buggify per-call activation coins */
37 XTC_SIM_RNG_NSTREAMS
38};
39
40/*
41 * 1 while a deterministic simulation run is active (set by
42 * xtc_sim_activate, cleared by xtc_sim_deactivate). Hot-path call
43 * sites branch on this to decide between seeded and ad-hoc randomness.
44 *
45 * PUBLIC: int __xtc_sim_active __P((void));
46 * PUBLIC: void __xtc_sim_nondeterminism __P((const char *));
47 * PUBLIC: void xtc_sim_strict __P((int));
48 * PUBLIC: int xtc_sim_nondeterminism_count __P((void));
49 * PUBLIC: void xtc_sim_activate __P((uint64_t));
50 * PUBLIC: void xtc_sim_deactivate __P((void));
51 * PUBLIC: uint64_t __xtc_sim_rng __P((int));
52 * PUBLIC: uint64_t __xtc_sim_rng_range __P((int, uint64_t));
53 * PUBLIC: void xtc_sim_clock_enable __P((int64_t));
54 * PUBLIC: void xtc_sim_clock_disable __P((void));
55 * PUBLIC: void xtc_sim_clock_advance __P((int64_t));
56 * PUBLIC: void xtc_sim_clock_set __P((int64_t));
57 * PUBLIC: int __xtc_sim_vclock __P((int64_t *));
58 */
59int __xtc_sim_active(void);
60
61/* Determinism guard. A sim-reachable primitive that would break seed
62 * replay (real clock, unseeded RNG, env read, raw thread id) calls
63 * __xtc_sim_nondeterminism(what); during a sim run it records the
64 * violation and, in strict mode (default), aborts naming the source.
65 * xtc_sim_strict toggles abort-vs-count; xtc_sim_nondeterminism_count
66 * lets a harness assert zero to PROVE a run was fully deterministic. */
67void __xtc_sim_nondeterminism(const char *what);
68void xtc_sim_strict(int on);
69int xtc_sim_nondeterminism_count(void);
70
71/* Clock skew (FoundationDB "the clock is not perfect"). The scheduler
72 * still schedules timers against the true virtual clock, but a fiber
73 * that reads the clock (xtc_proc time, timeout math) observes
74 * true + offset (+/- a seeded jitter), so its notion of elapsed time can
75 * disagree with when its timers actually fire -- stressing code that
76 * assumes wall time equals scheduler time. Seeded and bounded, so it
77 * replays; applied only on the observation seam, so it can never desync
78 * the scheduler. offset_ns is a fixed per-run offset; jitter_ns (>=0)
79 * is the +/- band of a seeded per-read wobble (0 = none). Off by
80 * default; reset on deactivate.
81 *
82 * PUBLIC: void xtc_sim_clock_skew __P((int64_t, int));
83 */
84void xtc_sim_clock_skew(int64_t offset_ns, int jitter_ns);
85
86/* Activate sim with a root seed (also resets every stream). Idempotent
87 * re-activation re-seeds. Test-only; never called in production. */
88void xtc_sim_activate(uint64_t seed);
89void xtc_sim_deactivate(void);
90
91/* Draw the next 64-bit value from stream `s` (0..XTC_SIM_RNG_NSTREAMS-1).
92 * Deterministic given the activation seed. Undefined if sim inactive
93 * (callers must gate on __xtc_sim_active()). */
94uint64_t __xtc_sim_rng(int s);
95
96/* Convenience: a uniform value in [0, bound) from stream `s`. bound==0
97 * returns 0. */
98uint64_t __xtc_sim_rng_range(int s, uint64_t bound);
99
100/* Deterministic fault toggle: returns 1 with probability
101 * pct_per_1000/1000, drawn from the dedicated FAULT stream (so enabling
102 * faults never shifts the schedule); 0 when sim is inactive. A test
103 * calls it at a fault decision point; the same seed reproduces the
104 * identical fault schedule.
105 *
106 * PUBLIC: int xtc_sim_fault __P((unsigned));
107 */
108int xtc_sim_fault(unsigned pct_per_1000);
109
110/*
111 * Critical-section fault points. A fault point marks an interleaving-
112 * sensitive critical section; under sim, when points are enabled,
113 * reaching one draws from the FAULT stream and, on a hit, records the
114 * fire. Enable with a per-1000 fire probability; query coverage with
115 * the fires/seen accessors. Production never
116 * reaches a fire (sim inactive); the call is a single relaxed load.
117 *
118 * PUBLIC: void xtc_sim_fault_points_enable __P((unsigned));
119 * PUBLIC: void xtc_sim_fault_points_disable __P((void));
120 * PUBLIC: int xtc_sim_fault_point __P((const char *));
121 * PUBLIC: uint64_t xtc_sim_fault_point_fires __P((const char *));
122 * PUBLIC: int xtc_sim_fault_points_seen __P((void));
123 */
124void xtc_sim_fault_points_enable(unsigned pct_per_1000);
125void xtc_sim_fault_points_disable(void);
126int xtc_sim_fault_point(const char *name);
127uint64_t xtc_sim_fault_point_fires(const char *name);
128int xtc_sim_fault_points_seen(void);
129
130/*
131 * Simulated I/O faults (DST). When enabled, the sim I/O backend defers
132 * file-AIO completions by a seeded latency (so completion ORDER across
133 * concurrent ops is part of the replayable schedule) and may inject a
134 * seeded fault (short transfer or EIO). Off by default -- a sim run
135 * that does not enable them gets inline completion. Seeded on the IO
136 * stream, so enabling does not perturb the schedule.
137 *
138 * PUBLIC: void xtc_sim_io_faults_enable __P((int64_t, int64_t, unsigned));
139 * PUBLIC: void xtc_sim_io_faults_disable __P((void));
140 * PUBLIC: int __xtc_sim_io_faults_active __P((void));
141 * PUBLIC: int64_t __xtc_sim_io_latency __P((void));
142 * PUBLIC: int __xtc_sim_io_should_fault __P((void));
143 */
144void xtc_sim_io_faults_enable(int64_t lat_min_ns, int64_t lat_max_ns,
145 unsigned fault_pct_per_1000);
146void xtc_sim_io_faults_disable(void);
147int __xtc_sim_io_faults_active(void);
148int64_t __xtc_sim_io_latency(void);
149int __xtc_sim_io_should_fault(void);
150
151/*
152 * Simulated TORN / CORRUPT writes and reads (DST) -- the torn-page fault
153 * class FoundationDB models. Distinct from the short-transfer / EIO
154 * faults above (which report a truncated-but-clean result the caller
155 * re-issues): a torn write actually PERSISTS fewer bytes than requested
156 * while still REPORTING full success, and a corrupt read flips a byte in
157 * the returned buffer. Both leave latent bad data a checksum must catch
158 * -- a storage engine's real durability hazard. Off by default; seeded
159 * on the IO stream so enabling does not perturb the schedule. A write
160 * is torn (its persisted prefix chosen) with probability corrupt_pct;
161 * likewise a read is bit-flipped with the same probability.
162 *
163 * PUBLIC: void xtc_sim_io_corrupt_enable __P((unsigned));
164 * PUBLIC: void xtc_sim_io_corrupt_disable __P((void));
165 * PUBLIC: int __xtc_sim_io_corrupt_active __P((void));
166 * PUBLIC: int __xtc_sim_io_torn_prefix __P((int));
167 * PUBLIC: int __xtc_sim_io_flip_byte __P((int));
168 */
169void xtc_sim_io_corrupt_enable(unsigned corrupt_pct_per_1000);
170void xtc_sim_io_corrupt_disable(void);
171int __xtc_sim_io_corrupt_active(void);
172int __xtc_sim_io_torn_prefix(int full_len);
173int __xtc_sim_io_flip_byte(int len);
174
175/*
176 * Disk-full (ENOSPC) injection: a seeded coin makes a WRITE fail with a
177 * hard out-of-space error mid-workload -- the whole op fails and nothing
178 * persists, distinct from a short transfer or a torn write -- so a
179 * storage engine's graceful-degradation path is exercised. Off by
180 * default; reset via enable(0). A no-op outside a sim run.
181 *
182 * PUBLIC: void xtc_sim_io_enospc_enable __P((unsigned));
183 * PUBLIC: int __xtc_sim_io_enospc __P((void));
184 */
185void xtc_sim_io_enospc_enable(unsigned pct_per_1000);
186int __xtc_sim_io_enospc(void);
187
188/*
189 * Stale-data reads (FoundationDB's "the disk returns an OLD durable
190 * version"). On a seeded coin a read at an offset that was written,
191 * then overwritten, returns the PRIOR (structurally valid but out of
192 * date) contents -- catching recovery/cache code that skips a
193 * version/LSN check. General (fd+offset level): a bounded ring
194 * snapshots superseded write payloads. Off by default; enable(pct)
195 * arms it and resets the ring for the run. A no-op outside a sim run.
196 *
197 * PUBLIC: void xtc_sim_io_stale_enable __P((unsigned));
198 * PUBLIC: void __xtc_sim_io_stale_record __P((int, uint64_t, const void *, int));
199 * PUBLIC: int __xtc_sim_io_stale_read __P((int, uint64_t, void *, int));
200 */
201void xtc_sim_io_stale_enable(unsigned pct_per_1000);
202void __xtc_sim_io_stale_record(int fd, uint64_t off, const void *buf, int len);
203int __xtc_sim_io_stale_read(int fd, uint64_t off, void *buf, int len);
204
205/*
206 * Write-back cache crash model (FoundationDB's simulated disk): pwrite
207 * lands in a volatile per-fd cache (tracked as the written high-water),
208 * fsync/fdatasync promotes the written extent to durable, and a crash
209 * loses everything past the last fsync. This lets a crash-recovery test
210 * catch a writer that ACKs a commit WITHOUT fsyncing it -- the sim
211 * writes to a real temp file, so without this model the bytes survive a
212 * crash regardless of fsync, hiding an ack-before-durable bug. A test
213 * arms it, and after the crash asks xtc_sim_io_durable_end(fd) for the
214 * true post-crash frontier (last fsync-confirmed byte) to truncate to,
215 * instead of trusting the writer's self-reported durable point. Off
216 * unless armed; bounded fd table.
217 *
218 * PUBLIC: void xtc_sim_io_wb_enable __P((int));
219 * PUBLIC: void __xtc_sim_io_wb_wrote __P((int, uint64_t));
220 * PUBLIC: void __xtc_sim_io_wb_synced __P((int));
221 * PUBLIC: uint64_t xtc_sim_io_durable_end __P((int));
222 */
223void xtc_sim_io_wb_enable(int on);
224void __xtc_sim_io_wb_wrote(int fd, uint64_t end_off);
225void __xtc_sim_io_wb_synced(int fd);
226uint64_t xtc_sim_io_durable_end(int fd);
227
228/*
229 * Simulated network partition + message latency (DST). A seeded,
230 * deterministic model of a partitioned / lossy / delayed network at the
231 * cross-LOOP message granularity xtc's sim models: xtc_send between
232 * procs on different loops routes through __mbox_deliver, the single
233 * cross-loop delivery seam. These let a DST test cut message flow
234 * between loops (a partition) and/or defer each cross-loop delivery by a
235 * seeded latency so delivery ORDER is part of the replayable schedule.
236 * All OFF by default (no behaviour change in production or normal sim).
237 *
238 * Loops are identified by pid.loop_id (== exec_id + 1; 0 == standalone).
239 * A blocked edge makes __mbox_deliver DROP the message via the sender's
240 * existing soft-full path (XTC_E_AGAIN), so a partitioned peer never
241 * deadlocks the sim. Scope: the in-process cross-loop path only -- the
242 * real cross-machine raw-socket transport (io_net.c) cannot run under
243 * the single-thread sim and is NOT modelled here.
244 *
245 * PUBLIC: void xtc_sim_partition_set __P((int, int, int));
246 * PUBLIC: void xtc_sim_partition_isolate __P((int));
247 * PUBLIC: void xtc_sim_partition_clear __P((void));
248 * PUBLIC: int __xtc_sim_partition_blocked __P((int, int));
249 * PUBLIC: void xtc_sim_net_latency __P((int64_t, int64_t));
250 * PUBLIC: int64_t __xtc_sim_net_latency __P((void));
251 */
252void xtc_sim_partition_set(int src_loop_id, int dst_loop_id, int blocked);
253void xtc_sim_partition_isolate(int loop_id);
254void xtc_sim_partition_clear(void);
255int __xtc_sim_partition_blocked(int src_loop_id, int dst_loop_id);
256void xtc_sim_net_latency(int64_t min_ns, int64_t max_ns);
257int64_t __xtc_sim_net_latency(void);
258
259/* Plant a critical-section fault point in runtime code. A single
260 * relaxed load in production (sim inactive); under sim with points
261 * enabled it perturbs/records per the FAULT stream. Elided entirely
262 * with XTC_INJECT_DISABLE, matching XTC_INJECTION_POINT. */
263#if defined(XTC_INJECT_DISABLE)
264# define XTC_SIM_FAULT_POINT(name) ((void)0)
265#else
266# define XTC_SIM_FAULT_POINT(name) ((void)xtc_sim_fault_point(name))
267#endif
268
269/*
270 * Buggify (FoundationDB-style). A named point in the REAL runtime code
271 * that, under sim, lets the code take a legal-but-pessimal path. Unlike
272 * xtc_sim_fault (a fresh draw per call), a buggify point is a coin
273 * flipped ONCE per run per site: decided on first reach, cached, and
274 * every later reach of the same name returns the same decision -- so a
275 * buggified site is consistent within a run and the run replays. 0 in
276 * production / when disabled. Enable with a per-1000 activation
277 * probability; query coverage with the active-count.
278 *
279 * PUBLIC: void xtc_sim_buggify_enable __P((unsigned));
280 * PUBLIC: void xtc_sim_buggify_disable __P((void));
281 * PUBLIC: int xtc_sim_buggify __P((const char *));
282 * PUBLIC: int xtc_sim_buggify_active_count __P((void));
283 * PUBLIC: int xtc_sim_buggify_reached_count __P((void));
284 * PUBLIC: int xtc_sim_buggify_site __P((int, char *, size_t, int *));
285 * PUBLIC: int xtc_sim_buggify_fault __P((unsigned));
286 */
287void xtc_sim_buggify_enable(unsigned pct_per_1000);
288void xtc_sim_buggify_disable(void);
289int xtc_sim_buggify(const char *name);
290int xtc_sim_buggify_active_count(void);
291int xtc_sim_buggify_reached_count(void);
292int xtc_sim_buggify_site(int idx, char *buf, size_t buflen,
293 int *out_activated);
294int xtc_sim_buggify_fault(unsigned pct_per_1000);
295
296/* Branch on a buggify point in runtime code:
297 * if (XTC_SIM_BUGGIFY("wal.flush.tiny_batch")) { ... pessimal ... }
298 * A single relaxed load in production; elided with XTC_INJECT_DISABLE. */
299#if defined(XTC_INJECT_DISABLE)
300# define XTC_SIM_BUGGIFY(name) (0)
301#else
302# define XTC_SIM_BUGGIFY(name) xtc_sim_buggify(name)
303#endif
304
305/* Virtual (logical) clock. When enabled, __os_clock_mono returns the
306 * virtual time instead of the host monotonic clock, so time is a pure
307 * function of the schedule. Test/scheduler-only. */
308void xtc_sim_clock_enable(int64_t start_ns);
309void xtc_sim_clock_disable(void);
310void xtc_sim_clock_advance(int64_t delta_ns);
311void xtc_sim_clock_set(int64_t ns);
312
313/* Query the virtual clock: returns 1 and writes *out_ns when active,
314 * 0 otherwise. The single seam __os_clock_mono consults. */
315int __xtc_sim_vclock(int64_t *out_ns);
316
317/*
318 * Run an executor's loops deterministically (DST scheduler). Activates
319 * sim with `seed` + the virtual clock, then drives the N loops as N
320 * cooperatively-scheduled entities on the calling thread under a
321 * seed-determined interleaving, until quiescence, the step budget
322 * (max_steps; <= 0 = unbounded), or xtc_exec_stop. Requires a sim
323 * build (--with-io-backend=sim). Returns XTC_OK on quiescence,
324 * XTC_E_AGAIN if the budget was hit with work remaining, or a negative
325 * code on a loop-step error. Declared opaquely (xtc_exec is defined in
326 * xtc_exec.h).
327 *
328 * PUBLIC: int xtc_sim_exec_run __P((struct xtc_exec *, uint64_t, long));
329 */
330struct xtc_exec;
331int xtc_sim_exec_run(struct xtc_exec *e, uint64_t seed, long max_steps);
332
333/* Structural invariant checker (run after each sim step): returns
334 * XTC_OK if all per-loop invariants hold, XTC_E_INTERNAL on the first
335 * violation. A 64-bit digest of observable per-loop state for replay
336 * equality (same seed+config -> same hash). Both take an xtc_exec.
337 *
338 * PUBLIC: int xtc_sim_check __P((struct xtc_exec *));
339 * PUBLIC: uint64_t xtc_sim_state_hash __P((struct xtc_exec *));
340 */
341int xtc_sim_check(struct xtc_exec *e);
342uint64_t xtc_sim_state_hash(struct xtc_exec *e);
343
344/*
345 * Adversarial scheduler bias (FoundationDB "hunt the worst interleaving"
346 * rather than a benign uniform-random order). When enabled, the sim
347 * scheduler, on a seeded XTC_SIM_RNG_SCHED coin, takes the PESSIMAL
348 * pick: it keeps running one pinned loop as long as that loop stays
349 * runnable -- monopolizing the executor and starving every peer (the
350 * classic worst order: a fiber holding a resource a peer is blocked on
351 * never yields). When the pin parks, a new victim is pinned (the
352 * least-recently-run runnable loop, so starvation rotates over a long
353 * run). pct_per_1000 is how often the pessimal pick is taken (0
354 * disables; e.g. 500 = half the picks monopolize, half uniform, so
355 * both orders are still explored across a seed sweep). OFF by default
356 * -- the default uniform pick keeps existing tests' schedules stable;
357 * opt in from an adversarial test / swarm. A no-op outside a sim run.
358 *
359 * PUBLIC: void xtc_sim_sched_pessimal __P((unsigned));
360 * PUBLIC: int __xtc_sim_sched_pessimal_pct __P((void));
361 */
362void xtc_sim_sched_pessimal(unsigned pct_per_1000);
363int __xtc_sim_sched_pessimal_pct(void);
364
365/*
366 * Completion / message SWIZZLE (reorder), independent of latency. The
367 * sim I/O event queue is normally due-time ordered; a deferred AIO
368 * completion or a cross-loop message is delivered in due order, so
369 * reordering only happens as a side effect of latency jitter. When
370 * swizzle is enabled, on a seeded XTC_SIM_RNG_IO coin an event is
371 * inserted one slot LATER than its due order would place it -- a
372 * legal reordering (the waiter simply wakes after a sibling completion
373 * it would otherwise have preceded), which explicitly explores
374 * completion/message-order interleavings the way FoundationDB swizzles
375 * connection and disk completions. pct_per_1000 is the per-insert
376 * reorder probability (0 disables). OFF by default. A no-op outside
377 * a sim run.
378 *
379 * PUBLIC: void xtc_sim_swizzle_enable __P((unsigned));
380 * PUBLIC: void xtc_sim_swizzle_disable __P((void));
381 * PUBLIC: int __xtc_sim_swizzle_pct __P((void));
382 */
383void xtc_sim_swizzle_enable(unsigned pct_per_1000);
384void xtc_sim_swizzle_disable(void);
385int __xtc_sim_swizzle_pct(void);
386
387/*
388 * Semantic consistency check (FoundationDB's end-of-test consistency
389 * workload). Install a callback that the sim runs ONCE at quiescence,
390 * after all the seeded faults/chaos, to assert a GLOBAL application
391 * invariant a per-step structural state hash cannot see -- e.g. "the
392 * B-tree is still well formed and holds exactly the acked-commit set".
393 * The callback returns XTC_OK if the invariant holds, nonzero otherwise;
394 * the sim propagates a nonzero result as the run's failure. NULL (the
395 * default) means no check. Set it before each run. A no-op in
396 * production.
397 *
398 * PUBLIC: void xtc_sim_set_consistency_check __P((xtc_sim_consistency_fn, void *));
399 * PUBLIC: int __xtc_sim_run_consistency_check __P((void));
400 */
401typedef int (*xtc_sim_consistency_fn)(void *arg);
402void xtc_sim_set_consistency_check(xtc_sim_consistency_fn fn, void *arg);
403int __xtc_sim_run_consistency_check(void);
404
405#endif /* XTC_SIM_H */