The delivery module StatifierRouter.Config names by default: one
binding's delivery of one event, as one transaction on the host's repo
(ADR-0003, section 1).
It has two doors onto one transaction. deliver/4 is the seam
StatifierRouter.route/3 calls for a binding, and deliver_event/4 the
one StatifierRouter.SendHandler calls for an execution-to-execution
send (ADR-0006, section 2), which brings its own event and its own
idempotency key and is otherwise settled here exactly as a binding's
delivery is.
deliver/4 reads five options of the configuration, four of them
required:
:store- the%StatifierPersistence.Storage{}executions are kept in. It must be built over the configuration's own:repo, so thatStatifierPersistence.Executions.create/4andStatifierPersistence.Executions.step/5write through the delivery's transaction rather than opening their own (statifier_persistence's README, "Writing inside a caller's transaction").:executor- theStatifierPersistence.Executorboth doors hand their effects to.:resolver- theStatifierRouter.Resolver, a module or an arity-2 fun, answering{content_hash, machine}or{:error, reason}for(scope, document): the chart a new execution ofdocumentstarts on (ADR-0002, section 4). It is called only when the delivery is about to create an execution. Thecontent_hashit answers is not carried anywhere: the machine is handed on, and statifier_persistence records the hashStatifier.Machine.identity/1derives from that machine, which is the hash:chart_resolveris later asked for (ADR-0002, the 2026-09-20 Note).:chart_resolver-(content_hash) -> {:ok, machine}or:error: the compiled chart an existing execution started on, whichstep/5is handed. An existing execution keeps the chart it started on, so this is looked up by the content hash its record carries, never by its document.:persistence_options- the per-call snapshot options both doors carry,:routes,:invoke_typesand:send_types. They reach astep/5beside the event and acreate/4inside itsinitialize::Statifier.MachineState.new/2is the one writer of the fields they set, and a create has no stored position to stamp (statifier_persistence'screate/4option docs, at statifier_persistence 9cd192b; its own driver placesinvoke_types:the same way).
What one delivery does
The transaction first claims (binding_id, message_id) with
StatifierRouter.Dedupe.claim/4, its first write under every create
mode. When the pair's row is present and unexpired, the delivery is a
duplicate, and the transaction writes the ledger row and nothing else:
no address row is read or written (ADR-0003, section 6). Past the claim,
the binding's create decides the rest (ADR-0003, section 4):
:if_absentreads the address row for(scope, document, key). When there is none, it mints an execution id and inserts the row with it, an insert that inserts nothing on a conflict with the unique index and does not fail the transaction; when it inserted nothing, another delivery's row won the race, and a following statement reads that row (ADR-0003, section 3). For the row it inserted, it asks the resolver for the chart and callscreate/4under the minted id; for a row it read, it reads the execution's status.:neverreads the address row and never inserts one. When there is none, the outcome is{:dropped, binding_id, :no_execution}: nothing is created or stepped, and the ledger row and the claim's dedupe row are the delivery's only writes. For a row it read, it reads the execution's status, as:if_absentdoes.:always_newneither reads nor writes an address row (ADR-0002, section 7). It mints an execution id, asks the resolver for the chart and callscreate/4under that id, for every delivery; the minted id is the only handle on the execution.
Then it calls step/5 with the event, unless the execution is
terminal, writes the ledger row (ADR-0004, section 4) and commits.
The outcomes are {:created_and_delivered, binding_id, execution_id}
when this delivery created the execution, {:delivered, binding_id, execution_id} when it stepped one that existed, {:duplicate, binding_id} when the claim found the pair already handled,
{:dropped, binding_id, :no_execution} for a :never binding whose
address has no row, and {:dropped, binding_id, :finished} when the
execution was terminal: read terminal before the step, created already
terminal, or answered {:discarded, execution} by step/5 (ADR-0004,
section 3). A terminal sighting through an address row stamps the row's
terminal_seen_at when it is still empty (ADR-0002, section 5); the
row itself is left in place, and removing it is
StatifierRouter.Addresses.reap/2's. Every outcome but the duplicate
commits the dedupe row the claim wrote, a drop included, and a
duplicate's ledger row carries the key and no execution id (ADR-0004,
section 4).
Nothing here writes the input log: step/5 appends the event it steps
(ADR-0003, section 1).
The completion hook
StatifierRouter.Config's :on_complete names a registered route an
execution's donedata is handed to on the delivery that finishes it. It
fires from the call that produced the termination - the create/4 or
the step/5 whose answer carries a terminal execution - and from no
other, because that answer is the only place the donedata exists:
StatifierPersistence.Execution.from_record/1's own documentation says
a stored record carries none (its ADR-0008 decision 3), so it sets the
field to nil on every struct built from a row. A delivery to an
execution that was already terminal answers
{:dropped, binding_id, :finished} and never reaches the hook.
The route is handed a done.execution event whose data is the
donedata verbatim - :undefined, statifier's no-value marker, for a
<final> that carries none - and whose origin is the execution id, under an idempotency key of that execution
id, the counters the answering state reports, and no ordinal. It runs
inside this delivery's transaction like any other route, and an
{:error, reason} from it settles the delivery as
{:error, {:on_complete, route_name, reason}}.
What a route may not do while a delivery runs
A route called at the executor seam runs inside this transaction, under
the execution's lock, and ADR-0005 decision 5 forbids it to call back
into the sending execution: a nested step would run from the position
the outer step has not written yet and would then be overwritten by it.
deliver/4 refuses while StatifierRouter.SendHandler.sending_execution/0
names an execution, answering
{:error, {:reentrant_route, execution_id}} before it opens anything,
so a route that calls StatifierRouter.route/3 steps nothing and writes
nothing. A route that calls
StatifierPersistence.Executions.step/5 directly reaches past this
door; the record forbids that call and this package has no guard for it.
The scope a delivery runs under is also set for the length of the call, because the executor seam's context carries an execution id and a content hash and no scope, and a per-scope route override needs one. The seam runs in this same process, inside this transaction.
An {:error, reason} from create/4, step/5 or
StatifierPersistence.Storage.fetch_execution/2,
{:error, {:unresolved_document, document, reason}} when the resolver
answers {:error, reason}, and
{:error, {:chart_not_resolved, content_hash}} when the chart resolver
answers :error, roll this delivery's own transaction back and are
returned. On deliver/4 that rolls back the whole transaction, since it
is the outermost one; deliver_event/4 rolls back to a savepoint of its
own instead and never touches the transaction it was called inside, and
its own documentation says why. A raise rolls back the same way and
propagates; nothing here rescues it. Effects the executor was handed
before a rollback stay fired (ADR-0003, section 2).
The execution id is a UXID with the prefix ex, minted by
UXID.generate!/1; nothing is derived from the address (ADR-0002,
section 3).
Summary
Types
The message one delivery carries: the event stepped into the execution, the id its dedupe row and its ledger row are written under, the scope it runs in and the time its rows carry.
What one delivery settles, past the event itself: the name its ledger row
and its dedupe claim are written under, the document its address names,
the create mode a miss follows, and the dedupe horizon that claim uses.
Types
@type envelope() :: %{ :event => Statifier.Event.t(), :message_id => String.t(), :scope => String.t(), :now => DateTime.t(), optional(atom()) => term() }
The message one delivery carries: the event stepped into the execution, the id its dedupe row and its ledger row are written under, the scope it runs in and the time its rows carry.
@type plan() :: %{ :id => String.t(), :document => String.t(), :create => :if_absent | :never | :always_new, :dedupe => %{by: :message_id, horizon_ms: pos_integer()}, optional(atom()) => term() }
What one delivery settles, past the event itself: the name its ledger row
and its dedupe claim are written under, the document its address names,
the create mode a miss follows, and the dedupe horizon that claim uses.
A StatifierRouter.Binding.t/0 is one of these, and it is the one
deliver/4 builds. The other is ADR-0006's execution target: the
reserved name execution, the document param the chart wrote, the
create param it wrote, and ADR-0001, section 1's default horizon, since
no binding supplies one there.
Functions
@spec deliver( StatifierRouter.Config.t(), StatifierRouter.Binding.t(), String.t(), StatifierRouter.delivery() ) :: StatifierRouter.outcome() | {:error, term()}
Delivers one event for one binding under key, as the module
documentation describes, and answers with the outcome or
{:error, reason}.
@spec deliver_event(StatifierRouter.Config.t(), plan(), String.t(), envelope()) :: StatifierRouter.outcome() | {:error, term()}
Delivers one prebuilt event under plan, in the same transaction and the
same order deliver/4 uses, and answers with the same outcomes.
This is the door ADR-0006, section 2 names: an execution-to-execution
send resolves its address, gets or creates its execution and writes its
dedupe and ledger rows through this one function, so neither the unique
index race nor the ledger has a second implementation here. Its caller
owns three things deliver/4 owns for a binding: it builds the event
(ADR-0006, section 4 gives an execution-to-execution send the engine
builder rather than Statifier.Event.external/2), it composes the
message_id, and it decides whether a scope override is in force.
StatifierRouter.SendHandler is that caller.
The reentrancy refusal deliver/4 opens with is not repeated here. It
guards the route door, and an execution-to-execution send never enters a
route: StatifierRouter.SendHandler branches on the reserved target name
before it marks a route as running.
This door takes a savepoint, and it does not roll back
deliver/4 is the outermost transaction on its path, so the rollback its
error arm takes ends that transaction and answers its caller
{:error, reason}. This door is not outermost: it is called at the
executor seam, inside the sending execution's own step/5, which
statifier_persistence has already opened a transaction for. A
Ecto.Repo.rollback/1 there would take the sender's transaction
with it, answer a bare :rollback in place of the reason, and raise
DBConnection.ConnectionError out of the sending step's persist tail. And
mode: :savepoint does not prevent it: DBConnection.transaction/3
ignores its options once the connection is already in a transaction
(db_connection 2.10.2, the conn_mode: :transaction clause), so a nested
Ecto.Repo.transaction/2 gets no savepoint to roll back to.
That outcome is the opposite of what the records require: ADR-0005,
section 7 has the handler's {:error, reason} leave the sending step
standing, and ADR-0006, section 3 rests on it. So this door rolls nothing
back. It brackets the delivery in an explicit SQL savepoint of its own and
answers the reason as an ordinary return:
- on an outcome, the savepoint is released and the delivery's writes stand with the rest of the enclosing transaction;
- on a delivery-level error - an unresolved document, a chart that does
not resolve, a refusal from
create/4,step/5orStatifierPersistence.Storage.fetch_execution/2- the transaction rolls back to that savepoint, which undoes every write this delivery made on the target side (the dedupe row it claimed, the address row it inserted, the execution it created) and nothing else, and{:error, reason}carrying the real reason is returned.
The savepoint's name is minted from System.unique_integer/1, never from
anything a caller supplies, and a nested delivery gets its own. The host's
:repo therefore has to answer query!/1 as well, which every
Ecto.Adapters.SQL repo does; this package's tables are Postgres already.
A raise is the one case that still reaches the enclosing transaction, exactly as it does on the binding path: nothing here rescues, and the record leaves a raise as a raise.