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