module std.saga
std.saga — sagas and durable pipelines (§10.6, §11.3, D-5100..D-5189).
The compiler turns every saga and pipeline declaration into ordinary functions that call this module
(compiler/src/sema/SagaSema.cpp); nothing here is meant to be called by hand except status, resolve and list.
A saga is a list of steps; after a step undo r => … names its compensation. The generated function nests the steps:
enter(0, …) -> S1 -> guard(0, it, r => U1, x => ( done(0, x) -> enter(1, …) -> S2 -> done(1, it) ))so the compensations run by themselves, innermost first, when a later step fails (guard). Nothing is kept in memory
between steps except the call stack: a node that takes the saga over RE-EXECUTES the steps from the first one. That is
sound because every step carries the key sagaId#step as idempotency-key and the rpcs of a step are @idempotent
(the analyzer insists on it): a repeated step answers with its first result instead of running again.
Journal (std.kv store oxl.saga, all keys of one saga carry the tag {sagaId} and so live on ONE shard, so a transaction
may read and write them together):
s/{id}/h head: name, encoded arguments, caller, creation time (written once)
s/{id}/o lease of the executor: node, expiry, epoch (compare-and-set; epoch fences a stale executor)
s/{id}/t state: running | compensating | done | failed | cancelled | stuck, step cursor, result
s/{id}/c cancel marker (the caller's deadline passed)
s/{id}/uK compensation of step K was done
a/<saga>/{id} index of unfinished sagas of a kind; x/{id} index of stuck onesWho runs a saga: the node that was called when it has a role with runs sagas, otherwise any such node; its recovery loop
(serve) claims sagas without an executor and takes over those whose lease expired or whose node SWIM declared dead.
Deadline (customer answer 9): when the caller's deadline passes the driver writes the cancel marker (one transaction) and
returns Timeout at once; the executor sees the marker before the next step and compensates the finished ones in the
background. The marker and the commit of the last step are decided in one transaction: a saga either commits or is cancelled.
Example:
service Ledger {
@public @idempotent rpc openAcct(id: u64) -> u64
}
impl Ledger {
rpc openAcct(id) => id + 100
}
saga register(id: u64) -> u64 {
Ledger.openAcct(id)
}
fn main() io, net, time, db {
match register(5) {
u64 v => print("registered {v}")
CompensationFailed _ => print("stuck")
NetError _ => print("net error")
}
print("stuck sagas: {std.saga.list(.Stuck).len()}")
}registered 105 stuck sagas: 0
Types
data DlqEntry | A dead letter: a tail step failed (step, the error as text). |
data Handoff | Not an error: the tail of the pipeline was handed to a node with the role (phop); the rest of the chain is skipped. |
enum SagaFilter | Which sagas list returns |
enum SagaResolution | How an operator closes a stuck saga by hand |
data SagaStatus | The state of a saga as std.saga.status shows it. |
Functions
dbg | Step k (0-based) of the saga / pipeline name starts |
decodeLike | Decodes b as the result type of like (used to decode the value of a durable step when a pipeline resumes on another node). |
dlq | The dead-letter queue. |
dlqList | The entries of the dead-letter queue of a pipeline (qualified name), oldest first. |
done | End of step k. The last step (last) commits |
drive | Starts the saga name with the encoded arguments (or joins the one the idempotency key of the call names) and waits for its result. |
enter | Start of step k (0-based) with the value v of the previous one |
guard | Runs rest (the steps after step k) and, when it fails, the compensation comp of step k with v (the step's result). |
hopped | The tail was handed over by phop in this task (the error handler and the DLQ must not see it). |
isErr | Whether a value of a union type is one of its error alternatives. |
list | Sagas by state: .Stuck (needing manual work), .Active (not finished) or .All known to the index of unfinished ones. |
pdurable | A durable step: the value that enters step k is written to the journal first. |
pfinish | The pipeline (or its tail) is over |
phop | on role in the tail of a pipeline (D-5110) |
pipeScope | Runs the front of a pipeline. |
pmark | Step k of a pipeline starts (the label names it in on_error(e, step)); the value passes through. |
pon | on role where nothing can hop (before respond, over a stream) |
pstep | The label of the pipeline step that is running in this task ("" outside a pipeline). |
resolve | Manual resolution of a stuck saga |
respondThen | Answers the caller with v and runs tail(v) in the background scope of the node. |
sagaRoles | Cluster roles with runs sagas of this program (empty without a cluster declaration). |
serve | Registers the recovery loop of a saga kind; called once at node start by the code the compiler generates. |
servePipe | Registers the recovery loop of a pipeline with durable steps |
status | The state of a saga, or none when no such saga exists. |
valueOf | Type witness: the value type of a chain, without evaluating it (the generated code never calls it). |