Skip to content

Adds an optional whole-delivery wrapper - #152

Merged
johnnyt merged 2 commits into
mainfrom
sr-t36w-whole-delivery-wrapper
Oct 2, 2026
Merged

johnnyt merged 2 commits into
mainfrom
sr-t36w-whole-delivery-wrapper

Conversation

@johnnyt

@johnnyt johnnyt commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Adds :around_delivery, one optional configuration option that runs a whole delivery inside a host's own context, and records it as an Amendment on ADR-0003 at proposed (in this PR, appended at the foot; zero removed lines under docs/adr/).

What changes

  • StatifierRouter.Config takes :around_delivery: a module exporting around_delivery/3 or an arity-3 fun, handed (scope, door, work). It must call work exactly once and answer what work answered; anything else raises ArgumentError (Config.around_delivery/4, package-internal). new/1 checks the shape as it checks the hooks.
  • Doors that call it: :route (StatifierRouter.route/3: the bindings read, every key_refused row and every delivery, so also StatifierRouter.Webhook.handle/3 and each Broadway message), :partition (StatifierRouter.Broadway.partition/3's bindings read, in the producer), :basichttp (StatifierRouter.BasicHTTP.Front's delivery, after the token resolves, which stays outside).
  • Not wrapped: the execution-target door (SendHandler calling Delivery.deliver_event/4). At the executor seam it runs inside the sender's wrapped step; on the send-processor shape, and from a step the router did not drive, nothing of the router's encloses it. The record says so per shape.
  • Left out, nothing is called: the router issues the same statements and answers the same.
  • README: the hooks section drops its open-gap sentence and points at a new ### Wrapping a whole delivery subsection. Moduledocs of Config, Delivery, Broadway, BasicHTTP.Front and route/3 describe the door each owns.
  • Changelog fragment under Added.
  • A second commit, after the first review pass: the wrapper is called inside try/catch, so the work's report is drained from the caller's mailbox on every way out of the wrapper, and a failing wrapper is re-raised with its own kind, reason and stacktrace. Reports stay messages rather than a process-dictionary counter, because a wrapper may run the work in another process.

Provenance

  • The shape (the wrapper takes the scope, the front resolves outside it, the execution-target door is not wrapped) was ruled by the operator, 2026-10-01.
  • Engineering choices made in this PR, recorded in the Amendment: the option name :around_delivery and the door atoms :route, :partition, :basichttp; holding the wrapper to its contract with ArgumentError (a wrapper that returns a Repo.transaction/2 answer would otherwise pass {:ok, answer} off as a routing answer); wrapping the front at its own delivery call rather than inside deliver_event/4, because SendHandler calls that function too.
  • lib/statifier_router/delivery.ex gains only a moduledoc paragraph saying the wrapper is called by its callers.

Tests

test/statifier_router/around_delivery_test.exs, not async (Broadway's processes need the shared sandbox, as BroadwayTest does, and a telemetry handler on the test repo's query event captures every statement with the process-dictionary context it ran under):

  • the option's shape;
  • a configuration without it issues the statements captured at bbe6c16, before the option existed (normalized for savepoint numbers, parameter numbers and Ecto's column order), for a create, a BasicHTTP POST, a finishing step, a duplicate and a key_refused row;
  • route/3: one :route call, the bindings resolver and every statement, both ledger inserts included, under the context;
  • Webhook.handle/3 through route/3;
  • a wrapper opening a transaction: a transaction-local setting reaches each step, and both deliveries of one call roll back together;
  • Broadway: a :partition call in the producer whose resolver call sees the context, a :route call in the processor whose statements carry it;
  • the front: the token lookup runs with no context, the delivery under the row's scope;
  • the execution target: inside the sender's step at the executor seam (one wrapper call, the target's statements under the sender's context), and unwrapped on the send-processor shape;
  • the wrapper contract (a foreign answer, no call, two calls) and the module form;
  • a wrapper that raises, exits or throws after its work, or raises before it: its own error propagates and no report of the work is left in the caller's mailbox.

Each test carries a # sabotage: note from a mutation actually run; every mutation applied, failed its test on an assertion, and was restored byte-equal before the next.

Gate

Full mix quality green locally on the rebased head (448 of 448 tests, 98.0% coverage, isolated tests, Dialyzer, Docs, Doc links, Credo).

Review tier

Contract: a new public configuration option on the doors it wraps. Opened for the conductor's cold review; not merged by its author.

Config takes :around_delivery, handed (scope, door, work), which must
call work exactly once and answer what it answered. route/3 runs its
bindings read, key_refused rows and deliveries inside one call (door
:route, so the webhook front and each Broadway message too), the
Broadway partitioner its bindings read (:partition), and the BasicHTTP
front its delivery after the token resolves (:basichttp). The
execution-target door is not wrapped. Left out, the router issues the
same statements and answers as before, pinned against statements
captured at bbe6c16.

ADR-0003 gains an Amendment at proposed recording the doors, the
execution target per shape, and what a context reaches.

Refs: sr-t36w
A wrapper that raised, exited or threw after its work returned left
the work's report in the caller's mailbox: in Broadway's producer at
the :partition door, or in a host's long-lived process at :route. The
wrapper is now called inside try/catch, the reports are drained on
every way out, and a failure is re-raised with its own kind, reason
and stacktrace. Reports stay messages rather than a counter in the
process dictionary, since a wrapper may run the work elsewhere.

Refs: sr-t36w
@johnnyt
johnnyt force-pushed the sr-t36w-whole-delivery-wrapper branch from 2ba1e68 to cb8ff2b Compare October 2, 2026 10:21
@johnnyt
johnnyt merged commit be77380 into main Oct 2, 2026
1 check passed
@johnnyt
johnnyt deleted the sr-t36w-whole-delivery-wrapper branch October 2, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant