StatifierPersistence.Executor behaviour (StatifierPersistence v0.22.0)

Copy Markdown View Source

The seam through which a stepped execution's effects reach the host (ADR-0004 decision 4).

An executor is a module implementing this behaviour, or an arity-2 fun accepted anywhere a module is. StatifierPersistence.Executions invokes it once per effect, in the effect list's own order, for every effect the lifecycle does not consume itself.

Summary

Types

What execute/2 receives alongside each effect: the execution's caller-supplied id and the content hash of the chart revision it runs - enough to key idempotency storage and telemetry without another lookup.

t()

An executor: a module implementing this behaviour, or an arity-2 fun with execute/2's own signature, accepted anywhere a module is (ADR-0004 decision 4).

Callbacks

Executes one effect against the outside world.

Types

context()

@type context() :: %{execution_id: String.t(), content_hash: String.t()}

What execute/2 receives alongside each effect: the execution's caller-supplied id and the content hash of the chart revision it runs - enough to key idempotency storage and telemetry without another lookup.

t()

@type t() :: module() | (Statifier.Effect.t(), context() -> :ok | {:error, term()})

An executor: a module implementing this behaviour, or an arity-2 fun with execute/2's own signature, accepted anywhere a module is (ADR-0004 decision 4).

Callbacks

execute(effect, context)

@callback execute(effect :: Statifier.Effect.t(), context :: context()) ::
  :ok | {:error, term()}

Executes one effect against the outside world.

The contract, per ADR-0004 decisions 3 and 4:

  • Effects arrive in list order, one call per effect.
  • Only the public effect vocabulary (Statifier.Effect.t/0) ever arrives - never Session instruction tuples (st-ADR-0054 decision 1).
  • :done and :budget_exhausted never arrive: the lifecycle consumes both into execution status itself.
  • At-least-once redelivery is the contract: a crash between step and persist re-drives the same event and re-emits the same effects carrying identical deterministic keys (st-ADR-0054 decision 3, st-ADR-0059's timer_counter ordinal). The loop never dedupes; idempotency by that key is the implementer's.
  • The call runs inside the step, before the new position is written, so a door of StatifierPersistence.Executions called from here for the same execution answers {:error, {:reentrant_step, execution_id}} rather than being served (ADR-0004's 2026-09-26 Amendment). A door for any other execution is served as usual.
  • That mark lives in the calling process, so a task the executor spawns that calls a door for the same execution is not refused: it meets the serialization strategy, which the step still holds. Under the default StatifierPersistence.Serialization.AdapterLock the task blocks on the execution's lock (the Ecto adapter's transaction-scoped advisory lock, the in-memory adapter's lock table) until the step returns, so an executor that awaits such a task hangs until its await gives up. StatifierPersistence.Executions.inputs/2 takes no lock and does not wait.