libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_cfg.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_cfg.h
7 * Typed runtime-configurable settings registry -- named, typed,
8 * bounds-checked tunables resolved at run time. Each variable has:
9 * - a name (string key)
10 * - a type (int, int64, double, bool, string, enum)
11 * - a default value
12 * - optional min/max bounds
13 * - an optional validator callback
14 * - an optional change callback
15 *
16 * Use cases:
17 * - Tunable knobs that ops can change at runtime without
18 * restart (e.g. log level, backpressure thresholds).
19 * - Configuration discovery (a "show all" surface).
20 * - Test-time fault injection (set a knob in a test, restore
21 * in cleanup).
22 *
23 * Storage model:
24 * - Single global registry keyed by name.
25 * - Mutex-protected list (linear scan; suitable for ~hundreds
26 * of vars; M11.5 swaps in xtc_chash for thousands).
27 * - Each var holds its declared type + current value via union.
28 *
29 * Per-session / per-database scoping (the override-stack model) is
30 * implemented -- see the session section at the bottom of this
31 * header. Configuration-file parsing (xtc_cfg_load_file) and
32 * SIGHUP-driven reload (xtc_cfg_reload) are done too. See
33 * docs/KNOWN_ISSUES.md for what remains.
34 *
35 * Hot-path reads:
36 * - The name-keyed xtc_cfg_get_* do a registry lookup per call;
37 * that is fine for cold/occasional reads but too slow for a knob
38 * read on every operation. For hot paths, resolve the variable
39 * ONCE to an opaque handle with xtc_cfg_ref() and then read
40 * through xtc_cfg_ref_get_* -- no name lookup, no scan. The
41 * handle stays valid until the variable is unregistered (a
42 * registry entry is never relocated), so it can be cached for
43 * the process lifetime like a compiled-in pointer.
44 */
45
46#ifndef XTC_CFG_H
47#define XTC_CFG_H
48
49#include "xtc_export.h"
50
51#include <stddef.h>
52#include <stdint.h>
53
54#include "xtc.h"
55
56typedef enum xtc_cfg_kind {
57 XTC_CFG_BOOL = 1,
58 XTC_CFG_INT = 2,
59 XTC_CFG_INT64 = 3,
60 XTC_CFG_DOUBLE = 4,
61 XTC_CFG_STRING = 5,
62 XTC_CFG_ENUM = 6
63} xtc_cfg_kind_t;
64
65typedef int (*xtc_cfg_validator_fn)(const void *new_val, void *user);
66typedef void (*xtc_cfg_changed_fn)(const char *name, const void *old_val,
67 const void *new_val, void *user);
68
69/* Opaque, pointer-stable handle to a registered variable, for hot-path
70 * reads that must avoid a per-read name lookup. Obtain with
71 * xtc_cfg_ref(); valid until the variable is unregistered. */
72typedef struct xtc_cfg_var *xtc_cfg_ref_t;
73
74/* Spec used at registration time. */
75typedef struct xtc_cfg_spec {
76 size_t size; /* sizeof(xtc_cfg_spec_t) */
77 const char *name;
78 const char *short_desc; /* ops-friendly description */
79 xtc_cfg_kind_t kind;
80
81 /* Default value (interpreted per kind). */
82 union {
83 int d_bool; /* 0/1 */
84 int d_int;
85 int64_t d_int64;
86 double d_double;
87 const char *d_string;
88 int d_enum;
89 } dflt;
90
91 /* Bounds for numeric types (inclusive); 0/0 means unbounded. */
92 int64_t min_int;
93 int64_t max_int;
94 double min_double;
95 double max_double;
96
97 /* For ENUM: NULL-terminated array of allowed string labels;
98 * the int value is the index into this array. */
99 const char *const *enum_labels;
100 int n_enum_labels;
101
102 /* Optional callbacks. */
103 xtc_cfg_validator_fn validator;
104 xtc_cfg_changed_fn on_change;
105 void *cb_user;
107
108/* Set .size in every spec: { .size = sizeof(xtc_cfg_spec_t), .name = ... }
109 * or start from XTC_CFG_SPEC_INIT. name and kind are required, so a size
110 * must cover them. */
111#define XTC_CFG_SPEC_INIT { .size = sizeof(xtc_cfg_spec_t) }
112
113/*
114 * PUBLIC: int xtc_cfg_register __P((const xtc_cfg_spec_t *));
115 * PUBLIC: int xtc_cfg_unregister __P((const char *));
116 *
117 * PUBLIC: int xtc_cfg_get_bool __P((const char *, int *));
118 * PUBLIC: int xtc_cfg_get_int __P((const char *, int *));
119 * PUBLIC: int xtc_cfg_get_int64 __P((const char *, int64_t *));
120 * PUBLIC: int xtc_cfg_get_double __P((const char *, double *));
121 * PUBLIC: int xtc_cfg_get_string __P((const char *, const char **));
122 * PUBLIC: int xtc_cfg_get_string_copy __P((const char *, char **));
123 * PUBLIC: int xtc_cfg_get_enum __P((const char *, int *));
124 *
125 * PUBLIC: int xtc_cfg_set_bool __P((const char *, int));
126 * PUBLIC: int xtc_cfg_set_int __P((const char *, int));
127 * PUBLIC: int xtc_cfg_set_int64 __P((const char *, int64_t));
128 * PUBLIC: int xtc_cfg_set_double __P((const char *, double));
129 * PUBLIC: int xtc_cfg_set_string __P((const char *, const char *));
130 * PUBLIC: int xtc_cfg_set_enum __P((const char *, int));
131 *
132 * PUBLIC: int xtc_cfg_count __P((void));
133 * PUBLIC: int xtc_cfg_kind __P((const char *, xtc_cfg_kind_t *));
134 * PUBLIC: int xtc_cfg_load_file __P((const char *));
135 * PUBLIC: int xtc_cfg_reload __P((void));
136 *
137 * PUBLIC: int xtc_cfg_ref __P((const char *, xtc_cfg_ref_t *));
138 * PUBLIC: int xtc_cfg_ref_get_bool __P((xtc_cfg_ref_t, int *));
139 * PUBLIC: int xtc_cfg_ref_get_int __P((xtc_cfg_ref_t, int *));
140 * PUBLIC: int xtc_cfg_ref_get_int64 __P((xtc_cfg_ref_t, int64_t *));
141 * PUBLIC: int xtc_cfg_ref_get_double __P((xtc_cfg_ref_t, double *));
142 * PUBLIC: int xtc_cfg_ref_get_string __P((xtc_cfg_ref_t, const char **));
143 * PUBLIC: int xtc_cfg_ref_get_enum __P((xtc_cfg_ref_t, int *));
144 */
145
146/* xtc_cfg_register returns XTC_E_INVAL for a NULL spec / name AND for a
147 * name that is already registered (there is no separate "exists" code);
148 * XTC_E_NOMEM on allocation failure. An unregistered name is
149 * XTC_E_INVAL from the name-keyed getters/setters, xtc_cfg_kind and
150 * xtc_cfg_unregister, and XTC_E_NOTFOUND from xtc_cfg_ref and the
151 * session source/reset calls. */
152XTC_API int xtc_cfg_register(const xtc_cfg_spec_t *spec);
153XTC_API int xtc_cfg_unregister(const char *name);
154
155XTC_API int xtc_cfg_get_bool(const char *name, int *out);
156XTC_API int xtc_cfg_get_int(const char *name, int *out);
157XTC_API int xtc_cfg_get_int64(const char *name, int64_t *out);
158XTC_API int xtc_cfg_get_double(const char *name, double *out);
159/* STRING LIFETIME. xtc_cfg_get_string (and xtc_cfg_ref_get_string) lend
160 * the registry's -- or the bound session's -- own buffer: it is valid
161 * only until the next set of that key (xtc_cfg_set_string, a session set
162 * / commit / reset / abort / destroy for a session value, xtc_cfg_load_file /
163 * _reload, xtc_cfg_unregister), which frees it. Another thread can do
164 * that at any moment, so read it only where no concurrent set is
165 * possible. xtc_cfg_get_string_copy returns a private copy, taken under
166 * the registry lock, that the caller frees with xtc_free; use it whenever
167 * a set may race the read. A NULL value copies as NULL. */
168XTC_API int xtc_cfg_get_string(const char *name, const char **out);
169XTC_API int xtc_cfg_get_string_copy(const char *name, char **out);
170XTC_API int xtc_cfg_get_enum(const char *name, int *out);
171
172XTC_API int xtc_cfg_set_bool(const char *name, int v);
173XTC_API int xtc_cfg_set_int(const char *name, int v);
174XTC_API int xtc_cfg_set_int64(const char *name, int64_t v);
175XTC_API int xtc_cfg_set_double(const char *name, double v);
176XTC_API int xtc_cfg_set_string(const char *name, const char *v);
177XTC_API int xtc_cfg_set_enum(const char *name, int v);
178
179XTC_API int xtc_cfg_count(void);
180XTC_API int xtc_cfg_kind(const char *name, xtc_cfg_kind_t *out);
181
182/* Load a postgresql.conf-style `name = value` file: per-line, with `#`
183 * comments and optional quotes; each value parsed per the variable's
184 * registered kind and applied via xtc_cfg_set_* (bounds/validators
185 * apply). Unknown names and bad values are skipped. Returns the
186 * count applied (>= 0), XTC_E_INVAL (NULL path), or XTC_E_IO (open
187 * failed). Remembers the path for xtc_cfg_reload. */
188XTC_API int xtc_cfg_load_file(const char *path);
189
190/* Re-read the file last loaded by xtc_cfg_load_file. Meant to back a
191 * SIGHUP handler, but is NOT async-signal-safe (uses stdio and the
192 * registry lock): set a flag in the handler and call this from the
193 * event loop. Returns the applied count, XTC_E_INVAL (no file loaded),
194 * or XTC_E_IO. */
195XTC_API int xtc_cfg_reload(void);
196
197/*
198 * Hot-path read handles. xtc_cfg_ref() resolves a name to an opaque,
199 * pointer-stable handle ONCE (the only name lookup); thereafter
200 * xtc_cfg_ref_get_* read the current value with no lookup and no scan.
201 * The handle stays valid until the variable is unregistered. Reads
202 * through a handle still observe live xtc_cfg_set_* updates. The
203 * kind must match (a _get_int on a non-INT handle returns XTC_E_INVAL),
204 * exactly like the name-keyed getters. xtc_cfg_ref returns XTC_E_INVAL
205 * on NULL args and XTC_E_NOTFOUND for an unregistered name.
206 */
207XTC_API int xtc_cfg_ref(const char *name, xtc_cfg_ref_t *out);
208XTC_API int xtc_cfg_ref_get_bool(xtc_cfg_ref_t ref, int *out);
209XTC_API int xtc_cfg_ref_get_int(xtc_cfg_ref_t ref, int *out);
210XTC_API int xtc_cfg_ref_get_int64(xtc_cfg_ref_t ref, int64_t *out);
211XTC_API int xtc_cfg_ref_get_double(xtc_cfg_ref_t ref, double *out);
212XTC_API int xtc_cfg_ref_get_string(xtc_cfg_ref_t ref, const char **out);
213XTC_API int xtc_cfg_ref_get_enum(xtc_cfg_ref_t ref, int *out);
214
215/* ---- per-session scoping + a transactional override stack ----
216 *
217 * A single global value per name is what a database server CANNOT use
218 * for most settings: `SET work_mem` must affect one session and no
219 * other. A session holds per-variable OVERRIDES layered over the
220 * global registry, so a read resolves
221 *
222 * session override (newest wins) -> global current value
223 *
224 * and the global value is never touched by a session set. The name-
225 * keyed and ref getters above resolve through the CURRENT session
226 * (xtc_cfg_session_bind) when one is bound, and fall straight through
227 * to the global value when none is -- so existing single-config code is
228 * unchanged, and a bare `xtc_cfg_get_int("work_mem", &v)` in a bound
229 * session automatically sees that session's value. This is the shape
230 * PostgreSQL reads GUCs with (bare names in thousands of places), with
231 * an explicit-scope variant for admin paths that must reach across
232 * sessions.
233 *
234 * TRANSACTIONAL OVERRIDE STACK. Within a session each variable's
235 * overrides form a stack of LEVELS, opened by xtc_cfg_session_push and
236 * closed by _commit (collapse into the parent -- the values set at this
237 * level survive) or _abort (discard -- the values revert to what the
238 * parent saw). Nestable to any depth. This backs SET LOCAL (a value
239 * scoped to the current transaction), SET inside a transaction that
240 * later ROLLBACKs, function-local SET (proconfig), and subtransactions.
241 *
242 * SOURCE PRECEDENCE. Each set carries a source rank (xtc_cfg_source_t).
243 * A set is applied only if its rank is >= the rank that last set the
244 * value at the current level, and the winning source is reported by
245 * xtc_cfg_session_source -- mirroring PostgreSQL's
246 * PGC_S_DEFAULT < file < database < user < client < session <
247 * PGC_S_OVERRIDE ordering and pg_settings.source.
248 *
249 * Thread/fiber model: a session is owned by one fiber at a time (bind
250 * it on entry, unbind on exit). The BINDING is per-fiber, not
251 * per-thread: it is keyed on the calling fiber's identity, so two
252 * fibers on ONE loop can each bind their own session, yield, and keep
253 * reading their own values -- and a work-stolen fiber that resumes on a
254 * different thread keeps its binding. The overrides themselves are not
255 * shared, so no lock guards them; only the fallback read of the global
256 * value takes the registry lock, exactly as the unscoped getters do.
257 *
258 * Variable lifetime: unregistering a variable that a live session still
259 * overrides is safe in EITHER order -- the entry's storage outlives the
260 * unregister until the last override naming it is torn down, while
261 * being immediately invisible to every lookup.
262 */
263typedef struct xtc_cfg_session xtc_cfg_session_t;
264
265/* Source ranks, low to high precedence. A higher rank overrides a
266 * lower one at the same stack level; an equal-or-higher rank is
267 * required to replace an existing value. */
268typedef enum xtc_cfg_source {
269 XTC_CFG_SRC_DEFAULT = 0, /* compiled-in default */
270 XTC_CFG_SRC_FILE = 1, /* config file */
271 XTC_CFG_SRC_DATABASE = 2, /* per-database default */
272 XTC_CFG_SRC_USER = 3, /* per-role default */
273 XTC_CFG_SRC_CLIENT = 4, /* connection string / startup packet */
274 XTC_CFG_SRC_SESSION = 5, /* interactive SET */
275 XTC_CFG_SRC_OVERRIDE = 6 /* forced; beats everything */
276} xtc_cfg_source_t;
277
278/*
279 * PUBLIC: int xtc_cfg_session_create __P((xtc_cfg_session_t **));
280 * PUBLIC: void xtc_cfg_session_destroy __P((xtc_cfg_session_t *));
281 * PUBLIC: xtc_cfg_session_t *xtc_cfg_session_bind __P((xtc_cfg_session_t *));
282 * PUBLIC: xtc_cfg_session_t *xtc_cfg_session_current __P((void));
283 * PUBLIC: int xtc_cfg_session_push __P((xtc_cfg_session_t *));
284 * PUBLIC: int xtc_cfg_session_commit __P((xtc_cfg_session_t *));
285 * PUBLIC: int xtc_cfg_session_abort __P((xtc_cfg_session_t *));
286 * PUBLIC: int xtc_cfg_session_source __P((xtc_cfg_session_t *, const char *, xtc_cfg_source_t *));
287 * PUBLIC: int xtc_cfg_session_reset __P((xtc_cfg_session_t *, const char *));
288 * PUBLIC: int xtc_cfg_ssn_set_bool __P((xtc_cfg_session_t *, const char *, int, xtc_cfg_source_t));
289 * PUBLIC: int xtc_cfg_ssn_set_int __P((xtc_cfg_session_t *, const char *, int, xtc_cfg_source_t));
290 * PUBLIC: int xtc_cfg_ssn_set_int64 __P((xtc_cfg_session_t *, const char *, int64_t, xtc_cfg_source_t));
291 * PUBLIC: int xtc_cfg_ssn_set_double __P((xtc_cfg_session_t *, const char *, double, xtc_cfg_source_t));
292 * PUBLIC: int xtc_cfg_ssn_set_string __P((xtc_cfg_session_t *, const char *, const char *, xtc_cfg_source_t));
293 * PUBLIC: int xtc_cfg_ssn_set_enum __P((xtc_cfg_session_t *, const char *, int, xtc_cfg_source_t));
294 */
295
296/* Lifecycle. A fresh session has no overrides and one (base) level.
297 * destroy frees all levels and any string overrides; NULL-safe. */
298XTC_API int xtc_cfg_session_create(xtc_cfg_session_t **out);
299XTC_API void xtc_cfg_session_destroy(xtc_cfg_session_t *s);
300
301/* Bind `s` (or NULL to unbind) as the current session for the calling
302 * FIBER, so the unscoped xtc_cfg_get_* / ref getters resolve through
303 * it. The binding rides with the fiber across a yield and across a
304 * work-stealing migration; off a fiber (a plain OS thread) it is
305 * per-thread. Returns the previously bound session (or NULL) so a
306 * caller can save/restore. A session is bound to one fiber at a time;
307 * do not share a bound session across fibers concurrently. */
308XTC_API xtc_cfg_session_t *xtc_cfg_session_bind(xtc_cfg_session_t *s);
309XTC_API xtc_cfg_session_t *xtc_cfg_session_current(void);
310
311/* Transactional levels. push opens a new (innermost) level; set within
312 * it, then commit to fold those values into the parent, or abort to
313 * discard them. commit/abort of the base level is XTC_E_INVAL -- there
314 * is always one level. Nestable. */
315XTC_API int xtc_cfg_session_push(xtc_cfg_session_t *s);
316XTC_API int xtc_cfg_session_commit(xtc_cfg_session_t *s);
317XTC_API int xtc_cfg_session_abort(xtc_cfg_session_t *s);
318
319/* The source that set the effective session value for `name`, or
320 * XTC_CFG_SRC_DEFAULT when the session has no override (the global
321 * value wins). XTC_E_NOTFOUND for an unregistered name. */
322XTC_API int xtc_cfg_session_source(xtc_cfg_session_t *s, const char *name,
323 xtc_cfg_source_t *out);
324
325/* Drop `name`'s override at the current level (RESET): the value
326 * reverts to what the parent level / the global value provides.
327 * XTC_OK whether or not an override existed. */
328XTC_API int xtc_cfg_session_reset(xtc_cfg_session_t *s, const char *name);
329
330/* Set a per-session override at the current level. The value is
331 * validated against the variable's registered kind/bounds/validator
332 * exactly like the global setters, and applied only if `src` outranks
333 * the source that last set it at this level. A NULL session targets
334 * the fiber's currently-bound session; XTC_E_INVAL if none is bound.
335 * Out of bounds is XTC_E_RANGE, as for the global setters (releases
336 * before 1.50 returned XTC_E_INVAL here); a validator rejection is
337 * XTC_E_INVAL on both. */
338XTC_API int xtc_cfg_ssn_set_bool(xtc_cfg_session_t *s, const char *name,
339 int v, xtc_cfg_source_t src);
340XTC_API int xtc_cfg_ssn_set_int(xtc_cfg_session_t *s, const char *name,
341 int v, xtc_cfg_source_t src);
342XTC_API int xtc_cfg_ssn_set_int64(xtc_cfg_session_t *s, const char *name,
343 int64_t v, xtc_cfg_source_t src);
344XTC_API int xtc_cfg_ssn_set_double(xtc_cfg_session_t *s, const char *name,
345 double v, xtc_cfg_source_t src);
346XTC_API int xtc_cfg_ssn_set_string(xtc_cfg_session_t *s, const char *name,
347 const char *v, xtc_cfg_source_t src);
348XTC_API int xtc_cfg_ssn_set_enum(xtc_cfg_session_t *s, const char *name,
349 int v, xtc_cfg_source_t src);
350
351#endif /* XTC_CFG_H */