libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
loop_int.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/loop_int.h
6 * Internal definitions for the L2 event loop. Not part of the
7 * public ABI.
8 */
9
10#ifndef XTC_LOOP_INT_H
11#define XTC_LOOP_INT_H
12
13#include <stdatomic.h>
14#include <stdint.h>
15
16#include "xtc_loop.h"
17#include "xtc_io.h"
18#include "xtc_res.h"
19#include "deque.h"
20#include "os_thread.h"
21
22/*
23 * Task state machine. Transitions:
24 * SCHEDULED -> RUNNING loop pops from queue
25 * RUNNING -> SCHEDULED fn returned RESCHED
26 * RUNNING -> PARKED fn returned PENDING
27 * RUNNING -> DONE fn returned DONE
28 * PARKED -> SCHEDULED waker fired (or timer / fd ready)
29 * PARKED -> DONE (not reachable; PENDING tasks
30 * are reaped only after they
31 * next return DONE)
32 */
33enum xtc_task_state {
34 XTC_TS_SCHEDULED = 0,
35 XTC_TS_RUNNING = 1,
36 XTC_TS_PARKED = 2,
37 XTC_TS_DONE = 3
38};
39
40struct xtc_task {
41 xtc_task_fn fn;
42 void *user;
43 xtc_loop_t *loop;
44 _Atomic int state; /* xtc_task_state; atomic + CAS on PARKED->SCHEDULED
45 * so a cross-loop XTC_INB_WAKE drained on a peer
46 * loop cannot race the owning loop's dispatch
47 * write (the concurrent-commit task->state race,
48 * TSan-reported 2026-08-30). */
49 /* L1 proportional-share scheduler: index into loop->classes of the
50 * run-class this task belongs to, or -1 for the default (implicit,
51 * plain-FIFO) class. Set at spawn from xtc_proc_opts_t.sched_class
52 * or xtc_proc_set_class; survives a work-steal (a stolen task keeps
53 * its tag and is placed into the same class index on the thief).
54 * -1 by default, so a loop with no classes created is byte-for-byte
55 * the old FIFO+deque path. INSPIRED BY Glommio's task-queue handle
56 * (executor/mod.rs TaskQueue). */
57 int sched_class;
58 /* Monotonic time (ns) this run quantum was dispatched, recorded
59 * by the scheduler when loop->yield_budget_ns > 0. xtc_yield_check
60 * compares against it; 0 means not yet recorded this quantum. */
61 int64_t run_start_ns;
62 /* Pinned tasks run only on their home loop -- they go on the
63 * owner-only FIFO, never the stealable deque. Used for explicit
64 * placement (xtc_exec_spawn_on) and for processes, which keep a
65 * shard-style affinity to one loop. Unpinned tasks (the general
66 * pool) go on the Chase-Lev deque and may be work-stolen. */
67 int pinned;
68 /* Run-queue intrusive next pointer. */
69 struct xtc_task *q_next;
70
71 /* Park bookkeeping. At most one of these is active at a time
72 * while the task is in PARKED state. */
73 xtc_timer_t *park_timer;
74 int park_fd; /* -1 when not parked on fd */
75 /* Voluntary park: when set by a primitive (e.g. xtc_amutex) just
76 * before yielding, the coro step returns PENDING instead of
77 * RESCHED, so the task sleeps until a waker re-enqueues it rather
78 * than busy-spinning. Read-and-cleared by the step. */
79 int park_requested;
80
81 /* Wakeup-cause flags, set by the dispatcher / timer callback /
82 * mbox_deliver when the task is unparked. Sampled and cleared
83 * by the parker on resume (e.g. xtc_proc_wait_fd). Encodes
84 * XTC_IO_* flags from the dispatched event plus the synthetic
85 * XTC_WAIT_MAILBOX (set by __mbox_deliver) and XTC_WAIT_TIMEOUT
86 * (set by the timer callback). Atomic: a cross-thread xtc_send
87 * ORs XTC_WAIT_MAILBOX in from a FOREIGN thread (proc.c) while the
88 * owning loop ORs/reads/zeros it (task.c/loop.c/proc.c), so a plain
89 * uint32_t RMW here is a data race (TSan-reportable). All accesses
90 * use relaxed atomics -- ordering is provided by the waker/mailbox
91 * lock; the atomic only makes the OR itself race-free. */
92 _Atomic uint32_t wake_revents;
93
94 /* Latched cross-thread wake that arrived while this task was NOT yet
95 * PARKED (the prepare/park race: a foreign xtc_waker_wake fires
96 * between the parker arming its waker and the loop transitioning it
97 * to PARKED on yield). The WAKE-drain sets this instead of dropping
98 * the wake; the RUNNING->PARKED transition consumes it and
99 * re-schedules rather than parking, so the wake is never lost.
100 * Set cross-thread, consumed on the owning loop's thread. */
101 _Atomic int wake_pending;
102
103 /* Doubly linked into loop->all_tasks so a completed task can be
104 * unlinked in O(1) and recycled to the loop's task_slab (instead of
105 * lingering until loop_fini). all_prev == NULL means the head. */
106 struct xtc_task *all_next;
107 struct xtc_task *all_prev;
108
109 /* 1 if this task struct is eligible to be recycled onto the loop's
110 * task free-list when it completes (a plain task on its home loop);
111 * cleared for tasks that must not be recycled. All task structs
112 * are __os_calloc'd regardless -- this only gates the free-list
113 * push, not the allocation source. */
114 int recyclable;
115
116 /* Optional cleanup hook invoked by xtc_loop_fini before the task
117 * struct is freed. The coroutine layer sets this to release the
118 * fiber stack + coro struct that wrap a task; plain tasks leave
119 * it NULL. Keeps task lifetime owned by the loop while letting
120 * higher layers reclaim what they attached. */
121 void (*cleanup)(void *cleanup_arg);
122 void *cleanup_arg;
123};
124
125/*
126 * Timer record. Kept in a binary min-heap inside the loop.
127 *
128 * Cancel is lazy: we mark cancelled and skip on pop. Cancel is O(1)
129 * by cost; the heap may carry up to N stale entries until the next
130 * extraction reaches them. For M3 this is good enough; M5 may
131 * upgrade to a hierarchical wheel.
132 */
133struct xtc_timer {
134 int64_t deadline_ns;
135 xtc_timer_fn cb;
136 void *user;
137 xtc_task_t *waiter; /* task to wake when fired (NULL if pure cb) */
138 int heap_idx; /* current position in heap (-1 if not in) */
139 int cancelled;
140 int fired;
141 int sim_late; /* DST: 1 once a buggify late-fire bumped
142 * this timer's deadline (bump at most
143 * once so a late fire cannot spin);
144 * always 0 outside sim. */
145 xtc_loop_t *loop; /* back-pointer for cancel-by-handle */
146 struct xtc_timer *all_next; /* per-loop linked list for cleanup */
147};
148
149/*
150 * Inbox message kinds. All inbox traffic is cross-thread; the loop
151 * owner drains its inbox at the top of every step.
152 */
153enum xtc_inbox_kind {
154 XTC_INB_WAKE = 0, /* re-queue a parked task */
155 XTC_INB_PUBLISH = 1, /* publish a freshly-allocated task */
156};
157
159 enum xtc_inbox_kind kind;
160 xtc_task_t *task;
161 struct xtc_inbox_msg *next;
162};
163
164struct xtc_inbox {
165 __os_mutex_t lock;
166 struct xtc_inbox_msg *head;
167 struct xtc_inbox_msg *tail;
168 int inited;
169};
170
171/*
172 * L1 proportional-share (weighted-fair) run class. INSPIRED BY Glommio
173 * (Glauber Costa / ScyllaDB): Glommio's executor keeps a set of task
174 * queues, each with SHARES (1..1000) and a virtual runtime, and a
175 * min-vruntime pick gives each queue a weighted CPU fraction
176 * (executor/mod.rs TaskQueue + account_vruntime; shares.rs
177 * reciprocal_shares).
178 *
179 * A class is a per-loop overlay on the ready set: it carries its own
180 * ready FIFO (q_head/q_tail through the task's q_next) alongside the
181 * class's shares, virtual runtime, and the precomputed reciprocal used
182 * by the account formula. The default (implicit) class is index -1 on
183 * a task and is NOT one of these entries -- it uses the loop's plain
184 * q_head/q_tail + deque, so a loop with zero classes runs the exact old
185 * path with zero overhead. The vruntime pick activates only once a
186 * class has been created on the loop (loop->n_classes > 0).
187 */
189 int shares; /* 1..1000 (Glommio's range) */
190 int64_t latency_ns; /* optional latency bound; 0 = none */
191 uint64_t reciprocal; /* (1<<22)/shares, precomputed */
192 uint64_t vruntime; /* accumulated weighted runtime */
193 uint64_t runs; /* times a task from this class was run
194 * (telemetry / CPU-share observability) */
195 struct xtc_task *q_head; /* this class's ready FIFO */
196 struct xtc_task *q_tail;
197 int in_use; /* 1 once created */
198};
199
200/* Max scheduling classes per loop. Glommio apps use a handful of task
201 * queues; a small fixed array keeps __queue_pop a tiny linear scan (no
202 * heap, no alloc on the dispatch path). Creating more than this many
203 * classes on one loop returns XTC_E_AGAIN. */
204#define XTC_LOOP_MAX_CLASSES 16
205
206/* Shares weight of the implicit default lane (untagged tasks) when it
207 * races the explicit classes in the min-vruntime pick. Chosen at the
208 * top of Glommio's 1..1000 range so untagged/background work gets a
209 * fair-but-not-dominant slice against a small set of weighted classes;
210 * a class must be created with shares > this to out-run the default. */
211#define XTC_DEFAULT_CLASS_SHARES 100
212#define XTC_DEFAULT_CLASS_RECIP (((uint64_t)1 << 22) / XTC_DEFAULT_CLASS_SHARES)
213
214/* Minimum per-run cost charged to a class's vruntime (reduction-style
215 * floor). Guarantees vruntime always advances so the min-vruntime pick
216 * cannot degenerate -- essential under the deterministic simulator,
217 * where virtual time does not advance within a compute run (elapsed 0).
218 * In production a real run's elapsed exceeds this, so the accounting is
219 * Glommio-faithful (weighted by measured CPU time). */
220#define XTC_VRUNTIME_MIN_QUANTUM_NS 1000
221
222struct xtc_loop {
223 xtc_io_t *io;
224
225 /* Local run queue (Chase-Lev deque, owner pushes/pops). */
226 xtc_deque_t deque;
227
228 /* Slow-path overflow when the deque is full. Owner-only. */
229 struct xtc_task *q_head;
230 struct xtc_task *q_tail;
231
232 /* Timer min-heap. */
233 xtc_timer_t **timers;
234 int n_timers;
235 int cap_timers;
236
237 /* All tasks ever spawned, for cleanup. Owner-only after init. */
238 struct xtc_task *all_tasks;
239
240 /* All timers ever created, for cleanup at fini. */
241 xtc_timer_t *all_timers;
242
243 /* M11.5b: per-loop slab cache for xtc_timer_t. Created lazily
244 * by xtc_timer_set; freed in loop_fini. Per-loop = single-
245 * threaded ownership = magazine fast path is lock-free. */
246 struct xtc_slab *timer_slab;
247
248 /* Per-loop task-struct free-list: a plain single-threaded LIFO of
249 * recycled task structs (linked through their q_next while free).
250 * xtc_task_spawn pops from it instead of malloc'ing, and a
251 * completed plain task on its home loop is pushed back instead of
252 * freed -- the spawn-heavy hot path, with no allocator call and no
253 * accumulation. Only the owning loop thread touches it, so it is
254 * lock-free. Drained (structs __os_free'd) at loop_fini. */
255 struct xtc_task *task_free;
256 int task_free_n;
257
258 /* Live-task counter. Atomic so cross-thread spawns/completions
259 * can update it without lock. */
260 _Atomic int n_alive;
261
262 /* Per-loop work statistics (executor observability). tasks_run
263 * counts task steps executed on this loop; steals counts tasks
264 * this loop successfully stole from a peer. Relaxed atomics:
265 * read-mostly counters, exactness across a concurrent read is
266 * not required. */
267 _Atomic uint64_t n_tasks_run;
268 _Atomic uint64_t n_steals;
269
270 /* I/O fairness: counts task runs since the last I/O poll. When the
271 * run queue never empties (busy-yielding fibers, e.g. a buffer
272 * manager spinning on eviction), the loop would otherwise never
273 * poll I/O and parked completions would starve. Every
274 * IO_FAIRNESS_QUANTUM runs the step does a non-blocking poll. */
275 unsigned int runs_since_poll;
276
277 /* Cooperative yield watchdog (opt-in). When yield_budget_ns > 0
278 * the scheduler records each quantum's start time on the task and
279 * xtc_yield_check reports a task over budget; n_yield_due counts
280 * over-budget reports (telemetry). */
281 int64_t yield_budget_ns;
282 _Atomic uint64_t n_yield_due;
283
284 /* L1 proportional-share scheduler (opt-in). n_classes == 0 (the
285 * default) means no class was ever created and the scheduler uses
286 * the plain q_head/q_tail FIFO + deque -- byte-for-byte the old
287 * path. Once a class exists, __queue_pop picks the min-vruntime
288 * in-use class and pops its FIFO. Owner-only (no lock): classes
289 * are created and picked only on the loop's own thread. */
290 struct xtc_run_class classes[XTC_LOOP_MAX_CLASSES];
291 int n_classes;
292 /* Virtual runtime of the IMPLICIT default lane (the plain
293 * q_head/q_tail FIFO + deque, for tasks with sched_class == -1).
294 * When classes exist, the default lane races in the same
295 * min-vruntime pick as a class with fixed default shares, so
296 * untagged/background work is never starved by always-ready class
297 * work -- it just gets a default weight. Unused (stays 0) until a
298 * class is created. INSPIRED BY Glommio's default task queue. */
299 uint64_t default_vruntime;
300 /* Effective per-loop latency bound: the smallest latency_ns over
301 * all in-use classes (0 = none). Shrinks the yield/preempt
302 * interval so a latency class is checked often, like Glommio's
303 * reevaluate_preempt_timer. Recomputed on class create. */
304 int64_t class_latency_ns;
305
306 /* L3 over-budget stall watchdog (opt-in, off by default). When
307 * stall_budget_ns > 0 the run-end boundary in __xtc_loop_step
308 * compares the elapsed run time against it and, on an overrun,
309 * invokes stall_cb (or logs). A single branch on stall_budget_ns
310 * == 0 when off, so zero cost unless enabled. INSPIRED BY
311 * Glommio's stall detector (executor/stall.rs). */
312 int64_t stall_budget_ns;
313 void (*stall_cb)(xtc_loop_t *loop, xtc_task_t *task,
314 int64_t ran_ns, int64_t budget_ns,
315 void *user);
316 void *stall_cb_user;
317 _Atomic uint64_t n_stalls; /* over-budget reports (telemetry) */
318
319 /* Set by xtc_loop_stop, read by xtc_loop_run's condition. ATOMIC
320 * because xtc_loop_stop is a cross-thread call by construction: it
321 * pairs the flag with xtc_io_wakeup, whose whole purpose is to nudge
322 * a loop running on ANOTHER thread out of its I/O wait. A plain int
323 * here is a real data race (TSan flags it as soon as any test stops a
324 * loop from a foreign thread), and relaxed ordering is sufficient:
325 * the wakeup's own release/acquire provides the ordering, this flag
326 * only has to be seen eventually and its store must not tear. */
327 _Atomic int stop_requested;
328
329 /* Cross-thread inbox: wakers and remote spawns deposit here;
330 * the owner drains in __xtc_loop_drain_inbox. */
331 struct xtc_inbox inbox;
332
333 /* For the multi-loop executor: 0-based index in xtc_exec; -1 if
334 * this loop is standalone (M3 single-thread mode). */
335 int exec_id;
336
337 /* Back-pointer to the executor (NULL if standalone). */
338 struct xtc_exec *exec;
339
340 /*
341 * Resource accountant. Either owned by the loop (allocated and
342 * freed at init/fini) or borrowed from the executor. Tracks
343 * tasks-alive, inbox messages, channels, etc.
344 */
345 xtc_res_t *res;
346 int owns_res;
347
348#if defined(XTC_DIAGNOSTIC)
349 /*
350 * DIAGNOSTIC owner-thread guard. A loop's owner-only structures
351 * (the Chase-Lev deque, the q_head/q_tail slow FIFO, the timer
352 * min-heap + all_timers, the task_slab free-list) must be mutated
353 * ONLY by the OS thread that runs this loop. Four bugs in the
354 * v1.40.1..v1.40.4 arc were exactly a cross-loop mutation of one of
355 * these from a work-stolen fiber resuming on the wrong thread.
356 * owner_tid is recorded when the loop begins running on its thread
357 * (the exec worker, or xtc_loop_run); XTC_ASSERT_LOOP_OWNER aborts
358 * the instant a non-owner touches an owner-only structure, turning
359 * that whole race category from a probabilistic eventual wedge into
360 * a deterministic, immediate, located abort in the first offending
361 * run. Compiled out entirely in a normal build (zero cost). */
362 pthread_t owner_tid;
363 int owner_set;
364#endif
365};
366
367#if defined(XTC_DIAGNOSTIC)
368/*
369 * Abort if the calling thread is not this loop's owner. `site` names the
370 * owner-only structure being mutated, for a legible message. A loop with
371 * no recorded owner yet (owner_set == 0: before it has started running on
372 * a thread, e.g. spawn-time init) is exempt -- there is no concurrent
373 * owner to race. __xtc_current_loop is deliberately NOT used to decide
374 * ownership: it is the fiber's LOGICAL loop binding, preserved across a
375 * work-steal migration, so it does not identify the physical OS thread
376 * (this is the exact trap that caused a regression in the v1.40.3 fix).
377 * pthread_self() is the authoritative physical-thread identity.
378 */
379void __xtc_loop_owner_violation(const struct xtc_loop *loop, const char *site);
380extern XTC_THREAD_LOCAL struct xtc_loop *__xtc_current_loop;
381extern int __xtc_sim_active(void);
382#define XTC_ASSERT_LOOP_OWNER(loop, site) \
383 do { \
384 const struct xtc_loop *_l = (loop); \
385 if (_l == NULL) \
386 break; \
387 if (__xtc_sim_active()) { \
388 /* Single-thread DST: the physical-thread check can \
389 * never fire (one thread drives all loops), but the \
390 * VIOLATION still executes -- a fiber mutating a loop \
391 * other than the one currently being stepped is the \
392 * exact cross-loop bug, just serialized so it cannot \
393 * corrupt. __xtc_current_loop is the loop being \
394 * stepped, i.e. the only legitimate mutation target. \
395 * This makes the whole category DETERMINISTICALLY \
396 * reproducible under a seed. */ \
397 if (__xtc_current_loop != NULL && \
398 __xtc_current_loop != _l) \
399 __xtc_loop_owner_violation(_l, (site)); \
400 } else if (_l->owner_set && \
401 !pthread_equal(pthread_self(), _l->owner_tid)) { \
402 __xtc_loop_owner_violation(_l, (site)); \
403 } \
404 } while (0)
405#else
406#define XTC_ASSERT_LOOP_OWNER(loop, site) ((void)0)
407#endif
408
409/* Internal helpers shared between loop.c, task.c, timer.c. */
410int __xtc_loop_enqueue(xtc_loop_t *loop, xtc_task_t *t);
411/* L1: create a run class on `loop`. shares in 1..1000; latency_ns >= 0
412 * (0 = none). Returns the class index in *out_idx (>= 0), or an error
413 * (XTC_E_INVAL bad args, XTC_E_AGAIN if the per-loop class cap is hit).
414 * Owner-only. */
415int __xtc_loop_class_create(xtc_loop_t *loop, int shares,
416 int64_t latency_ns, int *out_idx);
417/* Spawn with explicit pinned-ness: pinned tasks stay on `loop` (FIFO,
418 * never work-stolen); unpinned tasks may migrate via the deque. */
419int __xtc_task_spawn_ex(xtc_loop_t *loop, xtc_task_fn fn, void *user,
420 int pinned, xtc_task_t **out_task);
421int __xtc_timer_heap_push(xtc_loop_t *loop, xtc_timer_t *t);
422xtc_timer_t *__xtc_timer_heap_pop_due(xtc_loop_t *loop, int64_t now_ns);
423int64_t __xtc_timer_heap_next_deadline(xtc_loop_t *loop);
424void __xtc_task_cancel_park_timer(xtc_task_t *self);
425int __xtc_loop_dispatch_event(xtc_loop_t *loop, xtc_io_event_t *ev);
426
427/*
428 * L2 io_uring ring-pointer preempt seam (INSPIRED BY Glommio's
429 * need_preempt). Real on the io_uring backend (src/io/io_uring.c),
430 * portable no-op stubs elsewhere, so loop.c/exec.c call them
431 * unconditionally. _arm returns XTC_OK only when the ring is now the
432 * preempt trigger (uring backend); _due is the two-load Glommio check
433 * (1 = a preempt slice elapsed); _disarm tears the ring down. */
434int __xtc_io_uring_preempt_arm(xtc_io_t *io, int64_t interval_ns);
435int __xtc_io_uring_preempt_due(xtc_io_t *io);
436void __xtc_io_uring_preempt_disarm(xtc_io_t *io);
437
438/* Implemented in proc.c. Called from loop_fini to release the
439 * proc-table side struct hashed against this loop pointer. Must be
440 * idempotent. */
441void __xtc_proc_loop_unregister(xtc_loop_t *loop);
442/* Inbox API. Producer-side functions are thread-safe. */
443int __xtc_inbox_init(struct xtc_inbox *ib);
444void __xtc_inbox_fini(struct xtc_inbox *ib);
445int __xtc_inbox_push(struct xtc_inbox *ib, enum xtc_inbox_kind k, xtc_task_t *t);
446int __xtc_inbox_drain(xtc_loop_t *loop); /* owner-only; drains into local queue */
447
448/* Per-thread cursor: which loop the calling thread is running.
449 * NULL on threads that aren't loop owners. */
450extern XTC_THREAD_LOCAL xtc_loop_t *__xtc_current_loop;
451
452/*
453 * Fiber-context preservation hook. The L3 process layer keeps a
454 * per-thread "current proc" pointer that must survive a yield (the
455 * scheduler runs other fibers in between, which overwrite it). The
456 * L2 coro layer cannot depend on L3, so every yield/await jump saves
457 * the opaque context before jumping to the scheduler and restores it
458 * on resume through these hooks. proc.c installs them on first
459 * spawn; while NULL (no process layer in use) the calls are no-ops.
460 * Set once to stable function addresses, so a plain pointer load is
461 * safe without synchronization.
462 */
463extern void *(*__xtc_fiber_ctx_save)(void);
464extern void (*__xtc_fiber_ctx_restore)(void *);
465
466/* Post-resume cancellation hook. Installed by the process layer
467 * (proc.c). Called at the universal fiber resume point (after a yield
468 * returns) so a fiber that had a kill/cancel requested while it was
469 * NOT at a cooperative point -- e.g. a pure CPU loop that was
470 * involuntarily preempted -- honors the kill the instant the scheduler
471 * resumes it, by unwinding via xtc_exit_self. NULL when no process
472 * layer is present (bare coroutine use). Returns without effect if no
473 * kill is pending. */
474extern void (*__xtc_fiber_kill_check)(void);
475
476/* Loop-fini hook. Installed by the process layer (proc.c) on first
477 * spawn; lets xtc_loop_fini release the loop's per-loop proc table
478 * without the L2 loop depending on the L3 proc layer directly (the
479 * loop calls the hook, proc.c points it at __xtc_proc_loop_unregister).
480 * NULL when no process layer is in use, so a bare-coroutine loop has
481 * nothing to clean up. */
482extern void (*__xtc_loop_fini_hook)(xtc_loop_t *loop);
483
484/* Forward declaration for back-pointer in xtc_loop. */
485struct xtc_exec;
486
487#endif /* XTC_LOOP_INT_H */