saga/storage/conformance

Checks a storage adapter against the saga/storage contract.

Use this module from an adapter’s own tests; it needs no test framework. run gives each scenario a fresh fixture and fresh execution ids, and checks atomic creation, unchanged data after refused writes, claims as values (any holder may commit; a rebuilt claim with another token is refused), revision and generation checks, cancellation races, release, that a live owner keeps its claim, that a lost owner’s claim ends within owner_loss_within, the unfinished listing, and that a saga/durable drive whose runner is killed frees the execution at once for the next drive, which resumes from the checkpoint. One fixture may serve several executions, as a database pool does. The memory, file and PostgreSQL adapters pass the same checks.

A pass covers the storage protocol from one VM; it does not certify cross-node fencing, media durability or power-loss behavior.

import gleam/time/duration
import saga/storage/conformance

let result =
  conformance.run(
    fn() {
      let assert Ok(store) = memory.start()
      Ok(conformance.fixture(memory.storage(store), cleanup: fn() {
        memory.stop(store)
      }))
    },
    timeout: duration.seconds(5),
    owner_loss_within: duration.milliseconds(500),
  )

owner_loss_within is the adapter’s declared window for noticing that an owner is gone: near zero for the memory adapter, which watches the claiming process, and the lease duration for a lease-based adapter.

Types

Why the suite failed. The union is closed.

pub type Failure {
  SetupFailed(String)
  UnexpectedResult(check: String)
  AdapterCrashed(String)
  TimedOut
  InvalidTimeout
}

Constructors

  • SetupFailed(String)

    The fixture factory returned this error.

  • UnexpectedResult(check: String)

    The named check saw an unexpected result.

  • AdapterCrashed(String)

    An adapter operation raised; the formatted exception, for logs.

  • TimedOut

    A scenario did not finish in time.

  • InvalidTimeout

    timeout or owner_loss_within was below 1 millisecond.

A storage under test and how to clean it up after one scenario.

pub opaque type Fixture

Values

pub fn fixture(
  storage: storage.Storage,
  cleanup cleanup: fn() -> Nil,
) -> Fixture

Builds a fixture.

pub fn run(
  fresh: fn() -> Result(Fixture, String),
  timeout timeout: duration.Duration,
  owner_loss_within owner_loss_within: duration.Duration,
) -> Result(Nil, Failure)

Runs each scenario on a fresh fixture. timeout bounds each scenario’s storage work, and scenarios that wait for an owner to be lost get owner_loss_within on top. The factory and cleanup run in the caller; adapter operations run in test workers. Cleanup runs after the scenario worker exits, including on failure or timeout.

✨ Search Document