Docket.Backend.TransitionStore behaviour (docket v0.2.0)

Copy Markdown View Source

Versioned semantic write contract for durable lifecycle transitions.

A transition store receives complete, substrate-neutral proposals. It owns the native atomic primitive used to validate and publish each proposal; core never passes an Elixir callback, transaction handle, or Docket.Runtime.Moment through this contract.

Version 1 defines three operations:

  • initialize/4 creates an initial run, its schedule/supporting state, and assigned events after validating the owner-scoped graph precondition.
  • commit_claimed/4 advances a run under both the checkpoint-sequence and claim-token fences.
  • commit_unclaimed/5 applies a signal/admin proposal under an optimistic checkpoint-sequence fence and never requires a claim.

Implementations validate the complete proposal before writing. Wrong-tenant and unknown resources both return :not_found. Validation precedes lookup, immutable identity precedes fences, and event validation precedes publication. A failed operation publishes no run, schedule, support, or event changes. Events are idempotent by canonical content at {run_id, seq}: a pre-existing identical event is accepted, and different content at a stored sequence returns :event_conflict.

The portable error algebra is:

  • :not_found — the scoped graph/run is absent (including tenant concealment);
  • :invalid_transition — malformed proposal, immutable mismatch, or invalid schedule/event identity;
  • :stale_checkpoint — a claim-token or checkpoint fence lost; core may refetch and re-evaluate a pure unclaimed mutation;
  • :conflict — the proposed run already exists in the owner scope;
  • :event_conflict — an existing event sequence has different canonical content;
  • {:retryable, reason} — an infrastructure failure that is safe to retry;
  • {:permanent, reason} — a non-retryable infrastructure failure.

Summary

Types

Data-only claim-fenced transition proposal.

Data-only initialization proposal.

Storage effect applied with a committed run transition.

Data-only optimistic transition proposal.

Callbacks

Atomically publishes one claim-fenced transition and its assigned events.

Atomically publishes one unclaimed transition under a checkpoint CAS fence.

Atomically creates an initialized run and its assigned events.

Functions

The transition contract version backends declare in capabilities/0 under transitions: %{version: ...}.

Types

claimed_proposal()

@type claimed_proposal() :: %{
  run: Docket.Run.t(),
  expected_checkpoint_seq: non_neg_integer(),
  claim_token: Docket.Backend.RunStore.claim_token(),
  checkpoint_type: Docket.Checkpoint.type(),
  schedule: schedule()
}

Data-only claim-fenced transition proposal.

  • :run — the complete next run value; its checkpoint_seq must equal expected_checkpoint_seq + 1.
  • :expected_checkpoint_seq — the committed checkpoint sequence this transition fences against.
  • :claim_token — the non-empty claim token that must still hold the run.
  • :checkpoint_type — the checkpoint type recorded for this transition.
  • :schedule — the claim disposition. :retain_claim requires a :running run; {:release_claim, :immediate} and {:release_claim, {:at, at}} require :running; {:release_claim, :external} requires :waiting; {:release_claim, :terminal} requires :done, :failed, or :cancelled.

ctx()

@type ctx() :: Docket.Backend.ctx()

error_reason()

@type error_reason() ::
  :not_found
  | :invalid_transition
  | :stale_checkpoint
  | :conflict
  | :event_conflict
  | {:retryable, term()}
  | {:permanent, term()}

init_proposal()

@type init_proposal() :: %{
  run: Docket.Run.t(),
  checkpoint_type: Docket.Checkpoint.type(),
  wake_at: DateTime.t()
}

Data-only initialization proposal.

  • :run — the complete initial run value; its checkpoint_seq is at least 1.
  • :checkpoint_type — must be :run_initialized.
  • :wake_at — the run's first explicit schedule.

owner_scope()

@type owner_scope() :: Docket.Backend.owner_scope()

result()

@type result() :: {:ok, Docket.Run.t()} | {:error, error_reason()}

schedule()

@type schedule() ::
  :retain_claim
  | {:release_claim, :immediate | :external | :terminal | {:at, DateTime.t()}}

Storage effect applied with a committed run transition.

:retain_claim keeps the current token, refreshes its claimed time, and leaves the run without a wake. A release clears the token and claimed time. :immediate records a wake at the backend's current time, {:at, time} records a future or current wake, and :external or :terminal records no wake. The two nil-wake reasons remain distinct here so implementations can validate the proposed run status.

scope()

@type scope() :: Docket.Backend.scope()

unclaimed_proposal()

@type unclaimed_proposal() :: %{
  run: Docket.Run.t(),
  checkpoint_type: Docket.Checkpoint.type(),
  schedule: schedule()
}

Data-only optimistic transition proposal.

Carries the same fields as claimed_proposal/0 except the claim token, which unclaimed transitions never require, and the expected checkpoint sequence, which commit_unclaimed/5 receives as its own argument because it is also the compare-and-swap input core re-reads on :stale_checkpoint. :retain_claim is not a valid unclaimed schedule.

Callbacks

commit_claimed(ctx, scope, claimed_proposal, list)

@callback commit_claimed(ctx(), scope(), claimed_proposal(), [Docket.Event.t()]) ::
  result()

Atomically publishes one claim-fenced transition and its assigned events.

commit_unclaimed(ctx, scope, expected_checkpoint_seq, unclaimed_proposal, list)

@callback commit_unclaimed(
  ctx(),
  scope(),
  expected_checkpoint_seq :: non_neg_integer(),
  unclaimed_proposal(),
  [Docket.Event.t()]
) :: result()

Atomically publishes one unclaimed transition under a checkpoint CAS fence.

Core may refetch and re-evaluate the pure mutation when this returns {:error, :stale_checkpoint}. Mutation evaluation must therefore be deterministic, bounded, and free of external side effects. No-change and mutation errors do not invoke this callback and publish nothing.

initialize(ctx, owner_scope, init_proposal, list)

@callback initialize(ctx(), owner_scope(), init_proposal(), [Docket.Event.t()]) ::
  result()

Atomically creates an initialized run and its assigned events.

Functions

version()

@spec version() :: pos_integer()

The transition contract version backends declare in capabilities/0 under transitions: %{version: ...}.