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#ifdef XTC_ENABLE_SCHED_POLICY
22#include "sched_int.h"
23#endif
24
25/*
26 * Task state machine. Transitions:
27 * SCHEDULED -> RUNNING loop pops from queue
28 * RUNNING -> SCHEDULED fn returned RESCHED
29 * RUNNING -> PARKED fn returned PENDING
30 * RUNNING -> DONE fn returned DONE
31 * PARKED -> SCHEDULED waker fired (or timer / fd ready)
32 * PARKED -> DONE (not reachable; PENDING tasks
33 * are reaped only after they
34 * next return DONE)
35 */
36enum xtc_task_state {
37 XTC_TS_SCHEDULED = 0,
38 XTC_TS_RUNNING = 1,
39 XTC_TS_PARKED = 2,
40 XTC_TS_DONE = 3
41};
42
43struct xtc_task {
44 xtc_task_fn fn;
45 void *user;
46 xtc_loop_t *loop;
47 _Atomic int state; /* xtc_task_state; atomic + CAS on PARKED->SCHEDULED
48 * so a cross-loop XTC_INB_WAKE drained on a peer
49 * loop cannot race the owning loop's dispatch
50 * write (the concurrent-commit task->state race,
51 * TSan-reported 2026-08-30). */
52 /* L1 proportional-share scheduler: index into loop->classes of the
53 * run-class this task belongs to, or -1 for the default (implicit,
54 * plain-FIFO) class. Set at spawn from xtc_proc_opts_t.sched_class
55 * or xtc_proc_set_class; survives a work-steal (a stolen task keeps
56 * its tag and is placed into the same class index on the thief).
57 * -1 by default, so a loop with no classes created is byte-for-byte
58 * the old FIFO+deque path. INSPIRED BY Glommio's task-queue handle
59 * (executor/mod.rs TaskQueue). */
60 int sched_class;
61 /* Monotonic time (ns) this run quantum was dispatched, recorded
62 * by the scheduler when loop->yield_budget_ns > 0. xtc_yield_check
63 * compares against it; 0 means not yet recorded this quantum. */
64 int64_t run_start_ns;
65 /* Pinned tasks run only on their home loop -- they go on the
66 * owner-only FIFO, never the stealable deque. Used for explicit
67 * placement (xtc_exec_spawn_on) and for processes, which keep a
68 * shard-style affinity to one loop. Unpinned tasks (the general
69 * pool) go on the Chase-Lev deque and may be work-stolen. */
70 int pinned;
71 /* Run-queue intrusive next pointer. */
72 struct xtc_task *q_next;
73
74 /* Park bookkeeping. At most one of these is active at a time
75 * while the task is in PARKED state. */
76 /*
77 * The park timer armed for this task, or NULL. ATOMIC for the same
78 * reason park_fd below is: TWO THREADS WRITE IT. The parking fiber
79 * arms it and clears it in its post-park cleanup, and the TIMER-
80 * EXPIRY path clears it from the owning loop's thread
81 * (src/evt/loop.c) -- and it does so AFTER xtc_waker_wake has already
82 * made the task runnable, so the woken fiber can be resumed on
83 * another thread and reach its own cleanup while that store is still
84 * in flight.
85 *
86 * Both concurrent stores write NULL (the non-NULL arm only ever runs
87 * on the fiber itself, before it parks), so no wrong or torn value
88 * was ever observable and this was benign in effect -- but it was an
89 * unsynchronized pointer write on a field a peer thread reads, which
90 * is undefined behavior regardless of the values involved, and it is
91 * indistinguishable to a reader from the case that is NOT benign.
92 * Relaxed ordering is sufficient: the field is a "is a timer armed?"
93 * hint, the timer's own `fired`/`cancelled` flags are the arbiter of
94 * whether a cancel does anything, and the wake itself carries the
95 * happens-before for the task's scheduling state.
96 */
97 _Atomic(xtc_timer_t *) park_timer;
98 /*
99 * The fd this task is parked on, or -1. ATOMIC, and every
100 * unregister CLAIMS it with an exchange rather than testing then
101 * using it.
102 *
103 * Two threads race on this field: the parking fiber (which registers
104 * it, then unregisters in its post-park cleanup) and, for a
105 * MIGRATABLE fiber, a PEER loop's __xtc_loop_dispatch_event, which
106 * clears it when the readiness arrives. As a plain int the parker's
107 *
108 * if (park_fd >= 0) ... del(park_fd)
109 *
110 * was a torn test-then-use: dispatch cleared the field in between, so
111 * the del was issued for -1 and the kqueue registration SURVIVED.
112 * Measured on FreeBSD: 596 leaked registrations in one run, all from
113 * this site, leaving 105 of 115 live fds registered in more than one
114 * kqueue at once (one fd in all TWELVE of a 12-loop executor).
115 * Several loops then watch the same open file and whichever polls
116 * first dispatches the readiness to ITS OWN tag, waking a task that
117 * is not the parker -- so the parker stays blocked on a completion
118 * whose result was already published. That was the
119 * /m5/exec/Blk2_concurrent_commit hang.
120 *
121 * Claiming it with atomic_exchange makes the unregister exactly-once:
122 * whichever side takes the non-negative value owns the del, and the
123 * loser sees -1 and does nothing.
124 */
125 _Atomic int park_fd;
126 /*
127 * The io that holds park_fd's registration. Needed because the two
128 * park entry points register on DIFFERENT ios -- xtc_task_park_on_fd
129 * on self->loop->io (the home loop), xtc_proc_wait_fd on the loop the
130 * fiber is RUNNING on -- so the unregister cannot infer the registry.
131 * Written before park_fd is published and read after it is claimed,
132 * so the claim orders it.
133 */
134 struct xtc_io *park_io;
135 /* Voluntary park: when set by a primitive (e.g. xtc_amutex) just
136 * before yielding, the coro step returns PENDING instead of
137 * RESCHED, so the task sleeps until a waker re-enqueues it rather
138 * than busy-spinning. Read-and-cleared by the step. */
139 int park_requested;
140
141 /* Wakeup-cause flags, set by the dispatcher / timer callback /
142 * mbox_deliver when the task is unparked. Sampled and cleared
143 * by the parker on resume (e.g. xtc_proc_wait_fd). Encodes
144 * XTC_IO_* flags from the dispatched event plus the synthetic
145 * XTC_WAIT_MAILBOX (set by __mbox_deliver) and XTC_WAIT_TIMEOUT
146 * (set by the timer callback). Atomic: a cross-thread xtc_send
147 * ORs XTC_WAIT_MAILBOX in from a FOREIGN thread (proc.c) while the
148 * owning loop ORs/reads/zeros it (task.c/loop.c/proc.c), so a plain
149 * uint32_t RMW here is a data race (TSan-reportable). All accesses
150 * use relaxed atomics -- ordering is provided by the waker/mailbox
151 * lock; the atomic only makes the OR itself race-free. */
152 _Atomic uint32_t wake_revents;
153
154 /* Latched cross-thread wake that arrived while this task was NOT yet
155 * PARKED (the prepare/park race: a foreign xtc_waker_wake fires
156 * between the parker arming its waker and the loop transitioning it
157 * to PARKED on yield). The WAKE-drain sets this instead of dropping
158 * the wake; the RUNNING->PARKED transition consumes it and
159 * re-schedules rather than parking, so the wake is never lost.
160 * Set cross-thread, consumed on the owning loop's thread. */
161 _Atomic int wake_pending;
162
163 /* Doubly linked into loop->all_tasks so a completed task can be
164 * unlinked in O(1) and recycled to the loop's task_slab (instead of
165 * lingering until loop_fini). all_prev == NULL means the head. */
166 struct xtc_task *all_next;
167 struct xtc_task *all_prev;
168
169 /* 1 if this task struct is eligible to be recycled onto the loop's
170 * task free-list when it completes (a plain task on its home loop);
171 * cleared for tasks that must not be recycled. All task structs
172 * are __os_calloc'd regardless -- this only gates the free-list
173 * push, not the allocation source. */
174 int recyclable;
175
176 /* Optional cleanup hook invoked by xtc_loop_fini before the task
177 * struct is freed. The coroutine layer sets this to release the
178 * fiber stack + coro struct that wrap a task; plain tasks leave
179 * it NULL. Keeps task lifetime owned by the loop while letting
180 * higher layers reclaim what they attached. */
181 void (*cleanup)(void *cleanup_arg);
182 void *cleanup_arg;
183
184 /* --- sched (fix155): intrusive cross-loop inbox link ---
185 * A task enters a loop inbox only as SCHEDULED-and-unqueued: a
186 * cross-thread spawn (PUBLISH) or the waker that won the
187 * PARKED->SCHEDULED transition (WAKE). It leaves when that loop
188 * drains it. Nothing else can queue it in between (every other wake
189 * loses the transition and latches wake_pending) and it cannot reach
190 * DONE before it runs, so one link field is enough and the inbox
191 * needs no allocation and cannot fail. */
192 struct xtc_task *inbox_next;
193
194 /* --- lifetime (fix155): reclaim at DONE ---
195 * reclaim: set by the coro step when a PROC fiber finishes (a proc's
196 * task handle is never handed out), so the loop releases its stack,
197 * coro and task at DONE instead of at loop_fini. dead_next links a
198 * task that finished on a thief onto its home loop's dead stack.
199 * Reclaimed task structs are never returned to the allocator while
200 * the loop lives (see __xtc_task_reclaim): a late waker always lands
201 * on a valid task. */
202 int reclaim;
203 int typestable; /* once reclaimed: never freed before fini */
204 struct xtc_task *dead_next;
205};
206
207/*
208 * Timer record. Kept in a binary min-heap inside the loop: an array of
209 * {deadline, node} slots (struct xtc_timer_slot), so a sift compares
210 * keys in the array instead of dereferencing two scattered nodes per
211 * level. heap_idx is the node's slot.
212 *
213 * `fired` decides a cancel racing a fire: it leaves 0 exactly once, to
214 * XTC_TIMER_FIRED (the owner's pop claimed it) or XTC_TIMER_CANCELLED.
215 * A cancel on the loop's own thread removes the node from the heap at
216 * once (O(log n), by heap_idx). A cancel from any other thread may not
217 * touch the heap: it only marks the node, and the owner drops it when it
218 * reaches the root or when a full heap is swept before growing
219 * (timer.c). So the heap is bounded by the live timers, and a non-empty
220 * heap whose cancelled root has been popped (what
221 * __xtc_timer_heap_next_deadline does) holds a live timer.
222 */
223#define XTC_TIMER_FIRED 1
224#define XTC_TIMER_CANCELLED 2
226 int64_t deadline_ns; /* == t->deadline_ns while queued */
227 struct xtc_timer *t;
228};
229struct xtc_timer {
230 int64_t deadline_ns;
231 xtc_timer_fn cb;
232 void *user;
233 xtc_task_t *waiter; /* task to wake when fired (NULL if pure cb) */
234 int heap_idx; /* current position in heap (-1 if not in);
235 * owner-only */
236 _Atomic int cancelled; /* 1 once a cancel won (fired == CANCELLED) */
237 _Atomic int fired; /* 0, XTC_TIMER_FIRED or _CANCELLED */
238 int sim_late; /* DST: 1 once a buggify late-fire bumped
239 * this timer's deadline (bump at most
240 * once so a late fire cannot spin);
241 * always 0 outside sim. */
242 int from_slab; /* 1: from loop->timer_slab (xtc_timer_set);
243 * 0: __os_calloc (the park timers of
244 * xtc_task_park_on_timer / wait_fd). */
245 _Atomic int refs; /* owners left: the heap, plus the task's
246 * park_timer slot (park timers) or the
247 * caller's handle (xtc_timer_set, until
248 * xtc_timer_release). The last to release
249 * frees the node. Either owner may run on a
250 * different thread (a517a32), so atomic. */
251 xtc_loop_t *loop; /* back-pointer for cancel-by-handle */
252 struct xtc_timer *all_next; /* per-loop linked list for cleanup */
253};
254
255/*
256 * Inbox message kinds. All inbox traffic is cross-thread; the loop
257 * owner drains its inbox at the top of every step.
258 */
259enum xtc_inbox_kind {
260 XTC_INB_WAKE = 0, /* re-queue a parked task */
261 XTC_INB_PUBLISH = 1, /* publish a freshly-allocated task */
262};
263
264/*
265 * Lock-free MPSC inbox: producers push a task onto a LIFO stack with a
266 * CAS (no allocation, no lock); the owner takes the whole stack with one
267 * exchange and processes it in arrival order. PUBLISH and WAKE use
268 * separate stacks so loop_fini can reclaim unpublished tasks without
269 * touching WAKE entries, whose tasks the all_tasks walk already freed.
270 */
271struct xtc_inbox {
272 _Atomic(struct xtc_task *) pub_head;
273 _Atomic(struct xtc_task *) wake_head;
274 /* 1 while the owner is in (or entering) a BLOCKING poll: a producer
275 * writes the loop's wakeup fd only then (see __xtc_inbox_push and
276 * __xtc_inbox_sleep_begin). On the heads' line on purpose: a
277 * producer has just CASed it, so the read is free. */
278 _Atomic int sleeping;
279 int inited;
280};
281
282/*
283 * L1 proportional-share (weighted-fair) run class. INSPIRED BY Glommio
284 * (Glauber Costa / ScyllaDB): Glommio's executor keeps a set of task
285 * queues, each with SHARES (1..1000) and a virtual runtime, and a
286 * min-vruntime pick gives each queue a weighted CPU fraction
287 * (executor/mod.rs TaskQueue + account_vruntime; shares.rs
288 * reciprocal_shares).
289 *
290 * A class is a per-loop overlay on the ready set: it carries its own
291 * ready FIFO (q_head/q_tail through the task's q_next) alongside the
292 * class's shares, virtual runtime, and the precomputed reciprocal used
293 * by the account formula. The default (implicit) class is index -1 on
294 * a task and is NOT one of these entries -- it uses the loop's plain
295 * q_head/q_tail + deque, so a loop with zero classes runs the exact old
296 * path with zero overhead. The vruntime pick activates only once a
297 * class has been created on the loop (loop->n_classes > 0).
298 */
300 int shares; /* 1..1000 (Glommio's range) */
301 int64_t latency_ns; /* optional latency bound; 0 = none */
302 uint64_t reciprocal; /* (1<<22)/shares, precomputed */
303 uint64_t vruntime; /* accumulated weighted runtime */
304 uint64_t runs; /* times a task from this class was run
305 * (telemetry / CPU-share observability) */
306 struct xtc_task *q_head; /* this class's ready FIFO */
307 struct xtc_task *q_tail;
308 int in_use; /* 1 once created */
309};
310
311/* Max scheduling classes per loop. Glommio apps use a handful of task
312 * queues; a small fixed array keeps __queue_pop a tiny linear scan (no
313 * heap, no alloc on the dispatch path). Creating more than this many
314 * classes on one loop returns XTC_E_AGAIN. */
315#define XTC_LOOP_MAX_CLASSES 16
316
317/* Shares weight of the implicit default lane (untagged tasks) when it
318 * races the explicit classes in the min-vruntime pick. Chosen at the
319 * top of Glommio's 1..1000 range so untagged/background work gets a
320 * fair-but-not-dominant slice against a small set of weighted classes;
321 * a class must be created with shares > this to out-run the default. */
322#define XTC_DEFAULT_CLASS_SHARES 100
323#define XTC_DEFAULT_CLASS_RECIP (((uint64_t)1 << 22) / XTC_DEFAULT_CLASS_SHARES)
324
325/* Minimum per-run cost charged to a class's vruntime (reduction-style
326 * floor). Guarantees vruntime always advances so the min-vruntime pick
327 * cannot degenerate -- essential under the deterministic simulator,
328 * where virtual time does not advance within a compute run (elapsed 0).
329 * In production a real run's elapsed exceeds this, so the accounting is
330 * Glommio-faithful (weighted by measured CPU time). */
331#define XTC_VRUNTIME_MIN_QUANTUM_NS 1000
332
333#ifndef XTC_LOOP_STACK_SPARE
334#define XTC_LOOP_STACK_SPARE 64
335#endif
336
337struct xtc_loop {
338#ifdef XTC_ENABLE_SCHED_POLICY
339 struct xtc_sched_state sched;
340#endif
341 xtc_io_t *io;
342
343 /* Local run queue (Chase-Lev deque, owner pushes/pops). */
344 xtc_deque_t deque;
345
346 /* Slow-path overflow when the deque is full. Owner-only. */
347 struct xtc_task *q_head;
348 struct xtc_task *q_tail;
349
350 /* Timer min-heap (timer.c). */
351 struct xtc_timer_slot *timers;
352 int n_timers;
353 int cap_timers;
354
355 /* All tasks ever spawned, for cleanup. Owner-only after init. */
356 struct xtc_task *all_tasks;
357
358 /* Unused since 1.55.1 (always NULL): every timer is refcounted, and
359 * timers still held at fini are freed with timer_slab. */
360 xtc_timer_t *all_timers;
361
362 /* M11.5b: per-loop slab cache for xtc_timer_t. Created lazily
363 * by xtc_timer_set; freed in loop_fini. Per-loop = single-
364 * threaded ownership = magazine fast path is lock-free. */
365 struct xtc_slab *timer_slab;
366
367 /* Per-loop task-struct free-list: a plain single-threaded LIFO of
368 * recycled task structs (linked through their q_next while free).
369 * xtc_task_spawn pops from it instead of malloc'ing, and a
370 * completed plain task on its home loop is pushed back instead of
371 * freed -- the spawn-heavy hot path, with no allocator call and no
372 * accumulation. Only the owning loop thread touches it, so it is
373 * lock-free. Drained (structs __os_free'd) at loop_fini. */
374 struct xtc_task *task_free;
375 int task_free_n;
376
377 /* Live-task counter. Atomic so cross-thread spawns/completions
378 * can update it without lock. */
379 _Atomic int n_alive;
380 /* Of n_alive, the unpinned (stealable) ones; same accounting. Its
381 * 0 <-> 1 edges move the executor's n_mig_loops; while that is 0
382 * nothing can be stolen, so a run-queue-empty loop blocks until its
383 * own deadline instead of re-checking its peers every 1 ms
384 * (__xtc_exec_steal_recheck_ns). steal_sleep: set by a loop that
385 * blocked on that basis; any loop's up-edge clears it and wakes the
386 * loop. */
387 _Atomic int n_mig_alive;
388 _Atomic int steal_sleep;
389
390 /* Per-loop work statistics (executor observability). tasks_run
391 * counts task steps executed on this loop; steals counts tasks
392 * this loop successfully stole from a peer. Relaxed atomics:
393 * read-mostly counters, exactness across a concurrent read is
394 * not required. */
395 _Atomic uint64_t n_tasks_run;
396 _Atomic uint64_t n_steals;
397
398 /* I/O fairness: counts task runs since the last I/O poll. When the
399 * run queue never empties (busy-yielding fibers, e.g. a buffer
400 * manager spinning on eviction), the loop would otherwise never
401 * poll I/O and parked completions would starve. Every
402 * IO_FAIRNESS_QUANTUM runs the step does a non-blocking poll. */
403 unsigned int runs_since_poll;
404
405 /* --- sched (fix155): owner-written, kept off the producer-written
406 * inbox line ---
407 * pop_flip alternates the two default lanes (owner FIFO, stealable
408 * deque) when both hold work, so neither starves the other.
409 * n_timers_pub is n_timers as last published by the owner (after
410 * each step), for the executor supervisor's idle check, which must
411 * not read the owner-only n_timers. */
412 unsigned int pop_flip;
413 _Atomic int n_timers_pub;
414
415 /* Cooperative yield watchdog (opt-in). When yield_budget_ns > 0
416 * the scheduler records each quantum's start time on the task and
417 * xtc_yield_check reports a task over budget; n_yield_due counts
418 * over-budget reports (telemetry). */
419 int64_t yield_budget_ns;
420 _Atomic uint64_t n_yield_due;
421
422 /* L1 proportional-share scheduler (opt-in). n_classes == 0 (the
423 * default) means no class was ever created and the scheduler uses
424 * the plain q_head/q_tail FIFO + deque -- byte-for-byte the old
425 * path. Once a class exists, __queue_pop picks the min-vruntime
426 * in-use class and pops its FIFO. Owner-only (no lock): classes
427 * are created and picked only on the loop's own thread. */
428 struct xtc_run_class classes[XTC_LOOP_MAX_CLASSES];
429 int n_classes;
430 /* Virtual runtime of the IMPLICIT default lane (the plain
431 * q_head/q_tail FIFO + deque, for tasks with sched_class == -1).
432 * When classes exist, the default lane races in the same
433 * min-vruntime pick as a class with fixed default shares, so
434 * untagged/background work is never starved by always-ready class
435 * work -- it just gets a default weight. Unused (stays 0) until a
436 * class is created. INSPIRED BY Glommio's default task queue. */
437 uint64_t default_vruntime;
438 /* Effective per-loop latency bound: the smallest latency_ns over
439 * all in-use classes (0 = none). Shrinks the yield/preempt
440 * interval so a latency class is checked often, like Glommio's
441 * reevaluate_preempt_timer. Recomputed on class create. */
442 int64_t class_latency_ns;
443
444 /* L3 over-budget stall watchdog (opt-in, off by default). When
445 * stall_budget_ns > 0 the run-end boundary in __xtc_loop_step
446 * compares the elapsed run time against it and, on an overrun,
447 * invokes stall_cb (or logs). A single branch on stall_budget_ns
448 * == 0 when off, so zero cost unless enabled. INSPIRED BY
449 * Glommio's stall detector (executor/stall.rs). */
450 int64_t stall_budget_ns;
451 void (*stall_cb)(xtc_loop_t *loop, xtc_task_t *task,
452 int64_t ran_ns, int64_t budget_ns,
453 void *user);
454 void *stall_cb_user;
455 _Atomic uint64_t n_stalls; /* over-budget reports (telemetry) */
456
457 /* Set by xtc_loop_stop, read by xtc_loop_run's condition. ATOMIC
458 * because xtc_loop_stop is a cross-thread call by construction: it
459 * pairs the flag with xtc_io_wakeup, whose whole purpose is to nudge
460 * a loop running on ANOTHER thread out of its I/O wait. A plain int
461 * here is a real data race (TSan flags it as soon as any test stops a
462 * loop from a foreign thread), and relaxed ordering is sufficient:
463 * the wakeup's own release/acquire provides the ordering, this flag
464 * only has to be seen eventually and its store must not tear. */
465 _Atomic int stop_requested;
466
467 /* Cross-thread inbox: wakers and remote spawns deposit here;
468 * the owner drains in __xtc_loop_drain_inbox. */
469 struct xtc_inbox inbox;
470
471
472 /* For the multi-loop executor: 0-based index in xtc_exec; -1 if
473 * this loop is standalone (M3 single-thread mode). */
474 int exec_id;
475
476 /* Back-pointer to the executor (NULL if standalone). */
477 struct xtc_exec *exec;
478
479 /*
480 * Resource accountant. Either owned by the loop (allocated and
481 * freed at init/fini) or borrowed from the executor. Tracks
482 * tasks-alive, inbox messages, channels, etc.
483 */
484 xtc_res_t *res;
485 int owns_res;
486
487#if defined(XTC_DIAGNOSTIC)
488 /*
489 * DIAGNOSTIC owner-thread guard. A loop's owner-only structures
490 * (the Chase-Lev deque, the q_head/q_tail slow FIFO, the timer
491 * min-heap + all_timers, the task_slab free-list) must be mutated
492 * ONLY by the OS thread that runs this loop. Four bugs in the
493 * v1.40.1..v1.40.4 arc were exactly a cross-loop mutation of one of
494 * these from a work-stolen fiber resuming on the wrong thread.
495 * owner_tid is recorded when the loop begins running on its thread
496 * (the exec worker, or xtc_loop_run); XTC_ASSERT_LOOP_OWNER aborts
497 * the instant a non-owner touches an owner-only structure, turning
498 * that whole race category from a probabilistic eventual wedge into
499 * a deterministic, immediate, located abort in the first offending
500 * run. Compiled out entirely in a normal build (zero cost). */
501 pthread_t owner_tid;
502 int owner_set;
503#endif
504
505 /* --- lifetime (fix155): reclaimed-task bookkeeping ---
506 * dead_head: lock-free MPSC stack of this loop's tasks that reached
507 * DONE on another loop (pushed by the thief, drained by this loop's
508 * owner). task_free_mig: the free list for reclaimed MIGRATABLE task
509 * structs, kept apart from task_free (pinned) so a stale waker naming
510 * a thief loop can never re-queue a pinned task there. */
511 _Atomic(struct xtc_task *) dead_head;
512 struct xtc_task *task_free_mig;
513
514 /* --- rr3 (fix155): bounded task-struct retention across threads ---
515 * The owner-only free lists hold at most XTC_TASK_FREELIST_LOCAL
516 * structs (task_free_n); the owner pushes the rest onto task_spare,
517 * which ANY thread may pop under task_spare_lock (a spinlock), so a
518 * spawn from another loop reuses this loop's recycled structs instead
519 * of always mallocing. Indexed by pinned (0 migratable, 1 pinned),
520 * kept apart like the free lists. task_spare_n is written under the
521 * lock and read relaxed for the plain-task cap. See __task_alloc. */
522 _Atomic int task_spare_lock;
523 struct xtc_task *task_spare[2];
524 _Atomic int task_spare_n;
525
526 /* --- fu156: fiber stacks shared across threads ---
527 * Stacks of this loop's finished tasks that the finishing thread's
528 * own pool had no room for, handed to the next spawn onto this loop
529 * from any thread (coro_common.h __stack_spare_*). Locked like
530 * task_spare; stack_spare_n is written under the lock and read
531 * relaxed as an empty/full hint. Unmapped at xtc_loop_fini.
532 * stack_spare_want: spawns onto this loop from another thread,
533 * counted up to XTC_LOOP_STACK_SPARE; the spare is filled only once
534 * it gets there, so a loop only its own thread spawns onto (startup
535 * spawns from main aside) keeps exactly the per-thread pool. */
536 _Atomic int stack_spare_want;
537 _Atomic int stack_spare_lock;
538 _Atomic int stack_spare_n;
539 size_t stack_spare_sz;
540 void *stack_spare[XTC_LOOP_STACK_SPARE];
541};
542
543#if defined(XTC_DIAGNOSTIC)
544/*
545 * Abort if the calling thread is not this loop's owner. `site` names the
546 * owner-only structure being mutated, for a legible message. A loop with
547 * no recorded owner yet (owner_set == 0: before it has started running on
548 * a thread, e.g. spawn-time init) is exempt -- there is no concurrent
549 * owner to race. __xtc_current_loop is deliberately NOT used to decide
550 * ownership: it is the fiber's LOGICAL loop binding, preserved across a
551 * work-steal migration, so it does not identify the physical OS thread
552 * (this is the exact trap that caused a regression in the v1.40.3 fix).
553 * pthread_self() is the authoritative physical-thread identity.
554 */
555void __xtc_loop_owner_violation(const struct xtc_loop *loop, const char *site);
556extern XTC_THREAD_LOCAL struct xtc_loop *__xtc_current_loop;
557extern int __xtc_sim_active(void);
558#define XTC_ASSERT_LOOP_OWNER(loop, site) \
559 do { \
560 const struct xtc_loop *_l = (loop); \
561 if (_l == NULL) \
562 break; \
563 if (__xtc_sim_active()) { \
564 /* Single-thread DST: the physical-thread check can \
565 * never fire (one thread drives all loops), but the \
566 * VIOLATION still executes -- a fiber mutating a loop \
567 * other than the one currently being stepped is the \
568 * exact cross-loop bug, just serialized so it cannot \
569 * corrupt. __xtc_current_loop is the loop being \
570 * stepped, i.e. the only legitimate mutation target. \
571 * This makes the whole category DETERMINISTICALLY \
572 * reproducible under a seed. */ \
573 if (__xtc_current_loop != NULL && \
574 __xtc_current_loop != _l) \
575 __xtc_loop_owner_violation(_l, (site)); \
576 } else if (_l->owner_set && \
577 !pthread_equal(pthread_self(), _l->owner_tid)) { \
578 __xtc_loop_owner_violation(_l, (site)); \
579 } \
580 } while (0)
581#else
582#define XTC_ASSERT_LOOP_OWNER(loop, site) ((void)0)
583#endif
584
585/* Internal helpers shared between loop.c, task.c, timer.c. */
586int __xtc_loop_enqueue(xtc_loop_t *loop, xtc_task_t *t);
587/* L1: create a run class on `loop`. shares in 1..1000; latency_ns >= 0
588 * (0 = none). Returns the class index in *out_idx (>= 0), or an error
589 * (XTC_E_INVAL bad args, XTC_E_AGAIN if the per-loop class cap is hit).
590 * Owner-only. */
591int __xtc_loop_class_create(xtc_loop_t *loop, int shares,
592 int64_t latency_ns, int *out_idx);
593/* Spawn with explicit pinned-ness: pinned tasks stay on `loop` (FIFO,
594 * never work-stolen); unpinned tasks may migrate via the deque. */
595int __xtc_task_spawn_ex(xtc_loop_t *loop, xtc_task_fn fn, void *user,
596 int pinned, xtc_task_t **out_task);
597/* lifetime (fix155): reclaim a finished proc fiber (task.c). */
598void __xtc_task_reclaim(xtc_loop_t *loop, xtc_task_t *t);
599void __xtc_task_reclaim_drain(xtc_loop_t *loop);
600/* rr3 (fix155): move the cross-thread spares onto the free lists (fini). */
601void __xtc_task_spare_fini(xtc_loop_t *loop);
602int __xtc_task_spawn_cleanup(xtc_loop_t *loop, xtc_task_fn fn, void *user,
603 int pinned, void (*cleanup)(void *), void *cleanup_arg,
604 xtc_task_t **out_task);
605int __xtc_timer_heap_push(xtc_loop_t *loop, xtc_timer_t *t);
606xtc_timer_t *__xtc_timer_heap_pop_due(xtc_loop_t *loop, int64_t now_ns);
607/*
608 * Drop one reference to a timer and free it at zero. Its two owners --
609 * the timer heap, and the task's park_timer slot or the caller's
610 * xtc_timer_set handle -- each call this exactly once when they let go,
611 * so the node is reclaimed as soon as it has fired/cancelled AND been let
612 * go, not retained until loop_fini. Defined in timer.c.
613 */
614void __xtc_timer_park_release(xtc_timer_t *t);
615/* Cancel; 1 if this call stopped the fire, else 0. Any thread; the
616 * caller holds a reference. Defined in timer.c. */
617int __xtc_timer_try_cancel(xtc_timer_t *t);
618int64_t __xtc_timer_heap_next_deadline(xtc_loop_t *loop);
619void __xtc_task_cancel_park_timer(xtc_task_t *self);
620int __xtc_loop_dispatch_event(xtc_loop_t *loop, xtc_io_event_t *ev);
621
622/*
623 * L2 io_uring ring-pointer preempt seam (INSPIRED BY Glommio's
624 * need_preempt). Real on the io_uring backend (src/io/io_uring.c),
625 * portable no-op stubs elsewhere, so loop.c/exec.c call them
626 * unconditionally. _arm returns XTC_OK only when the ring is now the
627 * preempt trigger (uring backend); _due is the two-load Glommio check
628 * (1 = a preempt slice elapsed); _disarm tears the ring down. */
629int __xtc_io_uring_preempt_arm(xtc_io_t *io, int64_t interval_ns);
630int __xtc_io_uring_preempt_due(xtc_io_t *io);
631void __xtc_io_uring_preempt_disarm(xtc_io_t *io);
632/* The calling thread stops running loop->io's loop (io_int.h). */
633void __xtc_io_owner_leave(xtc_io_t *io);
634/* io_uring only (submission is deferred to the poll): submit the io's
635 * queued SQEs now. Owner thread only; a no-op otherwise. The loop calls
636 * it after a yield and every few runs (io_uring.c __submit_wait). */
637void __xtc_io_flush(xtc_io_t *io);
638
639/* Implemented in proc.c. Called from loop_fini to release the
640 * proc-table side struct hashed against this loop pointer. Must be
641 * idempotent. */
642void __xtc_proc_loop_unregister(xtc_loop_t *loop);
643/* Inbox API. Producer-side functions are thread-safe and cannot fail.
644 * __xtc_inbox_push queues the task and nudges the loop's poller. */
645int __xtc_inbox_init(struct xtc_inbox *ib);
646void __xtc_inbox_fini(struct xtc_inbox *ib);
647int __xtc_inbox_push(xtc_loop_t *loop, enum xtc_inbox_kind k, xtc_task_t *t);
648int __xtc_inbox_nonempty(const struct xtc_inbox *ib);
649int __xtc_inbox_drain(xtc_loop_t *loop); /* owner-only; drains into local queue */
650/* Owner-only bracket around a BLOCKING xtc_io_poll: begin returns 1 if
651 * the inbox already holds work (do not block; the bracket is closed),
652 * else 0 (block, then call end). */
653int __xtc_inbox_sleep_begin(xtc_loop_t *loop);
654void __xtc_inbox_sleep_end(xtc_loop_t *loop);
655
656/* Per-thread cursor: which loop the calling thread is running.
657 * NULL on threads that aren't loop owners. */
658extern XTC_THREAD_LOCAL xtc_loop_t *__xtc_current_loop;
659
660/* The same cursor read out of line on the CURRENT thread. Fiber code
661 * that reads it after xtc_yield (or any call that can switch stacks) must
662 * use this: a migratable fiber can resume on another carrier, and
663 * compilers keep a thread-local's address live across calls, so a direct
664 * read would see the previous thread's loop. */
665xtc_loop_t *__xtc_current_loop_get(void);
666
667/*
668 * Fiber-context preservation hook. The L3 process layer keeps a
669 * per-thread "current proc" pointer that must survive a yield (the
670 * scheduler runs other fibers in between, which overwrite it). The
671 * L2 coro layer cannot depend on L3, so every yield/await jump saves
672 * the opaque context before jumping to the scheduler and restores it
673 * on resume through these hooks. proc.c installs them on first
674 * spawn; while NULL (no process layer in use) the calls are no-ops.
675 * Set once to stable function addresses, so a plain pointer load is
676 * safe without synchronization.
677 */
678extern void *(*__xtc_fiber_ctx_save)(void);
679extern void (*__xtc_fiber_ctx_restore)(void *);
680
681/* Post-resume cancellation hook. Installed by the process layer
682 * (proc.c). Called at the universal fiber resume point (after a yield
683 * returns) so a fiber that had a kill/cancel requested while it was
684 * NOT at a cooperative point -- e.g. a pure CPU loop that was
685 * involuntarily preempted -- honors the kill the instant the scheduler
686 * resumes it, by unwinding via xtc_exit_self. NULL when no process
687 * layer is present (bare coroutine use). Returns without effect if no
688 * kill is pending. */
689extern void (*__xtc_fiber_kill_check)(void);
690
691/* Deliver a pending kill now, at a park site that has undone its own
692 * state (xtc_await, after unregistering as the target's waiter): unwinds
693 * via xtc_exit_self, or latches the kill if the proc is masked and
694 * returns. Installed by proc.c (__xtc_proc_kill_poll); NULL without a
695 * process layer. */
696extern void (*__xtc_fiber_kill_poll)(void);
697
698/* Loop-fini hook. Installed by the process layer (proc.c) on first
699 * spawn; lets xtc_loop_fini release the loop's per-loop proc table
700 * without the L2 loop depending on the L3 proc layer directly (the
701 * loop calls the hook, proc.c points it at __xtc_proc_loop_unregister).
702 * NULL when no process layer is in use, so a bare-coroutine loop has
703 * nothing to clean up. */
704extern void (*__xtc_loop_fini_hook)(xtc_loop_t *loop);
705
706/* Forward declaration for back-pointer in xtc_loop. */
707struct xtc_exec;
708
709#endif /* XTC_LOOP_INT_H */