libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_saga.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_saga.h
6 * L3 saga: a sequence of (forward-action, compensating-action)
7 * pairs run in order from an ordinary fiber. If a forward action
8 * fails, every already-completed step's compensation runs in
9 * REVERSE order, then xtc_saga_run returns the ORIGINAL failing
10 * step's error code. All steps succeeding returns XTC_OK.
11 *
12 * This is the saga pattern (distributed-transaction compensation,
13 * as used for multi-service workflows and, per PLAN.md 19.5, the
14 * intended replication-coordination and extension-flow use case):
15 * there is no two-phase-commit coordinator, no lock held across
16 * steps -- just a forward action and a hand-written undo for each
17 * step, run by ordinary synchronous-looking C called from a fiber.
18 *
19 * NOTE on PLAN.md's original sketch: it proposed building this on
20 * a hypothetical "xtc_future" type with an "await()" call. Neither
21 * exists in this codebase (grep confirms no xtc_future, no await()
22 * besides the real xtc_await in xtc_async.h, which is a coroutine
23 * join primitive, not a generic future). A saga step is a plain
24 * function call, not a spawned task: xtc_saga_run calls each step
25 * function directly on the CALLING fiber, in order, exactly like
26 * calling a chain of ordinary functions. A step is free to spawn
27 * an xtc_proc / use xtc_async / call xtc_await internally and block
28 * until that sub-work finishes -- the saga does not need its own
29 * future type to support that, because xtc_await already exists for
30 * it. Sagas thus fit directly on top of the real primitives (fibers,
31 * xtc_proc, xtc_async/xtc_await) with no new async abstraction.
32 *
33 * A compensation that itself fails is a documented UNRECOVERABLE
34 * SAGA condition: the runtime does not swallow it. xtc_saga_run
35 * logs it loudly (stderr, matching the existing convention for a
36 * rare unrecoverable event -- see the redzone violation in
37 * src/ptc/slab.c and the determinism violation in src/evt/sim.c) and
38 * keeps running the REMAINING compensations (so a coding accident in
39 * one undo does not also skip cleanup of steps further back), then
40 * returns XTC_E_INTERNAL regardless of the forward failure's
41 * original code -- an unrecoverable saga is always distinguishable
42 * from an ordinary forward failure. xtc_saga_last_error() still
43 * reports the original forward failure for diagnostics, and
44 * xtc_saga_compensate_failed() reports whether a compensation failed
45 * on the last run.
46 */
47
48#ifndef XTC_SAGA_H
49#define XTC_SAGA_H
50
51#include "xtc_export.h"
52
53#include <stddef.h>
54
55#include "xtc.h"
56
57typedef struct xtc_saga xtc_saga_t;
58
59/*
60 * A step's forward action / compensating action. ctx is whatever
61 * the caller passed to xtc_saga_step (typically per-step state:
62 * connection handles, request payloads, ids to undo). Both return
63 * the usual XTC_OK / negative XTC_E_* convention. A compensation is
64 * called ONLY for a step whose forward action already returned
65 * XTC_OK; it must undo exactly that step's effect.
66 */
67typedef int (*xtc_saga_fn)(void *ctx);
68
69/*
70 * PUBLIC: int xtc_saga_create __P((xtc_saga_t **));
71 * PUBLIC: void xtc_saga_destroy __P((xtc_saga_t *));
72 * PUBLIC: int xtc_saga_step __P((xtc_saga_t *, xtc_saga_fn, xtc_saga_fn, void *));
73 * PUBLIC: int xtc_saga_run __P((xtc_saga_t *));
74 * PUBLIC: int xtc_saga_n_steps __P((const xtc_saga_t *));
75 * PUBLIC: int xtc_saga_n_completed __P((const xtc_saga_t *));
76 * PUBLIC: int xtc_saga_failed_step __P((const xtc_saga_t *));
77 * PUBLIC: int xtc_saga_last_error __P((const xtc_saga_t *));
78 * PUBLIC: int xtc_saga_compensate_failed __P((const xtc_saga_t *));
79 */
80
81/* Create an empty saga. Returns XTC_OK with *out set, or XTC_E_NOMEM /
82 * XTC_E_INVAL (out == NULL). */
83XTC_API int xtc_saga_create(xtc_saga_t **out);
84
85/* Destroy a saga. Does not touch step ctx pointers (caller-owned).
86 * Safe on NULL. */
87XTC_API void xtc_saga_destroy(xtc_saga_t *s);
88
89/*
90 * Append a step. Steps run in the order added. compensate may be
91 * NULL for a step with no undo (e.g. a pure read) -- xtc_saga_run then
92 * skips compensating it, exactly as if it were a no-op. action is
93 * required. Returns XTC_OK, XTC_E_INVAL (s or action NULL), or
94 * XTC_E_NOMEM. Steps may not be added once xtc_saga_run has started
95 * (returns XTC_E_INVAL).
96 */
97XTC_API int xtc_saga_step(xtc_saga_t *s, xtc_saga_fn action, xtc_saga_fn compensate,
98 void *ctx);
99
100/*
101 * Run the saga on the CALLING fiber: call each step's action in order.
102 * On the first action that returns != XTC_OK, run the compensation of
103 * every step that already completed successfully, in REVERSE order,
104 * skipping any step whose compensate is NULL, then return that
105 * action's error code (xtc_saga_last_error() also reports it).
106 *
107 * If a compensation itself returns != XTC_OK, that is an unrecoverable
108 * saga: xtc_saga_run logs it loudly to stderr (it does NOT abort the
109 * process or silently swallow the error), continues running the
110 * REMAINING compensations in reverse order regardless (so one broken
111 * undo does not also skip cleanup further back), and the overall
112 * return value becomes XTC_E_INTERNAL -- distinct from any ordinary
113 * step error, so a caller can tell "a step failed and every
114 * compensation ran cleanly" (the original step's code) apart from "a
115 * step failed AND compensation itself is broken" (XTC_E_INTERNAL).
116 * xtc_saga_compensate_failed() reports the latter case after the run.
117 *
118 * All steps succeeding returns XTC_OK. A saga with zero steps
119 * trivially succeeds (XTC_OK). Returns XTC_E_INVAL if s is NULL or
120 * xtc_saga_run was already called on this saga (a saga runs at most
121 * once; create a new one to run again).
122 */
123XTC_API int xtc_saga_run(xtc_saga_t *s);
124
125/* Number of steps added via xtc_saga_step. */
126XTC_API int xtc_saga_n_steps(const xtc_saga_t *s);
127
128/* Number of steps whose action completed successfully on the last
129 * run (== n_steps if the saga fully succeeded). 0 before running. */
130XTC_API int xtc_saga_n_completed(const xtc_saga_t *s);
131
132/* Index (0-based) of the step whose action failed on the last run, or
133 * -1 if no step has failed (including: not yet run, or fully
134 * succeeded). */
135XTC_API int xtc_saga_failed_step(const xtc_saga_t *s);
136
137/* The failing step's error code from the last run, or XTC_OK if the
138 * saga has not been run or fully succeeded. */
139XTC_API int xtc_saga_last_error(const xtc_saga_t *s);
140
141/* 1 if any compensation itself failed during the last run (the
142 * unrecoverable-saga condition), 0 otherwise. */
143XTC_API int xtc_saga_compensate_failed(const xtc_saga_t *s);
144
145#endif /* XTC_SAGA_H */