libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_mctx.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_mctx.h
7 * Memory contexts: hierarchical allocation pools with parent-
8 * tracked lifetime and bulk reset/destroy.
9 *
10 * A context owns:
11 * - all allocations made within it (chained on a free list);
12 * - zero or more child contexts (siblings of each other);
13 * - optional "before-destroy" callbacks for non-memory cleanup.
14 *
15 * Destroying a context destroys its children first, runs the
16 * cleanup callbacks bottom-up, then frees all chunks. Reset
17 * frees chunks but keeps children alive.
18 *
19 * M11 deliberately ships a simple-but-correct allocator first
20 * (every allocation = malloc + chain). M11.5 will swap in slab
21 * caches for fixed-size hot paths and arena-free for large
22 * short-lived contexts. The public API doesn't change.
23 *
24 * Thread-safety: each context carries an optional pthread mutex.
25 * The default factory makes contexts unlocked for the per-loop
26 * (single-thread) common case; a flag enables locking for
27 * contexts that legitimately span threads.
28 */
29
30#ifndef XTC_MCTX_H
31#define XTC_MCTX_H
32
33#include "xtc_export.h"
34
35#include <stddef.h>
36#include <stdint.h>
37
38#include "xtc.h"
39
40typedef struct xtc_mctx xtc_mctx_t;
41
42typedef enum xtc_mctx_flags {
43 XTC_MCTX_DEFAULT = 0,
44 XTC_MCTX_THREAD_SAFE = 1u << 0 /* internally locked */
45} xtc_mctx_flags_t;
46
47typedef void (*xtc_mctx_cleanup_fn)(void *user);
48
49/*
50 * PUBLIC: int xtc_mctx_create __P((xtc_mctx_t *, const char *, unsigned, xtc_mctx_t **));
51 * PUBLIC: void xtc_mctx_destroy __P((xtc_mctx_t *));
52 * PUBLIC: void xtc_mctx_reset __P((xtc_mctx_t *));
53 *
54 * PUBLIC: void *xtc_mctx_alloc __P((xtc_mctx_t *, size_t));
55 * PUBLIC: void *xtc_mctx_calloc __P((xtc_mctx_t *, size_t, size_t));
56 * PUBLIC: void *xtc_mctx_strdup __P((xtc_mctx_t *, const char *));
57 * PUBLIC: void xtc_mctx_free __P((xtc_mctx_t *, void *));
58 *
59 * PUBLIC: int xtc_mctx_register_cleanup __P((xtc_mctx_t *, xtc_mctx_cleanup_fn, void *));
60 *
61 * PUBLIC: const char *xtc_mctx_name __P((const xtc_mctx_t *));
62 * PUBLIC: size_t xtc_mctx_total_bytes __P((const xtc_mctx_t *));
63 * PUBLIC: size_t xtc_mctx_total_chunks __P((const xtc_mctx_t *));
64 */
65
66/* Create a child context. parent==NULL produces a root context.
67 * name is copied for diagnostics. flags = bitmask of XTC_MCTX_*. */
68XTC_API int xtc_mctx_create(xtc_mctx_t *parent, const char *name,
69 unsigned flags, xtc_mctx_t **out);
70
71/* Destroy a context: recursively destroys children, runs cleanups
72 * bottom-up, frees all chunks. Detaches from parent. */
73XTC_API void xtc_mctx_destroy(xtc_mctx_t *m);
74
75/* Free all allocations and run cleanups, but keep the context (and
76 * its children) usable. Useful for per-iteration scratch contexts. */
77XTC_API void xtc_mctx_reset(xtc_mctx_t *m);
78
79/* Allocate within the context. Returns NULL on failure. */
80XTC_API void *xtc_mctx_alloc(xtc_mctx_t *m, size_t size);
81XTC_API void *xtc_mctx_calloc(xtc_mctx_t *m, size_t n, size_t size);
82
83/* Strdup into the context. Lives until reset/destroy. */
84XTC_API void *xtc_mctx_strdup(xtc_mctx_t *m, const char *s);
85
86/* Free a single allocation early. Optional -- most code just lets
87 * destroy/reset reclaim. */
88XTC_API void xtc_mctx_free(xtc_mctx_t *m, void *p);
89
90/* Register a cleanup callback. Runs at destroy/reset time, before
91 * the chunks are freed. Multiple callbacks run in LIFO order.
92 *
93 * The context's lock (if XTC_MCTX_THREAD_SAFE) is NOT held while the
94 * callback runs, so a callback may call back into its own context --
95 * xtc_mctx_total_bytes/_total_chunks, or an alloc. What it must NOT do
96 * is reset or destroy the very context that is running it (that would
97 * recurse into the same teardown). Callbacks are detached before they
98 * run, so each fires exactly once even if the context is reset
99 * concurrently; register again if the next reset should call it too. */
100XTC_API int xtc_mctx_register_cleanup(xtc_mctx_t *m,
101 xtc_mctx_cleanup_fn fn, void *user);
102
103XTC_API const char *xtc_mctx_name(const xtc_mctx_t *m);
104XTC_API size_t xtc_mctx_total_bytes(const xtc_mctx_t *m);
105XTC_API size_t xtc_mctx_total_chunks(const xtc_mctx_t *m);
106
107/* ---- arena groups: wholesale shared-state discard on kill ----
108 *
109 * The safe way to force-kill a fiber that mutates state OTHER fibers
110 * keep using. xtc_proc(3) documents why a plain async kill is unsafe
111 * there: releasing a lock does not undo a half-finished mutation, and a
112 * single address space gives no smaller recovery boundary than the
113 * process -- so the honest escalation is "take a larger unit down and
114 * rebuild its shared state from a durable log," the analogue of a
115 * crash-only database reinitialising shared memory.
116 *
117 * An arena group makes that unit smaller than the whole process. It
118 * binds THREE things that already exist -- a set of fibers, one
119 * memory context (the arena), and the async-kill primitive -- into one
120 * recovery boundary:
121 *
122 * - Every allocation the member fibers make for their SHARED state
123 * goes in the group's arena (xtc_arena_group_mctx()).
124 * - xtc_arena_group_discard() kills every member with a deadline,
125 * waits until they are ALL confirmed gone, and only THEN resets the
126 * arena wholesale. Because no member is alive when the reset runs,
127 * nothing can be mid-mutation of the state being discarded -- the
128 * torn WAL record / half-updated page is thrown away with the
129 * arena, exactly as it would be on a process restart.
130 *
131 * This does NOT make an arbitrary mid-critical-section kill safe on its
132 * own; safety comes from the two-phase order (all members dead, THEN
133 * discard) plus the caller rebuilding from its log afterward. It is
134 * the group analogue of the whole-process fail-stop, at group
135 * granularity.
136 *
137 * Members are the fibers registered with xtc_arena_group_add. A fiber
138 * adds ITSELF, and the group records its pid TOGETHER WITH its owner loop
139 * (the loop it was spawned on): PIDs are executor-relative, so a pid alone
140 * can name an unrelated proc on another standalone loop. OWNER-LOOP
141 * LIFETIME: a member's owner loop must not be finalized (xtc_loop_fini /
142 * xtc_exec_fini) while the group still lists that member -- i.e. until a
143 * discard has reported all_gone == 1, or the group is destroyed. A member that exits on
144 * its own is reaped from the group lazily (a dead pid is skipped at
145 * discard time). The group's arena is a normal xtc_mctx_t and may have
146 * children; reset discards them too.
147 *
148 * JOIN/DISCARD RACE -- THE GROUP IS SEALED WHILE A DISCARD RUNS.
149 * discard() must kill a FIXED cohort, and killing yields, so the window
150 * between "snapshot the members" and "reset the arena" is long. A
151 * fiber joining inside that window would be neither killed (it is not
152 * in the snapshot) nor safe (the reset wipes memory it just allocated).
153 * So discard SEALS the group in the same lock hold that takes the
154 * snapshot: from then until the discard reaches its verdict,
155 * xtc_arena_group_add fails with XTC_E_AGAIN and MUST NOT touch the
156 * arena. A caller that gets XTC_E_AGAIN is racing a teardown of the
157 * state it wanted to share; the correct response is to back off and
158 * retry the join (succeeding once the discard finishes -- the group is
159 * reusable by a fresh cohort), never to proceed with the arena.
160 *
161 * A MEMBER IS "GONE" ONLY ONCE IT IS REAPED, not merely once it is no
162 * longer alive: a proc clears its alive flag before running its at-exit
163 * callbacks, and those callbacks can still touch arena memory. discard
164 * waits for the pid to disappear from the proc table entirely, which is
165 * the first moment nothing of the member can reach the arena.
166 */
167typedef struct xtc_arena_group xtc_arena_group_t;
168
169/*
170 * PUBLIC: int xtc_arena_group_create __P((const char *, xtc_arena_group_t **));
171 * PUBLIC: void xtc_arena_group_destroy __P((xtc_arena_group_t *));
172 * PUBLIC: xtc_mctx_t *xtc_arena_group_mctx __P((xtc_arena_group_t *));
173 * PUBLIC: int xtc_arena_group_add __P((xtc_arena_group_t *));
174 * PUBLIC: int xtc_arena_group_discard __P((xtc_arena_group_t *, int, int64_t, int *));
175 * PUBLIC: int xtc_arena_group_size __P((xtc_arena_group_t *));
176 */
177
178/* Create an empty group with a fresh thread-safe arena (members run on
179 * different carriers, so the arena is internally locked). `name` is
180 * copied for diagnostics. XTC_E_INVAL on NULL out, XTC_E_NOMEM on OOM. */
181XTC_API int xtc_arena_group_create(const char *name,
182 xtc_arena_group_t **out);
183
184/* Destroy the group and its arena. Does NOT kill members -- call
185 * discard first if any may still be alive; this just frees the group
186 * and the arena's memory. NULL-safe. */
187XTC_API void xtc_arena_group_destroy(xtc_arena_group_t *g);
188
189/* The group's arena. Allocate the members' SHARED state here so it can
190 * be discarded wholesale. Valid until xtc_arena_group_destroy. */
191XTC_API xtc_mctx_t *xtc_arena_group_mctx(xtc_arena_group_t *g);
192
193/* Register the CALLING fiber as a member. Must be called from a proc
194 * (xtc_self() != NONE); XTC_E_INVAL otherwise. Idempotent per pid.
195 * XTC_E_NOMEM if the member list cannot grow.
196 *
197 * XTC_E_AGAIN if a discard is in flight (the group is sealed): the
198 * arena is about to be reset wholesale, so the caller was NOT admitted
199 * and must NOT allocate in or read the arena. Back off and retry -- the
200 * seal lifts when the discard reaches its verdict, and the group is then
201 * reusable by a fresh cohort. */
202XTC_API int xtc_arena_group_add(xtc_arena_group_t *g);
203
204/* Discard the group: xtc_exit_pid_deadline every live member with
205 * `reason`, wait up to `timeout_ns` PER member for it to go, then --
206 * only if ALL members are confirmed gone -- reset the arena, throwing
207 * away every allocation made through it.
208 *
209 * Joins are SEALED OFF for the whole call (see above): the cohort that
210 * is killed is exactly the membership at entry, and no fiber can slip
211 * into the group between the snapshot and the reset.
212 *
213 * *out_all_gone (may be NULL) is set to 1 if every member terminated
214 * AND was reaped (its at-exit callbacks have finished) and the arena was
215 * reset, or 0 if at least one member was still alive or still unwinding
216 * at its deadline (a member wedged inside xtc_uncancelable with the
217 * kill deferred, or one stuck in an at-exit callback). In the 0 case
218 * the arena is NOT reset -- discarding state a live fiber may still
219 * touch is exactly the corruption this primitive exists to avoid -- and
220 * the caller should escalate to the whole-process fail-stop. Returns
221 * XTC_OK once a verdict is reached, XTC_E_INVAL on NULL group,
222 * XTC_E_AGAIN if another discard of this group is already in flight
223 * (the caller may retry, or just wait for that one's verdict).
224 * Callable from a fiber or a plain thread (the wait yields on a fiber,
225 * sleeps the OS thread off one), but NOT from a member of the group
226 * itself (that would be suicide mid-wait); XTC_E_INVAL if the caller is
227 * a member. */
228XTC_API int xtc_arena_group_discard(xtc_arena_group_t *g, int reason,
229 int64_t timeout_ns, int *out_all_gone);
230
231/* Current member count (live + not-yet-reaped). 0 on NULL. */
232XTC_API int xtc_arena_group_size(xtc_arena_group_t *g);
233
234#endif /* XTC_MCTX_H */