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

  • WaitTimedOut

    matching never accepted a snapshot before within elapsed overall.

  • RunEnded

    The run ended (the coordinator exited) before matching accepted a snapshot — execution.progress’s own ExecutionEnded.

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.

✨ Search Document