3.6 KiB
orchestrator/compensate-runner
Purpose
runFullCompensation(...) is the orchestrator's per-instance
compensation driver. It bridges the generic compensation module
(which only knows about CompensableStep + retry loop) with the
saga-specific concerns: looking up each completed step's registered
compensate handler, transitioning the step through compensating → compensated, recording the compensation counter + span, and emitting
saga.step.compensated. The result of the walk is returned as a
structured outcome so the orchestrator can settle the instance state.
This module is split out from orchestrator.ts to keep the main
file under the ~250-line soft limit and to isolate the compensation
concerns from the forward step-dispatch loop.
Features
- Walks completed step rows in reverse via the generic
runCompensationhelper. - Per-step: opens a
saga.compensatespan, transitions tocompensating, invokes the registered compensate handler under a freshRequestContext, incrementssaga.compensations_total, transitions tocompensated, emitssaga.step.compensated. - Honours per-saga
maxCompensationAttempts(defaults to 5 via thecompensationmodule). - On terminal failure: transitions the failed step row back to
failedso the persistence layer reflects the broken state.
Usage: Basic
Internal helper — typically only consumed by createSagaOrchestrator:
import { runFullCompensation } from "@suaveplan/api-saga/orchestrator/compensate-runner";
const result = await runFullCompensation({
instance,
maxCompensationAttempts: 5,
principal,
persistence,
registry,
emitter,
instruments,
requestContextFactory,
nowMs: () => Date.now(),
});
if (result.outcome === "compensated") {
persistence.transitionInstance(instance.id, "compensated");
}
Usage: Advanced
A consumer that wants to drive compensation manually (e.g. an
operator-triggered "rollback this saga" admin command) can call
this directly without going through start() or resume(). The
caller is responsible for transitioning the instance into the
matching terminal state afterwards:
import { runFullCompensation } from "@suaveplan/api-saga";
const result = await runFullCompensation({
instance: persistence.requireInstance(id),
maxCompensationAttempts: 3,
principal: null,
persistence,
registry,
emitter,
instruments,
requestContextFactory,
nowMs: () => Date.now(),
});
persistence.transitionInstance(
id,
result.outcome === "compensated"
? "compensated"
: "compensation_failed"
);
API Reference
runFullCompensation(args): Promise<CompensationFlowOutcome>.args:instance: SagaInstanceRow,maxCompensationAttempts: number,principal: Principal | null,persistence, registry, emitter, instruments, requestContextFactory, nowMs— injected dependencies.
CompensationFlowOutcome:{ outcome: "compensated" },{ outcome: "compensation_failed", failedStep: string }.
Implementation Notes
The compensation walk only considers step rows whose persisted
state is completed. Steps in pending, in_flight, or failed
have either not run or already failed; running compensation against
them would either invoke a compensate handler with no matching
output or repeat work the runner already declared lost.
updateStepPayload is invoked AFTER the compensate handler so the
persistence layer records the final attempt count rather than the
initial one. This matters for the operator-facing dashboards that
read the row to display "compensation took N attempts".