Routes external events to durable statifier executions, creating them when absent.
The front is Broadway: the host starts StatifierRouter.Broadway in its own
supervision tree with any producer, and partition_by keeps every message
for one key on one processor. Behind it sits a binding, addressing and
delivery layer over statifier_persistence.
What this package owns
- Bindings: source -> match -> key -> document -> event, with
matchandkeywritten as predicator programs over the normalized event. - The address table:
(scope, document, key)->execution_id. - Atomic get-or-create-and-deliver: the execution an address names is created when absent and handed the event in the same step.
- Dedupe on
(binding, message_id)with a horizon. - The recorded outcome vocabulary: every delivery attempt ends in one named outcome.
What it does not own
- Sinks and the route registry.
- Execution-to-execution sends.
- The source invoke.
- Any queue adapter.
- The webhook helper.
- Timers: those are
statifier_oban's. - A publish store: a host callback resolves a document to its active chart.
- Any process or supervisor: the host schedules the reapers and starts the pipeline.
scope is an opaque host string; the package gives it no meaning.
Of the pieces named above, this release builds the Broadway front, as
StatifierRouter.Broadway, and the binding, as
StatifierRouter.Binding. It builds the tables behind the rest,
created by StatifierRouter.Migrations and read through the schemas in
StatifierRouter.Schema. It builds route/3, which evaluates the
bindings for one event and hands each delivery to the configuration's
delivery module, and that module's default, StatifierRouter.Delivery,
which claims the message for the binding with StatifierRouter.Dedupe
and then, in the same transaction, gets or creates the execution an
address names and steps the event into it, under each of the three
create modes. It builds StatifierRouter.Resolver, the host's answer
to the chart a new execution starts on, with
StatifierRouter.Resolver.Static over charts compiled at boot. The
host schedules the two reapers,
StatifierRouter.Dedupe.reap/2 and StatifierRouter.Addresses.reap/2.
Each piece lands behind the decision record that fixes it, in
docs/adr/.
Routing an event
route/3 takes a StatifierRouter.Config, the event and options. The
event carries the host's scope beside its message id, its source and
the adapter-normalized event (ADR-0003, section 8). route/3 returns one
outcome per enabled binding whose source is the event's source, in the
order the configuration lists them (ADR-0004, section 6). Bindings are
chosen by source alone: a binding's selector is the source adapter's to
read, and the router never reads it (ADR-0001, section 1). For each
binding, the outcome is the first of these that applies (ADR-0004,
section 2):
- A
matchthat does not hold is{:no_match, binding_id}. It writes nothing durable (ADR-0004, section 5); it is reported as the telemetry event[:statifier_router, :route, :no_match], with the measurement%{count: 1}and the metadatabinding_id,source,scopeandmessage_id. - A
matchthat refuses, or akeythat refuses, is{:key_refused, binding_id, reason}, withreasonone of{:match, {:error, error}},{:match, {:value, value}},{:key, {:error, error}}and{:key, {:value, value}}(ADR-0004, section 1). One row is written to the routing ledger for it, on its own (ADR-0004, section 4), and the delivery module is not called. - Otherwise the delivery module is called, and its answer is the binding's outcome.
The ledger's reason column holds the reason term as inspect/1
renders it with its default options, so {:key, {:value, :undefined}}
is stored as that text. The encoding is for a person reading the
ledger; nothing parses it back.
An {:error, reason} from the delivery module ends the attempt: the
bindings after it are not evaluated, route/3 returns that error, and
what was already written for the bindings before it stays written
(ADR-0004, section 7). A raise inside a delivery is not rescued: it
propagates out of route/3 (ADR-0003, section 1).
The delivery seam
The configuration's :delivery module is called once for each binding
whose key produced one, as deliver(config, binding, key, delivery),
where delivery is a map of:
:name- the chart event, the binding'sevent;:data- the event's data projected through the binding'sdatapaths (StatifierRouter.Binding.project/2);:message_idand:scope- the event's own;:now- the time this attempt uses for the rows it writes.
It answers with one of {:delivered, binding_id, execution_id},
{:created_and_delivered, binding_id, execution_id},
{:duplicate, binding_id}, {:dropped, binding_id, :no_execution} and
{:dropped, binding_id, :finished} for the binding it was handed, or
with {:error, reason}, and it writes that outcome's rows inside the
delivery's own transaction (ADR-0003, section 1). Any other answer
raises ArgumentError. The default module is StatifierRouter.Delivery.
Summary
Types
What the delivery module is handed besides the configuration, binding and key.
One binding's outcome of one routing attempt (ADR-0004, section 1).
Which program refused an event for a binding, and how (ADR-0004, section 1).
The event a host hands route/3: the scope it routes under, the message
id its source adapter derived (a non-empty string, taken as given), the
source it came from, and the adapter-normalized event as a string-keyed
map. Other keys are ignored.
Functions
Routes one event through the configuration's bindings.
Returns this package's version, as mix.exs declares it.
Types
@type delivery() :: %{ name: String.t(), data: map(), message_id: String.t(), scope: String.t(), now: DateTime.t() }
What the delivery module is handed besides the configuration, binding and key.
@type outcome() :: {:delivered, String.t(), String.t()} | {:created_and_delivered, String.t(), String.t()} | {:duplicate, String.t()} | {:no_match, String.t()} | {:key_refused, String.t(), refusal_reason()} | {:dropped, String.t(), :no_execution | :finished}
One binding's outcome of one routing attempt (ADR-0004, section 1).
@type refusal_reason() :: {:match, {:error, term()}} | {:match, {:value, term()}} | {:key, {:error, term()}} | {:key, {:value, term()}}
Which program refused an event for a binding, and how (ADR-0004, section 1).
@type source_event() :: %{ :scope => String.t(), :message_id => String.t(), :source => String.t(), :data => map(), optional(atom()) => term() }
The event a host hands route/3: the scope it routes under, the message
id its source adapter derived (a non-empty string, taken as given), the
source it came from, and the adapter-normalized event as a string-keyed
map. Other keys are ignored.
Functions
@spec route(StatifierRouter.Config.t(), source_event(), keyword()) :: {:ok, [outcome()]} | {:error, term()}
Routes one event through the configuration's bindings.
Returns {:ok, outcomes}, one outcome per enabled binding whose source
is the event's source, in configuration order, and {:ok, []} when there
is no such binding. Returns {:error, reason} before any binding is
evaluated for an event whose message_id is nil or empty
(:no_message_id), for any other malformed event
({:invalid_event, event}) and for a malformed option
({:invalid_opts, opts}, {:unknown_key, name},
{:invalid_value, :now, value}), and for the first error the delivery
module answers with. The module documentation says what each outcome
writes.
opts:
:now- aDateTimein UTC, the time the attempt uses for the rows it and the delivery module write. Defaults toDateTime.utc_now/0.
@spec version() :: String.t()
Returns this package's version, as mix.exs declares it.
iex> is_binary(StatifierRouter.version())
true