# `StatifierPersistence.Executor`
[🔗](https://github.com/riddler/statifier_persistence/blob/v0.25.1/lib/statifier_persistence/executor.ex#L1)

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.

# `context`

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

What `c: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`

```elixir
@type t() :: module() | (Statifier.Effect.t(), context() -&gt; :ok | {:error, term()})
```

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

# `execute`

```elixir
@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 (`t: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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
