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 int state;
45 /* Monotonic time (ns) this run quantum was dispatched, recorded
46 * by the scheduler when loop->yield_budget_ns > 0. xtc_yield_check
47 * compares against it; 0 means not yet recorded this quantum. */
48 int64_t run_start_ns;
49 /* Pinned tasks run only on their home loop -- they go on the
50 * owner-only FIFO, never the stealable deque. Used for explicit
51 * placement (xtc_exec_spawn_on) and for processes, which keep a
52 * shard-style affinity to one loop. Unpinned tasks (the general
53 * pool) go on the Chase-Lev deque and may be work-stolen. */
54 int pinned;
55 /* Run-queue intrusive next pointer. */
56 struct xtc_task *q_next;
57
58 /* Park bookkeeping. At most one of these is active at a time
59 * while the task is in PARKED state. */
60 xtc_timer_t *park_timer;
61 int park_fd; /* -1 when not parked on fd */
62 /* Voluntary park: when set by a primitive (e.g. xtc_amutex) just
63 * before yielding, the coro step returns PENDING instead of
64 * RESCHED, so the task sleeps until a waker re-enqueues it rather
65 * than busy-spinning. Read-and-cleared by the step. */
66 int park_requested;
67
68 /* Wakeup-cause flags, set by the dispatcher / timer callback /
69 * mbox_deliver when the task is unparked. Sampled and cleared
70 * by the parker on resume (e.g. xtc_proc_wait_fd). Encodes
71 * XTC_IO_* flags from the dispatched event plus the synthetic
72 * XTC_WAIT_MAILBOX (set by __mbox_deliver) and XTC_WAIT_TIMEOUT
73 * (set by the timer callback). Atomic: a cross-thread xtc_send
74 * ORs XTC_WAIT_MAILBOX in from a FOREIGN thread (proc.c) while the
75 * owning loop ORs/reads/zeros it (task.c/loop.c/proc.c), so a plain
76 * uint32_t RMW here is a data race (TSan-reportable). All accesses
77 * use relaxed atomics -- ordering is provided by the waker/mailbox
78 * lock; the atomic only makes the OR itself race-free. */
79 _Atomic uint32_t wake_revents;
80
81 /* Latched cross-thread wake that arrived while this task was NOT yet
82 * PARKED (the prepare/park race: a foreign xtc_waker_wake fires
83 * between the parker arming its waker and the loop transitioning it
84 * to PARKED on yield). The WAKE-drain sets this instead of dropping
85 * the wake; the RUNNING->PARKED transition consumes it and
86 * re-schedules rather than parking, so the wake is never lost.
87 * Set cross-thread, consumed on the owning loop's thread. */
88 _Atomic int wake_pending;
89
90 /* Doubly linked into loop->all_tasks so a completed task can be
91 * unlinked in O(1) and recycled to the loop's task_slab (instead of
92 * lingering until loop_fini). all_prev == NULL means the head. */
93 struct xtc_task *all_next;
94 struct xtc_task *all_prev;
95
96 /* 1 if this task struct is eligible to be recycled onto the loop's
97 * task free-list when it completes (a plain task on its home loop);
98 * cleared for tasks that must not be recycled. All task structs
99 * are __os_calloc'd regardless -- this only gates the free-list
100 * push, not the allocation source. */
101 int recyclable;
102
103 /* Optional cleanup hook invoked by xtc_loop_fini before the task
104 * struct is freed. The coroutine layer sets this to release the
105 * fiber stack + coro struct that wrap a task; plain tasks leave
106 * it NULL. Keeps task lifetime owned by the loop while letting
107 * higher layers reclaim what they attached. */
108 void (*cleanup)(void *cleanup_arg);
109 void *cleanup_arg;
110};
111
112/*
113 * Timer record. Kept in a binary min-heap inside the loop.
114 *
115 * Cancel is lazy: we mark cancelled and skip on pop. Cancel is O(1)
116 * by cost; the heap may carry up to N stale entries until the next
117 * extraction reaches them. For M3 this is good enough; M5 may
118 * upgrade to a hierarchical wheel.
119 */
120struct xtc_timer {
121 int64_t deadline_ns;
122 xtc_timer_fn cb;
123 void *user;
124 xtc_task_t *waiter; /* task to wake when fired (NULL if pure cb) */
125 int heap_idx; /* current position in heap (-1 if not in) */
126 int cancelled;
127 int fired;
128 int sim_late; /* DST: 1 once a buggify late-fire bumped
129 * this timer's deadline (bump at most
130 * once so a late fire cannot spin);
131 * always 0 outside sim. */
132 xtc_loop_t *loop; /* back-pointer for cancel-by-handle */
133 struct xtc_timer *all_next; /* per-loop linked list for cleanup */
134};
135
136/*
137 * Inbox message kinds. All inbox traffic is cross-thread; the loop
138 * owner drains its inbox at the top of every step.
139 */
140enum xtc_inbox_kind {
141 XTC_INB_WAKE = 0, /* re-queue a parked task */
142 XTC_INB_PUBLISH = 1, /* publish a freshly-allocated task */
143};
144
146 enum xtc_inbox_kind kind;
147 xtc_task_t *task;
148 struct xtc_inbox_msg *next;
149};
150
151struct xtc_inbox {
152 __os_mutex_t lock;
153 struct xtc_inbox_msg *head;
154 struct xtc_inbox_msg *tail;
155 int inited;
156};
157
158struct xtc_loop {
159 xtc_io_t *io;
160
161 /* Local run queue (Chase-Lev deque, owner pushes/pops). */
162 xtc_deque_t deque;
163
164 /* Slow-path overflow when the deque is full. Owner-only. */
165 struct xtc_task *q_head;
166 struct xtc_task *q_tail;
167
168 /* Timer min-heap. */
169 xtc_timer_t **timers;
170 int n_timers;
171 int cap_timers;
172
173 /* All tasks ever spawned, for cleanup. Owner-only after init. */
174 struct xtc_task *all_tasks;
175
176 /* All timers ever created, for cleanup at fini. */
177 xtc_timer_t *all_timers;
178
179 /* M11.5b: per-loop slab cache for xtc_timer_t. Created lazily
180 * by xtc_timer_set; freed in loop_fini. Per-loop = single-
181 * threaded ownership = magazine fast path is lock-free. */
182 struct xtc_slab *timer_slab;
183
184 /* Per-loop task-struct free-list: a plain single-threaded LIFO of
185 * recycled task structs (linked through their q_next while free).
186 * xtc_task_spawn pops from it instead of malloc'ing, and a
187 * completed plain task on its home loop is pushed back instead of
188 * freed -- the spawn-heavy hot path, with no allocator call and no
189 * accumulation. Only the owning loop thread touches it, so it is
190 * lock-free. Drained (structs __os_free'd) at loop_fini. */
191 struct xtc_task *task_free;
192 int task_free_n;
193
194 /* Live-task counter. Atomic so cross-thread spawns/completions
195 * can update it without lock. */
196 _Atomic int n_alive;
197
198 /* Per-loop work statistics (executor observability). tasks_run
199 * counts task steps executed on this loop; steals counts tasks
200 * this loop successfully stole from a peer. Relaxed atomics:
201 * read-mostly counters, exactness across a concurrent read is
202 * not required. */
203 _Atomic uint64_t n_tasks_run;
204 _Atomic uint64_t n_steals;
205
206 /* I/O fairness: counts task runs since the last I/O poll. When the
207 * run queue never empties (busy-yielding fibers, e.g. a buffer
208 * manager spinning on eviction), the loop would otherwise never
209 * poll I/O and parked completions would starve. Every
210 * IO_FAIRNESS_QUANTUM runs the step does a non-blocking poll. */
211 unsigned int runs_since_poll;
212
213 /* Cooperative yield watchdog (opt-in). When yield_budget_ns > 0
214 * the scheduler records each quantum's start time on the task and
215 * xtc_yield_check reports a task over budget; n_yield_due counts
216 * over-budget reports (telemetry). */
217 int64_t yield_budget_ns;
218 _Atomic uint64_t n_yield_due;
219
220 int stop_requested;
221
222 /* Cross-thread inbox: wakers and remote spawns deposit here;
223 * the owner drains in __xtc_loop_drain_inbox. */
224 struct xtc_inbox inbox;
225
226 /* For the multi-loop executor: 0-based index in xtc_exec; -1 if
227 * this loop is standalone (M3 single-thread mode). */
228 int exec_id;
229
230 /* Back-pointer to the executor (NULL if standalone). */
231 struct xtc_exec *exec;
232
233 /*
234 * Resource accountant. Either owned by the loop (allocated and
235 * freed at init/fini) or borrowed from the executor. Tracks
236 * tasks-alive, inbox messages, channels, etc.
237 */
238 xtc_res_t *res;
239 int owns_res;
240};
241
242/* Internal helpers shared between loop.c, task.c, timer.c. */
243int __xtc_loop_enqueue(xtc_loop_t *loop, xtc_task_t *t);
244/* Spawn with explicit pinned-ness: pinned tasks stay on `loop` (FIFO,
245 * never work-stolen); unpinned tasks may migrate via the deque. */
246int __xtc_task_spawn_ex(xtc_loop_t *loop, xtc_task_fn fn, void *user,
247 int pinned, xtc_task_t **out_task);
248int __xtc_timer_heap_push(xtc_loop_t *loop, xtc_timer_t *t);
249xtc_timer_t *__xtc_timer_heap_pop_due(xtc_loop_t *loop, int64_t now_ns);
250int64_t __xtc_timer_heap_next_deadline(xtc_loop_t *loop);
251void __xtc_task_cancel_park_timer(xtc_task_t *self);
252int __xtc_loop_dispatch_event(xtc_loop_t *loop, xtc_io_event_t *ev);
253
254/* Implemented in proc.c. Called from loop_fini to release the
255 * proc-table side struct hashed against this loop pointer. Must be
256 * idempotent. */
257void __xtc_proc_loop_unregister(xtc_loop_t *loop);
258/* Inbox API. Producer-side functions are thread-safe. */
259int __xtc_inbox_init(struct xtc_inbox *ib);
260void __xtc_inbox_fini(struct xtc_inbox *ib);
261int __xtc_inbox_push(struct xtc_inbox *ib, enum xtc_inbox_kind k, xtc_task_t *t);
262int __xtc_inbox_drain(xtc_loop_t *loop); /* owner-only; drains into local queue */
263
264/* Per-thread cursor: which loop the calling thread is running.
265 * NULL on threads that aren't loop owners. */
266extern XTC_THREAD_LOCAL xtc_loop_t *__xtc_current_loop;
267
268/*
269 * Fiber-context preservation hook. The L3 process layer keeps a
270 * per-thread "current proc" pointer that must survive a yield (the
271 * scheduler runs other fibers in between, which overwrite it). The
272 * L2 coro layer cannot depend on L3, so every yield/await jump saves
273 * the opaque context before jumping to the scheduler and restores it
274 * on resume through these hooks. proc.c installs them on first
275 * spawn; while NULL (no process layer in use) the calls are no-ops.
276 * Set once to stable function addresses, so a plain pointer load is
277 * safe without synchronization.
278 */
279extern void *(*__xtc_fiber_ctx_save)(void);
280extern void (*__xtc_fiber_ctx_restore)(void *);
281
282/* Post-resume cancellation hook. Installed by the process layer
283 * (proc.c). Called at the universal fiber resume point (after a yield
284 * returns) so a fiber that had a kill/cancel requested while it was
285 * NOT at a cooperative point -- e.g. a pure CPU loop that was
286 * involuntarily preempted -- honors the kill the instant the scheduler
287 * resumes it, by unwinding via xtc_exit_self. NULL when no process
288 * layer is present (bare coroutine use). Returns without effect if no
289 * kill is pending. */
290extern void (*__xtc_fiber_kill_check)(void);
291
292/* Loop-fini hook. Installed by the process layer (proc.c) on first
293 * spawn; lets xtc_loop_fini release the loop's per-loop proc table
294 * without the L2 loop depending on the L3 proc layer directly (the
295 * loop calls the hook, proc.c points it at __xtc_proc_loop_unregister).
296 * NULL when no process layer is in use, so a bare-coroutine loop has
297 * nothing to clean up. */
298extern void (*__xtc_loop_fini_hook)(xtc_loop_t *loop);
299
300/* Forward declaration for back-pointer in xtc_loop. */
301struct xtc_exec;
302
303#endif /* XTC_LOOP_INT_H */