saga/execution
Runs a saga.Workflow: run configuration and its validation, starting
a run, waiting for or inspecting its outcome, and cancellation.
Use this module to execute a workflow defined with saga in memory.
run is the ordinary path: it starts a run and blocks until it ends.
start and start_reporting run without blocking; await, progress
and cancel act on the returned Execution. saga/durable drives the
same workflow with saved checkpoints and returns the same Outcome, and
saga/testing polls progress for tests.
import gleam/time/duration
import saga/execution
// The caller's own failure type keeps both reports intact.
type Failure(o, e, u) {
NotStarted(execution.RunError)
Unfinished(execution.Outcome(o, e, u))
}
let config =
execution.config()
|> execution.with_max_concurrency(4)
|> execution.with_deadline(execution.After(duration.seconds(5)))
case execution.run(workflow, "order-1", config) {
Ok(execution.Completed(receipt)) -> Ok(receipt)
Ok(outcome) -> Error(Unfinished(outcome))
Error(run_error) -> Error(NotStarted(run_error))
}
NotStarted keeps the RunError that refused the run. Unfinished
keeps the Outcome, so the caller still reads the failure and its
settlement, and execution.unknown_effects(outcome) names the actions
whose effect is unknown when the caller needs them.
Defaults. config() gives one concurrent task per scheduler, no run
deadline, a 60 second per-attempt timeout, a 5 second settle window, a 5
second bound on each compensation decision and undo, and a 300 second
cap on RetryAfter delays; see config for the setter of each. start
waits up to 5 seconds for the run’s coordinator to start. run waits
until the run ends.
Who learns the outcome. run returns it; start delivers it to the
starting process, which alone may await it; start_reporting delivers
it as one message to a caller-supplied Subject, which a surviving
process can hold even after the starting process exits, or which the
starting process can add to its own Selector. In every case the
starting process owns the run: its exit cancels the run, which still
settles and rolls back before ending.
Resource bounds. Once a run stops admitting new work (on a step
failure, the deadline, or a cancellation), in-flight attempts and
compensations get up to settle_timeout to finish on their own before
they are killed, and each compensation decision or undo action is
bounded by cleanup_timeout. With a deadline, worst-case run time is
therefore deadline + settle_timeout + (undone entries + compensations) * cleanup_timeout. Without one, a run lasts as long as its steps: each
attempt is bounded by its timeout, and each step by its attempt budget
and its RetryAfter delays, each at most max_retry_delay. A step’s
worst case is therefore max_attempts * (attempt timeout + cleanup_timeout + max_retry_delay).
Cancellation never reverses an unknown effect. A step whose attempt
or compensation is killed — by its own timeout, or by the settle
window closing — is reported interrupted: its effect is unknown and is
never journaled or undone. Only steps that are known to have completed
are undone.
Known results and unknown effects. Every action a run performs — a
step attempt, a compensation decision, an undo — ends either with a
known result (it returned Ok or a typed error) or with an unknown
effect (it crashed or its process exited, it was killed at its time
bound, or it was killed when the settle window closed).
unknown_effects(outcome) names every action of the second kind, for
every outcome kind; it is [] exactly when every action returned.
Types
Which of a step’s actions ended with an unknown effect.
pub type Action {
StepAttempt(attempt: Int)
StepCompensation(attempt: Int)
StepUndo
}
Constructors
-
StepAttempt(attempt: Int)The step’s attempt with this number (the first is
1). -
StepCompensation(attempt: Int)The
saga.compensatedecider, deciding about the attempt with this number. -
StepUndoThe step’s
saga.undo, during rollback.
Why await did not return an Outcome.
AlreadyAwaited is reported for the ordinary case (a previous await
on this Execution already consumed its outcome) and also for a second
await after a previous one already reported Lost — both leave
nothing further to observe, since the coordinator is gone either way and
no fresh Down will ever arrive again for a monitor that already fired.
A caller that needs the crash detail should keep the first await’s own
Lost result instead of relying on a second call to repeat it.
Timing. If await is issued immediately after a previous await
already succeeded (returned Ok), it may still report
Error(AwaitTimedOut) rather than Error(AlreadyAwaited): succeeding
only means the coordinator has sent its outcome, not that the
coordinator process has actually exited yet, and AlreadyAwaited is
only guaranteed once it has. A caller relying on AlreadyAwaited to
detect a repeated await should either retry past a timeout or give a
long enough one, rather than treating a single short-timeout call right
after success as conclusive.
pub type AwaitError {
AwaitTimedOut
NotOwner
AlreadyAwaited
Lost(crash: saga.Crash)
}
Constructors
-
AwaitTimedOut -
NotOwnerThe calling process does not receive this run’s outcome: it is not the process that called
start, or the run was started withstart_reportingand delivers its outcome to a report subject. -
AlreadyAwaited -
Lost(crash: saga.Crash)
Why a run was cancelled: an explicit cancel call, or the owning
process exiting.
pub type CancelReason {
CancelRequested
OwnerExited
}
Constructors
-
CancelRequested -
OwnerExited
The run-ending cause behind a Failed or a sibling failure recorded in
a Settlement. Keeps an application failure (StepFailed), an
execution failure (StepCrashed/StepTimedOut), retry exhaustion, and
an output-transform crash as distinct variants, so no report can
conflate them.
A cause names the last failure of a step, not every attempt before
it. Whether any attempt, compensation decision or undo left an effect of
unknown status is reported by unknown_effects, never by the cause’s
variant.
pub type Cause(e) {
StepFailed(step: saga.StepAddress, error: e)
StepCrashed(step: saga.StepAddress, crash: saga.Crash)
StepTimedOut(step: saga.StepAddress)
RetryLimitReached(
step: saga.StepAddress,
last: saga.AttemptFailure(e),
)
RetrySuperseded(
step: saga.StepAddress,
last: saga.AttemptFailure(e),
)
OutputCrashed(crash: saga.Crash)
DeadlineExceeded
}
Constructors
-
StepFailed(step: saga.StepAddress, error: e)The step’s attempt returned
errorand the step has nocompensatedecider, or its decider choseAbort(error)orAbortAfterCleanupFailure(error, _).A
StepFailedmay follow a crash. A decider is asked about a crashed or timed-out attempt too (saga.Crashed/saga.TimedOut), and anAbortit returns is reported asStepFailedwith the decider’s error, exactly like an aborted typed error. The crashed or timed-out attempt is then named inSettlement.unknown_effects: a decider author who aborts after a crash should choose an error that says so, and a consumer should consultunknown_effectsrather than infer a known result fromStepFailed. -
StepCrashed(step: saga.StepAddress, crash: saga.Crash) -
StepTimedOut(step: saga.StepAddress) -
RetryLimitReached( step: saga.StepAddress, last: saga.AttemptFailure(e), ) -
RetrySuperseded( step: saga.StepAddress, last: saga.AttemptFailure(e), )A
Retry/RetryAfterdecision was refused because settling had already begun for a different, unrelated trigger, not because this step’s own attempt budget was exhausted — distinct fromRetryLimitReachedfor that reason. -
OutputCrashed(crash: saga.Crash) -
DeadlineExceeded
A compensation decision itself failed to produce a clean outcome.
pub type CompensationFailure(u) {
CleanupFailed(step: saga.StepAddress, error: u)
CompensationCrashed(step: saga.StepAddress, crash: saga.Crash)
CompensationTimedOut(step: saga.StepAddress)
}
Constructors
-
CleanupFailed(step: saga.StepAddress, error: u) -
CompensationCrashed(step: saga.StepAddress, crash: saga.Crash) -
CompensationTimedOut(step: saga.StepAddress)
The bounds, pacing and labels of a run. Build one with config and the
with_* setters; run, start, start_reporting and durable.drive
check it and return InvalidConfig with every violation.
pub opaque type Config
A single violated Config bound. InvalidConfig lists every one that
applies, not just the first.
pub type ConfigError {
MaxConcurrencyNotPositive(value: Int)
DeadlineNotPositive(value: duration.Duration)
StepTimeoutNotPositive(value: duration.Duration)
SettleTimeoutNegative(value: duration.Duration)
CleanupTimeoutNotPositive(value: duration.Duration)
MaxRetryDelayNegative(value: duration.Duration)
}
Constructors
-
MaxConcurrencyNotPositive(value: Int) -
DeadlineNotPositive(value: duration.Duration) -
StepTimeoutNotPositive(value: duration.Duration) -
SettleTimeoutNegative(value: duration.Duration) -
CleanupTimeoutNotPositive(value: duration.Duration) -
MaxRetryDelayNegative(value: duration.Duration)
A started run. Only the process that called start may await it; a
run started with start_reporting delivers its outcome to its report
subject instead, and cannot be awaited at all. Either way, cancel,
progress, pid and run_id work from any process.
Deliberately stateless on the owner’s side: no process-dictionary flag
or other bookkeeping outside this immutable value survives between
await calls, so starting and awaiting any number of Executions never
grows the owner’s process dictionary. ToOwner.monitor is the
original monitor set up once, in start — an ordinary value held on
Execution, not owner-process state, so keeping it costs nothing once
the Execution itself is dropped. It is what makes a coordinator that
dies abnormally before ever being awaited reliably reported as Lost:
a monitor set up while the coordinator was still alive is the only way
to see its real exit reason, since a monitor set up afterwards (once
the coordinator is already dead) always reports the synthetic reason
noproc instead. See await’s doc comment for how a second await
(after monitor has already fired and been torn down) is still
detected — without extra owner-side state — using a second, freshly
created monitor for that call only.
Lifetime. Every await that actually consumes a signal (an outcome,
or a Down) demonitors/drains as it returns, so nothing is left in the
owner’s mailbox once that call happens — but a call that never
happens cannot clean up after itself. An Execution you stop awaiting
(an await that timed out, followed by cancel, with no await
afterwards) can still leave a monitor Down or an unclaimed outcome
message sitting in the owner’s mailbox once the run eventually settles.
Always await again after cancel (even with a short timeout) so the
run’s terminal outcome is consumed before the Execution is dropped.
A start_reporting run sets up no monitor in the starting process and
sends it nothing, so it leaves nothing behind there.
pub opaque type Execution(o, e, u)
A run’s terminal result: success, a failure with its settlement, a
cancellation, or an unresolved Hold that left completed effects
untouched. Cancelled’s settlement follows the same rules as Failed’s:
completed steps are undone, and interrupted or not-undoable effects are
listed rather than claimed reversed.
CompletedWithUnknownEffects is Completed’s counterpart for a run
that reached its output although a step attempt crashed (or its process
exited) or was killed at its timeout, and the step’s recovery decider
then chose Retry/RetryAfter/Continue. That attempt’s own effect is
still unknown and was never journaled or undone — only the
replacement attempt (the retry, or Continue’s supplied output) is
known. Kept as its own variant (never an always-present field on
Completed) so a case matching only Completed cannot silently drop
the uncertainty. unknown_effects is never empty on this variant, and a
plain Completed means every action of the run returned a result.
pub type Outcome(o, e, u) {
Completed(output: o)
CompletedWithUnknownEffects(
output: o,
unknown_effects: List(UnknownEffect),
)
Failed(cause: Cause(e), settlement: Settlement(e, u))
Cancelled(reason: CancelReason, settlement: Settlement(e, u))
Unresolved(
step: saga.StepAddress,
evidence: e,
settlement: Settlement(e, u),
)
}
Constructors
-
Completed(output: o) -
CompletedWithUnknownEffects( output: o, unknown_effects: List(UnknownEffect), ) -
Failed(cause: Cause(e), settlement: Settlement(e, u)) -
Cancelled(reason: CancelReason, settlement: Settlement(e, u)) -
Unresolved( step: saga.StepAddress, evidence: e, settlement: Settlement(e, u), )
The run’s current admission phase: accepting new work, letting active
siblings settle after a terminal trigger, or undoing completed steps.
The coordinator reports its outcome and exits in the same step that
finishes rollback (or reaches Unresolved/Completed), so there is no
observable window for progress to report a “finishing” phase.
pub type Phase {
Running
Settling
RollingBack
}
Constructors
-
Running -
Settling -
RollingBack
A read-only snapshot of a run: its phase and every step’s state. Never exposes values or executable closures.
pub type Progress {
Progress(run_id: Int, phase: Phase, steps: List(StepProgress))
}
Constructors
-
Progress(run_id: Int, phase: Phase, steps: List(StepProgress))
pub type ProgressError {
ProgressTimedOut
ExecutionEnded
}
Constructors
-
ProgressTimedOut -
ExecutionEnded
Why run/start could not return an execution or report.
InvalidConfig starts no work. ExecutionLost does not prove that no
work started: a coordinator may act before its startup handshake is lost.
pub type RunError {
InvalidConfig(errors: List(ConfigError))
ExecutionLost(crash: saga.Crash)
}
Constructors
-
InvalidConfig(errors: List(ConfigError)) -
ExecutionLost(crash: saga.Crash)
What happened to every step once a run stopped admitting new work:
which were undone (in reverse completion order), which undo actions
failed (all retained, not just the first), which had no undo configured,
which were held by an unresolved Hold, which attempts or compensations
were killed by their step’s own timeout or in flight when the settle
window closed (interrupted), and which sibling failures settled after
the primary cause.
unknown_effects is the complete record: every action of the whole run
— attempt, compensation decision or undo, whether before or after the
run stopped admitting work — that ended with an unknown effect, in the
order they ended. It includes each interrupted entry, a crashed
attempt whatever its decider then chose, a crashed or timed-out
compensation, and a crashed or timed-out undo. It is [] exactly when
every action of the run returned a result.
pub type Settlement(e, u) {
Settlement(
undone: List(saga.StepAddress),
undo_failures: List(UndoFailure(u)),
not_undoable: List(saga.StepAddress),
held: List(saga.StepAddress),
interrupted: List(saga.StepAddress),
compensation_failures: List(CompensationFailure(u)),
sibling_failures: List(Cause(e)),
unknown_effects: List(UnknownEffect),
)
}
Constructors
-
Settlement( undone: List(saga.StepAddress), undo_failures: List(UndoFailure(u)), not_undoable: List(saga.StepAddress), held: List(saga.StepAddress), interrupted: List(saga.StepAddress), compensation_failures: List(CompensationFailure(u)), sibling_failures: List(Cause(e)), unknown_effects: List(UnknownEffect), )
pub type StepProgress {
StepProgress(address: saga.StepAddress, state: StepState)
}
Constructors
-
StepProgress(address: saga.StepAddress, state: StepState)
One step’s current state, as reported by progress.
pub type StepState {
Waiting
Attempting(attempt: Int)
Compensating(attempt: Int)
RetryScheduled(next_attempt: Int)
Succeeded
FailedStep
Interrupted
Undoing
Undone
UndoFailedStep
Skipped
}
Constructors
-
Waiting -
Attempting(attempt: Int) -
Compensating(attempt: Int) -
RetryScheduled(next_attempt: Int) -
Succeeded -
FailedStep -
Interrupted -
Undoing -
Undone -
UndoFailedStep -
Skipped
A time bound that may be lifted. Infinity lifts it and must be chosen
explicitly.
pub type Timeout {
After(duration.Duration)
Infinity
}
Constructors
-
After(duration.Duration) -
Infinity
One completed step’s undo did not succeed, during rollback.
pub type UndoFailure(u) {
UndoFailed(step: saga.StepAddress, error: u)
UndoCrashed(step: saga.StepAddress, crash: saga.Crash)
UndoTimedOut(step: saga.StepAddress)
}
Constructors
-
UndoFailed(step: saga.StepAddress, error: u) -
UndoCrashed(step: saga.StepAddress, crash: saga.Crash) -
UndoTimedOut(step: saga.StepAddress)
One action of a run that ended without a result, so its effect is unknown: it may or may not have happened, and saga never journaled or undid it. Recorded when the action ends, whatever is decided afterwards: a crashed attempt retried to success, continued, aborted or held is still named.
pub type UnknownEffect {
UnknownEffect(
step: saga.StepAddress,
action: Action,
ending: UnknownEnding,
)
}
Constructors
-
UnknownEffect( step: saga.StepAddress, action: Action, ending: UnknownEnding, )
How an action ended without a result.
pub type UnknownEnding {
ActionCrashed(crash: saga.Crash)
ActionTimedOut
ActionInterrupted
ActionReturnedUnknown
}
Constructors
-
ActionCrashed(crash: saga.Crash)It raised, or its process exited (for example, killed from outside).
-
ActionTimedOutIt was killed at its time bound: the step’s
timeout(orConfig.step_timeout) for an attempt,Config.cleanup_timeoutfor a compensation decision or an undo. -
ActionInterruptedIt was still running when the settle window closed, and was killed.
-
ActionReturnedUnknownThe attempt returned an error that
saga.unknown_whenclassified as a possible effect, such as a timeout after the request may have been sent.
Values
pub fn await(
execution: Execution(o, e, u),
timeout timeout: duration.Duration,
) -> Result(Outcome(o, e, u), AwaitError)
Waits up to timeout for the run’s outcome. Returns
Error(AwaitTimedOut) on timeout — the run continues, and await may
be called again. Returns Error(AlreadyAwaited) if a previous await
on this same Execution already consumed the outcome, or if a
previous await already reported Lost for it (see AwaitError’s doc
comment). Returns Error(Lost(crash)) if the coordinator was killed
externally before it could report an outcome, whether that happened
before this call started or during it (execution.pid lets
applications monitor it themselves independently, for comparison).
Timing. An await issued immediately after a previous await
already returned Ok may still return Error(AwaitTimedOut) rather
than Error(AlreadyAwaited): a successful outcome only means the
coordinator has sent it, not that the coordinator process has exited
yet. AlreadyAwaited is only guaranteed once the coordinator has
actually exited — see AwaitError’s doc comment.
Stateless on the owner’s side: no process-dictionary flag or other
owner-process bookkeeping is used to detect a repeated await — see
Execution/await_selector’s doc comments for how a fresh, per-call
monitor takes its place instead, so starting and awaiting any number of
runs never grows the owner’s process dictionary.
pub fn cancel(execution: Execution(o, e, u)) -> Nil
Requests cancellation. Returns immediately; the request is idempotent
and a no-op once settling or later has already begun. Cancellation stops
admission, lets active siblings settle within settle_timeout, then
kills whatever remains (reported interrupted) and rolls back known
completed effects. It never reverses an interrupted or not-undoable
effect.
The settle window is the run’s Config.settle_timeout, fixed at start;
a cancellation cannot shorten it, because the owner’s exit cancels a
run with no call to carry a value. It is an upper bound, not a delay:
settling ends as soon as nothing is in flight. A zero settle timeout
kills in-flight work at once, reporting it interrupted; a longer
window lets it finish, so that it is known and undone.
pub fn config() -> Config
The default configuration:
| Setting | Default | Setter |
|---|---|---|
| concurrent attempts and compensation decisions | schedulers online | with_max_concurrency |
| run deadline | Infinity | with_deadline |
| per-attempt timeout | 60 seconds | with_step_timeout |
| settle window | 5 seconds | with_settle_timeout |
| each compensation decision and undo | 5 seconds | with_cleanup_timeout |
RetryAfter delay cap | 5 minutes | with_max_retry_delay |
| correlation | unique() (local), from_key(id) (durable) | with_correlation |
Why the run has no default deadline. A step’s attempts are bounded
by the per-attempt timeout, its attempt budget (saga.compensate’s
max_attempts) and the capped RetryAfter delay, so every run already
has a finite worst case without a run-wide deadline (see the module
doc). A deadline also cuts healthy long workflows, so it is opt-in.
pub fn describe_cause(
cause: Cause(e),
error describe_error: fn(e) -> String,
) -> String
Describes a cause for logs, naming the step and rendering the step’s
error e with describe_error, for example
"step publish returned an error: HTTP 500".
pub fn describe_config_error(error: ConfigError) -> String
Describes a configuration error for logs.
pub fn kind(outcome: Outcome(o, e, u)) -> telemetry.OutcomeKind
The closed classification of an outcome, the same value that
[saga, run, stop] reports. CompletedWithUnknownEffects has its own
kind, so a match on the kind cannot mistake it for a plain completion.
pub fn pid(execution: Execution(o, e, u)) -> process.Pid
The coordinator’s pid, for monitoring.
pub fn progress(
execution: Execution(o, e, u),
timeout timeout: duration.Duration,
) -> Result(Progress, ProgressError)
Synchronously inspects the run’s current phase and per-step states.
Once the run has already ended, the coordinator process is gone and
control is a dead subject: sending a request to it is a silent no-op,
so without a monitor this would always time out rather than report
ExecutionEnded promptly. A short-lived monitor (armed only for this
one call, always demonitored with [flush] before returning) races the
reply against the coordinator’s exit instead.
pub fn run(
workflow: saga.Workflow(i, o, e, u),
input: i,
config: Config,
) -> Result(Outcome(o, e, u), RunError)
Runs workflow with input to completion, validating config first.
This is the ordinary path: it blocks until the run finishes.
pub fn run_id(execution: Execution(o, e, u)) -> Int
The run’s id, the run field of its saga/telemetry events.
pub fn start(
workflow: saga.Workflow(i, o, e, u),
input: i,
config: Config,
) -> Result(Execution(o, e, u), RunError)
Starts a run without blocking, returning an Execution handle. Only the
calling process may await it. Returns Error(InvalidConfig(_)) with
every violated bound of config, or Error(ExecutionLost(_)) if the
coordinator process fails to complete its startup handshake within 5
seconds (it should not normally take anywhere near that long; this
guards against a wedged or unschedulable coordinator rather than
reporting it as a caller configuration error).
pub fn start_reporting(
workflow: saga.Workflow(i, o, e, u),
input: i,
config: Config,
to report: process.Subject(Outcome(o, e, u)),
) -> Result(Execution(o, e, u), RunError)
Starts a run like start, but delivers its outcome to report as one
message instead of to the calling process. report may belong to any
process — the caller itself, to receive the outcome in its own
Selector next to its other messages, or another process, which then
learns the outcome even if the caller is gone — or be a
process.named_subject, resolved when the outcome is sent.
Ownership. The calling process is still the run’s owner: if it
exits before the run ends, the run is cancelled with OwnerExited,
settles and rolls back exactly as for cancel, and the resulting
Cancelled(OwnerExited, settlement) is still delivered to report.
The settlement names every step undone, every undo or compensation that
failed or timed out, and every step interrupted with an unknown effect.
Delivery. report receives at most one message per run, sent by the
run’s coordinator when the run ends, never before. It receives exactly
one unless the coordinator itself is killed (then nothing is sent) or,
for a named subject, no process holds the name when the outcome is sent
(then the outcome is dropped and the coordinator still exits normally).
To detect a lost run, the receiver monitors pid(execution): the
coordinator’s Down always arrives after its outcome, so a Down with
no outcome before it means the run was lost. A monitor set up after the
coordinator exited still fires (with reason noproc) and still arrives
after the outcome, if one was sent.
await on the returned Execution reports Error(NotOwner), from any
process; cancel, progress, pid and run_id work as for start.
The starting process gets no monitor and no message from the run.
pub fn unknown_effects(
outcome: Outcome(o, e, u),
) -> List(UnknownEffect)
Every action of the run that ended with an unknown effect, in the order
they ended: [] for Completed, the variant’s own list for
CompletedWithUnknownEffects, and settlement.unknown_effects for
Failed, Cancelled and Unresolved. [] means every step attempt,
compensation decision and undo that ran returned a result, so no effect
of the run is of unknown status; effects a result left in place (an
undo that returned an error, a step with no undo, a held step) are
reported by the settlement, not here.
pub fn with_cleanup_timeout(
config: Config,
timeout: duration.Duration,
) -> Config
Bounds each compensation decision and each undo action. Must be at least 1 millisecond.
pub fn with_correlation(
config: Config,
correlation: correlation.Correlation,
) -> Config
Carries correlation in every saga/telemetry event of the run, so the
run can be followed with the caller’s other work. The run’s steps read it
as correlation of the saga.EffectKey that saga.effect, saga.undo
and saga.compensate receive, and pass it to the clients they call.
Every run has a correlation. A local run without one gets a fresh
correlation.unique() at start. A durable execution without one carries correlation.from_key of its execution id; its first
drive saves the correlation it uses, and later drives read it back (see
durable.with_correlation).
pub fn with_deadline(config: Config, deadline: Timeout) -> Config
Ends the run with DeadlineExceeded after deadline, settling and
rolling back like any failure. Infinity (the default) sets no deadline.
A duration must be at least 1 millisecond.
pub fn with_max_concurrency(config: Config, limit: Int) -> Config
Bounds how many step attempts and compensation decisions of one run execute at once. Must be at least 1.
pub fn with_max_retry_delay(
config: Config,
delay: duration.Duration,
) -> Config
Caps the delay of a RetryAfter decision. A longer requested delay is
shortened to the cap, and the decision’s
telemetry.CompensationMetadata reports retry_delay_capped: True.
Must not be negative.
pub fn with_settle_timeout(
config: Config,
timeout: duration.Duration,
) -> Config
How long in-flight attempts and compensation decisions may finish on
their own once the run stops admitting work, before they are killed and
reported interrupted. A zero duration kills them at once. Must not be
negative.
pub fn with_step_timeout(
config: Config,
timeout: Timeout,
) -> Config
Bounds each attempt of a step that has no saga.timeout of its own.
A step’s own saga.timeout always wins, shorter or longer. Infinity
leaves such attempts unbounded: the explicit opt-out of the 60 second
default. A duration must be at least 1 millisecond.