StatifierRouter.Route behaviour (StatifierRouter v0.9.2)

Copy Markdown View Source

The behaviour a host implements for one named outbound destination (ADR-0005, decision 2). A chart names a route in a <send>'s target under a type the host registered, and StatifierRouter.SendHandler hands the built event to the module registered for that name.

<send type="myapp:sink" target="joined_records" event="joined">
  <param name="impression_id" expr="impression_id"/>
</send>

A route is one-way

StatifierRouter.Route.deliver/3 answers :ok or {:error, term()} - handed off, or not handed off - and returns no data (ADR-0005, decision 3). A sink's result comes back as a new inbound event through a binding (ADR-0001), never as this callback's return. The one thing an {:error, _} causes in the sending execution is error.communication carrying the send's sendid, which is transport failure rather than an answer, and an adapter that swallows its own errors and answers :ok removes that transition from every chart on its route.

What a route may do where it is called

At StatifierPersistence.Executor.execute/2 a route runs inside the delivery's transaction, under the execution's lock, so it may only hand off durably: a job inserted on the host's own repo from the calling process joins that transaction, which is a transactional outbox and closes ADR-0003 section 2's window where a rollback does not un-fire what the executor was handed. A route called there must never call StatifierRouter.route/3, StatifierPersistence.Executions.step/5 or any other door of the sending execution; StatifierRouter.Delivery.deliver/4 refuses the call it can see, and StatifierRouter.SendHandler's own documentation says what that refusal does and does not reach.

The README's "A transactional outbox, end to end" walks the whole path: the insert inside the delivery's transaction, the unique key with on_conflict: :nothing, the drain after commit, the key carried to the sink as its own idempotency key, the live-session shape that has no delivery transaction, and the answer that comes back as an inbound event.

Idempotency

Neither host shape supplies an idempotency key, so the router composes one (ADR-0005, decision 4) and hands it over as idempotency_key/0. A route owes at-most-once on it. Effect execution is at-least-once: a crash between the step and the write re-drives the same event and re-emits the same effects with identical deterministic fields, and a delivery that rolled back is redriven the same way.

Adapter configuration

The first argument is the route's own configuration, as the host registered it under StatifierRouter.Config's :route_adapters, with any :route_overrides entry for the scope the delivery ran under already applied. A scope overrides a route's configuration and never its existence.

Summary

Types

The key StatifierRouter.SendHandler composes for one send (ADR-0005, decision 4): the scope half, where in the step the send sat, and the ordinal.

Where in the step one send sat: the deterministic fields Statifier.Send.Processor requires a processor to be idempotent on, read off the effect. Every field is the effect's own.

t()

A registered route: the module serving it and that module's own configuration.

Callbacks

Hands one built event off to this route's destination, at most once per key. Answers :ok when it was handed off and {:error, reason} when it was not.

Functions

Whether module can serve as a route: loadable and exporting deliver/3. StatifierRouter.Config holds every registered adapter to this, as it holds a resolver to StatifierRouter.Resolver's internal validity check.

Types

idempotency_key()

@type idempotency_key() ::
  {scope :: String.t(), position :: position(), ordinal :: pos_integer() | nil}

The key StatifierRouter.SendHandler composes for one send (ADR-0005, decision 4): the scope half, where in the step the send sat, and the ordinal.

The scope half is execution_id at the executor seam (StatifierPersistence.Executor's context) and session_id on the send-processor shape (Statifier.Send.Processor's t:ctx/0); it is the only part of the key that differs by host shape.

ordinal is nil in two cases: a send of a type the session did not register, which this handler never sees, and StatifierRouter.Config's :on_complete hook, which is not a <send> at all and has no ordinal to carry.

position()

@type position() :: %{
  send_id: String.t() | nil,
  macrostep: non_neg_integer(),
  microstep: non_neg_integer(),
  round: non_neg_integer(),
  c_index: non_neg_integer() | nil,
  owner: term()
}

Where in the step one send sat: the deterministic fields Statifier.Send.Processor requires a processor to be idempotent on, read off the effect. Every field is the effect's own.

t()

@type t() :: {module(), map()}

A registered route: the module serving it and that module's own configuration.

Callbacks

deliver(route_config, event, key)

@callback deliver(
  route_config :: map(),
  event :: Statifier.Event.t(),
  key :: idempotency_key()
) ::
  :ok | {:error, term()}

Hands one built event off to this route's destination, at most once per key. Answers :ok when it was handed off and {:error, reason} when it was not.

Functions

valid?(module)

@spec valid?(term()) :: boolean()

Whether module can serve as a route: loadable and exporting deliver/3. StatifierRouter.Config holds every registered adapter to this, as it holds a resolver to StatifierRouter.Resolver's internal validity check.

iex> StatifierRouter.Route.valid?(StatifierRouter.Route)
false