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