saga/testing
Waits in tests until a running workflow reaches a state, by polling
saga/execution.progress.
Use wait_until in a test that started a run with execution.start and
must act once a step reaches some state, for example to cancel the run
while a step is blocked. The rest of what such a test needs (a gate the
step body enters and the test releases) is ordinary BEAM concurrency;
see the execution design’s observation and verification section and the
order consumer’s test/support/gate.gleam for a recipe.
import gleam/list
import gleam/time/duration
import saga/execution
import saga/testing
let assert Ok(run) = execution.start(workflow, input, execution.config())
let attempting = fn(progress: execution.Progress) {
list.any(progress.steps, fn(step) {
case step.state {
execution.Attempting(_) -> True
_ -> False
}
})
}
let assert Ok(_progress) =
testing.wait_until(run, matching: attempting, within: duration.seconds(1))
execution.cancel(run)
Types
Why wait_until did not return a matching Progress.
execution.progress (unlike execution.await) has no single-owner
restriction — any process holding the Execution value may call it — so
there is no NotOwner case here to mirror execution.AwaitError’s.
pub type WaitError {
WaitTimedOut
RunEnded
}
Constructors
-
WaitTimedOutmatchingnever accepted a snapshot beforewithinelapsed overall. -
RunEndedThe run ended (the coordinator exited) before
matchingaccepted a snapshot —execution.progress’s ownExecutionEnded.
Values
pub fn wait_until(
execution: execution.Execution(o, e, u),
matching matching: fn(execution.Progress) -> Bool,
within within: duration.Duration,
) -> Result(execution.Progress, WaitError)
Polls execution.progress(execution, ..) until matching accepts a
snapshot, or within elapses overall, whichever comes
first. Each poll is a real (short) message round-trip with the
coordinator — never a fixed process.sleep — so this returns as soon as
matching is satisfied instead of waiting out a guessed duration. The
deadline is tracked with the monotonic clock, so it is immune to wall-clock
adjustments during the wait.
Polling can miss a state that passes through quickly. matching sees
only whatever phase/step-state snapshot happens to be current at each
poll; a state entered and left again between two polls is never observed.
Write matching to accept the target state or anything at or beyond
it, not the target state alone. For example, waiting for a step to reach
Compensating should also accept RetryScheduled (the decision may
already have been made and scheduled by the time a poll lands) rather
than failing because the run raced past Compensating before this
helper’s next poll — a predicate that only accepts Compensating itself
can time out on a perfectly healthy run.