The behaviour a host implements for the durable timer queue a delayed route send is recorded on (ADR-0005, decision 5).
On the process-less shape no session holds anything across a resume -
those holds are the live session's own state and are not part of
Statifier.Position, and this package resumes on every delivery - so
the handler owns a delayed send and its <cancel> itself, and it owns
them by writing rows rather than by scheduling anything in memory. The
queue is the host's: this package states the shape and calls it,
and a host that registers no queue cannot serve a delayed send on a
route at all, which StatifierRouter.SendHandler answers as a refusal
rather than a silent drop.
The two keys are two keys
schedule/2 is handed an entry whose cancellation key is
{scope, send_id} and whose dedup key is the composed
StatifierRouter.Route.idempotency_key/0 riding beside it. They are
not one key, and the queue is keyed on the first.
A send id is not a key on its own. A generated one is minted off a
per-execution counter and an author-written one is reused verbatim, so
send_1 recurs in every execution on the host. A queue keyed on
send_id alone deletes other executions' timers on the first cancel it
serves.
The queue owes at-most-once on the dedup key. Under at-least-once
delivery the same delayed send can reach schedule/2 more than once,
with the same key each time, and a queue that appends a row per call
fires that send once per call. So a schedule/2 whose key the queue
already holds a row for adds no second row and answers :ok. Two
entries under one {scope, send_id} with different keys are two sends,
and both are kept.
What a cancel does
cancel/3 deletes that scope's rows for its send_id, and no
other scope's. It may legitimately match more than one row, because
spec 6.3 cancels every delayed send under an id, and a cancel matching
nothing is a no-op rather than an error. A cancel carries nothing that
identifies the route - %Statifier.Effect.Cancel{} has no event,
target or type - which is why entry/0 carries the route name it
was scheduled against.
A cancel for a send already fired is a no-op, and a fire for a send already cancelled must not happen: the row is the single decision point, and both operations are writes against it.
Firing a row
This behaviour has no fire callback, because firing is the host's: the
queue is the host's and so is whatever wakes it when delay_ms has
passed. A host fires a row without leaving this package's public
surface:
{:ok, {module, _registered}} = StatifierRouter.Config.route(config, nil, entry.route)
:ok = module.deliver(entry.config, entry.event, entry.key)StatifierRouter.Config.route/3 answers the module serving the route
name the row carries; a scope overrides a route's configuration, never
its existence (ADR-0005, decision 2), so the module does not depend on
the scope passed. The configuration handed to
StatifierRouter.Route.deliver/3 is the row's own config, the one
resolved under the delivery's scope when the send was scheduled, and
the key is the row's own key, on which the route owes at-most-once
in turn. :error from StatifierRouter.Config.route/3 means the route
name was unregistered after the row was written.
The fire deletes the row in the same write that decides it fires, so that a cancel and a fire of one row cannot both succeed.
Summary
Types
One delayed route send, as it is handed to schedule/2.
A registered timer queue: the module serving it and that module's own configuration.
Callbacks
Deletes scope's rows for send_id, and no other scope's, answering
how many rows it deleted. Deleting none is a no-op.
Records one delayed route send durably, keyed by {scope, send_id},
at most once per entry.key.
Functions
Whether module can serve as a timer queue: loadable and exporting
schedule/2 and cancel/3.
Types
@type entry() :: %{ scope: String.t(), send_id: String.t() | nil, route: String.t(), config: map(), event: Statifier.Event.t(), key: StatifierRouter.Route.idempotency_key(), delay_ms: non_neg_integer() }
One delayed route send, as it is handed to schedule/2.
scope and send_id are the cancellation key; key is the dedup key;
route is the route name the send was scheduled against, and config
that route's resolved configuration; event is the built event to
deliver when it fires; delay_ms is the effect's own delay.
A registered timer queue: the module serving it and that module's own configuration.
Callbacks
@callback cancel(queue_config :: map(), scope :: String.t(), send_id :: String.t()) :: {:ok, non_neg_integer()} | {:error, term()}
Deletes scope's rows for send_id, and no other scope's, answering
how many rows it deleted. Deleting none is a no-op.
Records one delayed route send durably, keyed by {scope, send_id},
at most once per entry.key.
entry.key is the dedup key (ADR-0005, decision 5): when the queue
already holds a row with that key, this adds no second row and
answers :ok. Entries with different keys under one
{scope, send_id} are separate rows.