libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_exec.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * @stability stable
4 * Use of this source code is governed by the ISC License.
5 *
6 * src/inc/xtc_exec.h
7 * The L2 multi-loop executor. Owns N xtc_loop instances, each
8 * running on its own OS thread. Tasks may be spawned on any loop
9 * from any thread; cross-thread wakers go through a per-loop MPSC
10 * inbox + xtc_io_wakeup pingback.
11 *
12 * See M5_CLAIMS.md.
13 */
14
15#ifndef XTC_EXEC_H
16#define XTC_EXEC_H
17
18#include "xtc_export.h"
19
20#include <stdint.h>
21
22#include "xtc_loop.h"
23#include "xtc_async.h"
24
25typedef struct xtc_exec xtc_exec_t;
26
27/*
28 * L1 proportional-share (weighted-fair) scheduler handle. INSPIRED BY
29 * Glommio (Glauber Costa / ScyllaDB): a scheduling CLASS carries SHARES
30 * (1..1000) and an optional LATENCY bound, and a CFS-style vruntime
31 * pick gives each class a weighted CPU fraction on its loop. Opaque:
32 * created by xtc_exec_class_create, placed on a proc via
33 * xtc_proc_opts_t.sched_class or xtc_proc_set_class. A NULL handle
34 * (the default) means the implicit plain-FIFO class -- zero overhead
35 * until at least one class exists on a loop.
36 */
37typedef struct xtc_run_class *xtc_exec_class_t;
38
39/*
40 * PUBLIC: int xtc_exec_init __P((xtc_exec_t **, int));
41 * PUBLIC: int xtc_exec_fini __P((xtc_exec_t *));
42 * PUBLIC: int xtc_exec_run __P((xtc_exec_t *));
43 * PUBLIC: void xtc_exec_set_service_mode __P((xtc_exec_t *, int));
44 * PUBLIC: int xtc_exec_get_service_mode __P((xtc_exec_t *));
45 * PUBLIC: void xtc_exec_set_eager_rebalance __P((xtc_exec_t *, int));
46 * PUBLIC: int xtc_exec_get_eager_rebalance __P((xtc_exec_t *));
47 * PUBLIC: void xtc_exec_set_steal_backoff __P((xtc_exec_t *, int));
48 * PUBLIC: int xtc_exec_get_steal_backoff __P((xtc_exec_t *));
49 * PUBLIC: int xtc_exec_stop __P((xtc_exec_t *));
50 * PUBLIC: int xtc_exec_n_loops __P((xtc_exec_t *));
51 * PUBLIC: int xtc_exec_loop_id __P((void));
52 * PUBLIC: int xtc_shard_id __P((void));
53 * PUBLIC: int xtc_shard_count __P((void));
54 * PUBLIC: xtc_loop_t *xtc_exec_loop __P((xtc_exec_t *, int));
55 *
56 * PUBLIC: int xtc_exec_spawn __P((xtc_exec_t *, xtc_task_fn, void *, xtc_task_t **));
57 * PUBLIC: int xtc_exec_spawn_on __P((xtc_exec_t *, int, xtc_task_fn, void *, xtc_task_t **));
58 * PUBLIC: int xtc_exec_async __P((xtc_exec_t *, xtc_coro_fn, void *, xtc_task_t **));
59 * PUBLIC: int xtc_exec_async_on __P((xtc_exec_t *, int, xtc_coro_fn, void *, xtc_task_t **));
60 */
61
62/*
63 * Lifecycle. n_loops <= 0 selects __os_ncpus().
64 *
65 * `xtc_exec_run` blocks the calling thread until the executor stops:
66 * - all spawned tasks DONE and all timers fired/cancelled, or
67 * - xtc_exec_stop() called from any thread.
68 *
69 * On return all worker threads have been joined.
70 */
71XTC_API int xtc_exec_init(xtc_exec_t **out, int n_loops);
72XTC_API int xtc_exec_fini(xtc_exec_t *exec);
73XTC_API int xtc_exec_run(xtc_exec_t *exec);
74
75/*
76 * POLICY CONVENTION for every per-xtc_exec_t scheduler knob below
77 * (service mode, eager rebalance, and any future addition -- steal
78 * policy, NUMA affinity mode, migration eagerness, ...):
79 *
80 * - Named xtc_exec_set_<name> / xtc_exec_get_<name>, boolean knobs
81 * take/return a plain `int` (0/1).
82 * - Takes effect immediately; no xtc_exec_init/_fini needed to
83 * change it, and it is safe to call from any thread at any time
84 * (each knob is a single atomic field on the exec).
85 * - The doc comment states not just the EFFECT but the COST: a
86 * policy that trades CPU/syscalls for responsiveness (like eager
87 * rebalance) says so, so a caller can judge whether to opt in.
88 * - Distinct from xtc_proc_opts_t.migratable, which is a per-PROC
89 * ELIGIBILITY bit decided once at spawn (can this proc's coro be
90 * moved at all), not an executor-wide policy (how hard the
91 * executor tries to move eligible work). Keeping eligibility and
92 * policy as separate knobs means a consumer who wants some procs
93 * movable does not have to pay an executor-wide policy's runtime
94 * cost, and vice versa.
95 * - Distinct from xtc_preempt_set_involuntary, which is a
96 * PROCESS-WIDE toggle (no xtc_exec_t argument) governing the
97 * signal-context preemption redirect, not scoped to one executor.
98 *
99 * xtc_cfg (xtc_cfg.h) is a separate, PROCESS-GLOBAL named-tunable
100 * registry (log levels, backpressure thresholds, ops-facing settings
101 * meant to survive a config-file reload) with no per-instance scoping
102 * yet, so it is not used to back a per-exec knob like these; the two
103 * mechanisms serve different scopes on purpose.
104 */
105
106/* Service mode: when set, xtc_exec_run does not idle-auto-stop and runs
107 * until xtc_exec_stop is called. Used by a supervised xtc_app, which is
108 * a long-running service rather than a finite work pool. Default 0. */
109XTC_API void xtc_exec_set_service_mode(xtc_exec_t *exec, int on);
110XTC_API int xtc_exec_get_service_mode(xtc_exec_t *exec);
111
112/*
113 * Eager work-stealing rebalance (OFF by default).
114 *
115 * Every executor loop whose run queue is empty steals a peer's
116 * migratable (xtc_proc_opts_t.migratable) work before it blocks in the
117 * poller, whether or not it owns parked fibers or pending timers, and
118 * while its run queue stays empty it bounds each blocking poll to 1 ms
119 * (32 ms with steal backoff) and looks again. (Up to 1.55.0 a loop with
120 * any pending timer never stole unless eager rebalance was on.)
121 *
122 * Eager rebalance adds one thing: enqueuing a migratable task nudges one
123 * idle peer loop (via the poller wakeup) so it steals at once instead of
124 * on its next tick. This trades a cross-thread wakeup per enqueue (when
125 * a peer is idle) and some extra migration (with its cache/NUMA cost)
126 * for lower rebalance latency. A server whose migratable backends park
127 * on sockets opts in; a cache-locality-bound workload leaves it off.
128 * Only migratable tasks are ever moved; pinned work (the default) is
129 * unaffected either way.
130 */
131XTC_API void xtc_exec_set_eager_rebalance(xtc_exec_t *exec, int on);
132XTC_API int xtc_exec_get_eager_rebalance(xtc_exec_t *exec);
133XTC_API void xtc_exec_set_steal_backoff(xtc_exec_t *exec, int on);
134XTC_API int xtc_exec_get_steal_backoff(xtc_exec_t *exec);
135
136int xtc_exec_set_preempt(xtc_exec_t *exec, int64_t interval_ns);
137XTC_API int xtc_exec_stop(xtc_exec_t *exec);
138
139XTC_API int xtc_exec_n_loops(xtc_exec_t *exec);
140
141/*
142 * From inside a task running on a loop, returns that loop's
143 * 0-based index. From any other thread returns -1. Tests use this
144 * to verify cross-loop spawn placement and steals.
145 */
146XTC_API int xtc_exec_loop_id(void);
147
148/* Seastar-style per-shard API. xtc_shard_id() is the 0-based index of
149 * the loop the caller runs on (a standalone loop is shard 0 of 1; -1
150 * off a loop); xtc_shard_count() is the number of shards (1 for a
151 * standalone loop, 0 off a loop). Index per-core state with these
152 * for a shared-nothing design. */
153XTC_API int xtc_shard_id(void);
154XTC_API int xtc_shard_count(void);
155
156/* Borrow a loop pointer (for tests; not generally needed). */
157XTC_API xtc_loop_t *xtc_exec_loop(xtc_exec_t *exec, int idx);
158
159/*
160 * Per-loop work statistics, for observability and load-balance
161 * diagnosis (e.g. confirming work stealing is distributing under a
162 * tail-latency-sensitive workload).
163 *
164 * tasks_run -- task steps executed on this loop
165 * steals -- tasks this loop successfully stole from a peer
166 *
167 * Reads are lock-free snapshots of relaxed atomics; exactness across
168 * a concurrently running executor is not guaranteed.
169 */
170typedef struct xtc_loop_stats {
171 size_t size; /* sizeof(xtc_loop_stats_t): XTC_LOOP_STATS_INIT */
172 uint64_t tasks_run;
173 uint64_t steals;
175
176#define XTC_LOOP_STATS_INIT { .size = sizeof(xtc_loop_stats_t) }
177
178/*
179 * PUBLIC: int xtc_exec_loop_stats __P((xtc_exec_t *, int, xtc_loop_stats_t *));
180 */
181XTC_API int xtc_exec_loop_stats(xtc_exec_t *exec, int idx, xtc_loop_stats_t *out);
182
183/*
184 * Spawns. These are the multi-loop equivalents of xtc_task_spawn /
185 * xtc_async; they may be called from any thread.
186 */
187XTC_API int xtc_exec_spawn(xtc_exec_t *exec, xtc_task_fn fn, void *user,
188 xtc_task_t **out_task);
189XTC_API int xtc_exec_spawn_on(xtc_exec_t *exec, int loop_idx,
190 xtc_task_fn fn, void *user, xtc_task_t **out_task);
191XTC_API int xtc_exec_async(xtc_exec_t *exec, xtc_coro_fn fn, void *arg,
192 xtc_task_t **out_task);
193XTC_API int xtc_exec_async_on(xtc_exec_t *exec, int loop_idx,
194 xtc_coro_fn fn, void *arg, xtc_task_t **out_task);
195
196/*
197 * L1 -- OPT-IN PROPORTIONAL-SHARE (WEIGHTED-FAIR) SCHEDULER.
198 *
199 * INSPIRED BY Glommio (Glauber Costa / ScyllaDB): Glommio's executor
200 * gives each task queue SHARES (1..1000) and a Latency class, and a
201 * CFS-style min-vruntime scheduler (account_vruntime: delta *
202 * reciprocal_shares >> 12, reciprocal_shares = (1<<22)/shares) hands
203 * each queue a weighted CPU fraction while a Latency::Matters(d) class
204 * shrinks the preempt interval so it is checked within d. This is the
205 * one capability Glommio had that libxtc lacked.
206 *
207 * OFF BY DEFAULT WITH ZERO OVERHEAD: a loop with no class created runs
208 * the exact plain-FIFO + work-stealing-deque path, byte-for-byte. The
209 * vruntime pick activates only once a class exists on a loop.
210 *
211 * xtc_exec_class_create tags a scheduling class on `loop` with `shares`
212 * (1..1000) and an optional `latency_ns` bound (0 = none; a non-zero
213 * bound shrinks the loop's effective yield/preempt interval so the
214 * class is serviced promptly). Returns the handle in *out. A proc is
215 * placed in a class via xtc_proc_opts_t.sched_class at spawn, or via
216 * xtc_proc_set_class from inside the proc. Returns XTC_E_INVAL on bad
217 * args, XTC_E_AGAIN if the per-loop class cap is reached.
218 *
219 * xtc_exec_class_shares / _latency read a handle's parameters back
220 * (0 on a NULL handle).
221 *
222 * PUBLIC: int xtc_exec_class_create __P((xtc_loop_t *, int, int64_t, xtc_exec_class_t *));
223 * PUBLIC: int xtc_exec_class_shares __P((xtc_exec_class_t));
224 * PUBLIC: int64_t xtc_exec_class_latency __P((xtc_exec_class_t));
225 * PUBLIC: uint64_t xtc_exec_class_runs __P((xtc_exec_class_t));
226 * PUBLIC: uint64_t xtc_exec_class_vruntime __P((xtc_exec_class_t));
227 */
228XTC_API int xtc_exec_class_create(xtc_loop_t *loop, int shares,
229 int64_t latency_ns, xtc_exec_class_t *out);
230XTC_API int xtc_exec_class_shares(xtc_exec_class_t cls);
231XTC_API int64_t xtc_exec_class_latency(xtc_exec_class_t cls);
232XTC_API uint64_t xtc_exec_class_runs(xtc_exec_class_t cls);
233XTC_API uint64_t xtc_exec_class_vruntime(xtc_exec_class_t cls);
234
235/*
236 * L3 -- OVER-BUDGET STALL WATCHDOG (opt-in, off by default).
237 *
238 * INSPIRED BY Glommio's stall detector (executor/stall.rs): when a
239 * single task run exceeds a budget the runtime reports WHICH code
240 * monopolized the core. libxtc does it with a cheap in-loop
241 * wall-clock check at the run-end boundary -- no watcher thread, no
242 * signal -- so it is a single branch on a disabled flag when off.
243 *
244 * The report callback shape: `loop` and `task` name where it happened,
245 * `ran_ns` is how long the run took, `budget_ns` the configured budget.
246 * When no callback is set the runtime logs a WARN line and emits a
247 * backtrace of the loop to stderr.
248 *
249 * xtc_loop_set_stall_budget arms the per-loop budget (0 = disable).
250 * xtc_loop_set_stall_cb installs the report sink (NULL = log default).
251 * xtc_exec_set_stall_budget is the convenience that arms every loop of
252 * an executor at once. xtc_loop_stall_count reads the over-budget
253 * report count (telemetry).
254 *
255 * PUBLIC: void xtc_loop_set_stall_budget __P((xtc_loop_t *, int64_t));
256 * PUBLIC: void xtc_loop_set_stall_cb __P((xtc_loop_t *, xtc_stall_cb, void *));
257 * PUBLIC: void xtc_exec_set_stall_budget __P((xtc_exec_t *, int64_t));
258 * PUBLIC: uint64_t xtc_loop_stall_count __P((const xtc_loop_t *));
259 */
260typedef void (*xtc_stall_cb)(xtc_loop_t *loop, xtc_task_t *task,
261 int64_t ran_ns, int64_t budget_ns, void *user);
262XTC_API void xtc_loop_set_stall_budget(xtc_loop_t *loop, int64_t budget_ns);
263XTC_API void xtc_loop_set_stall_cb(xtc_loop_t *loop, xtc_stall_cb cb,
264 void *user);
265XTC_API void xtc_exec_set_stall_budget(xtc_exec_t *exec, int64_t budget_ns);
266XTC_API uint64_t xtc_loop_stall_count(const xtc_loop_t *loop);
267
268#endif /* XTC_EXEC_H */