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