Skip to content

Posts to the hold desk after the commit - #178

Merged
johnnyt merged 2 commits into
mainfrom
se-g9t4-desk-post-after-commit
Oct 2, 2026
Merged

johnnyt merged 2 commits into
mainfrom
se-g9t4-desk-post-after-commit

Conversation

@johnnyt

@johnnyt johnnyt commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

What changes

The library hold desk's executor (StatifierExamples.HoldDesk.execute/2) no longer makes its BasicHTTP POST inside the delivery's transaction. It plans the send with StatifierRouter.BasicHTTP.deliver/3 and inserts a StatifierExamples.HoldDesk.DeskPost job on this app's own Oban, in a new desk_posts queue. This is the shape statifier_router recommends in docs/adr/0002-addressing.md, the Amendment of 2026-10-02 on a durable execution's outbound BasicHTTP send (the shape ruled by the operator, 2026-10-01).

  • Inserted inside the delivery. The executor runs in the delivery's transaction and the job writes through the same repo, so the job commits with the step that sent and a rolled-back delivery takes it with it.
  • Keyed on the send's dedup key. The job is unique on the send's dedup key written out (the execution id, send_id, macrostep, microstep, round, c_index, owner, ordinal, each nil as -), so a redriven step inserts one job.
  • Performed after the commit. DeskPost.perform/1 calls StatifierRouter.BasicHTTP.perform/2 outside every delivery, so a slow desk holds no transaction and no SQLite write lock.
  • A failed POST comes back through the sanctioned path. After the third refused POST, the job delivers an external error.communication, carrying the send's id, through StatifierRouter.Delivery.deliver_event/4 with create: :never over the row from StatifierRouter.Addresses.by_execution/2, under the plan id desk_post_failure. The chart still ends in desk_unreached. A failure that reaches no execution (finished hold, missing address row) is cancelled and kept in the jobs table as the dead letter.
  • No-target and delayed sends are unchanged. A send with no target plans no job and still fails at once as {:basichttp_send_without_target, send_id}, which statifier_persistence re-enters in the same step. A delayed send is still refused.

Every router function the job calls exists in statifier_router 0.9.2 as mix.lock resolves it: BasicHTTP.deliver/3, BasicHTTP.perform/2, Addresses.by_execution/2, Delivery.deliver_event/4. No dependency moves.

Tests

In test/statifier_examples_web/controllers/basic_http_controller_test.exs, every hold test now drains desk_posts with Oban.drain_queue/2 (the suite runs Oban with testing: :manual), and the test transport (test/support/desk_transport.ex) can call a hook while the desk answers. New or changed tests:

  • A slow desk holds no delivery open. The delivery returns with the execution :active, one job queued and the desk not yet called. The desk is then called with Repo.in_transaction?() false.
  • A refused desk ends the hold unreached. It now proves the re-entry by reading error.communication, with its send id, from the execution's input log, because the event now arrives as a delivered external event in a step of its own, not as a same-step re-entry. It still asserts :completed and the 404.
  • A redriven send inserts one job, keyed as above.
  • A delivery that rolls back takes its desk post with it.
  • A failed POST that reaches no hold is kept as a dead letter, for both the finished-hold case and the no-address case.
  • The no-target test also asserts that no job was planned.
  • A job queued before a restart posts, whatever atoms this node has. It writes an instruction that names an atom no code creates and shows the job still posts.

Sabotage checks

Each check was run against the controller test file and restored byte-equal:

Mutation Result
executor performs the POST itself instead of inserting the job slow-desk test red (desk called before the delivery returned)
the job's POST wrapped in a Repo transaction slow-desk test red ({:desk_answering, false} not received)
the last failed POST cancels instead of delivering error.communication refused-desk test red
the job's unique option removed redriven-send test red
a missing address row answers :ok dead-letter test red
a dropped delivery answers :ok dead-letter test red
the no-target raise clause continues no-target test red
executor answers :ok without inserting the job every hold test red
the job's decode given [:safe] again restart test red (no POST made)
the insert moved to a Task outside the transaction error, not a kill: the sandbox refused the Task a connection

The rollback test is therefore not proven to discriminate: a sandboxed suite has one connection. Its test comment says so.

Provenance

  • Job arguments. The job carries the planned instruction and plan context as :erlang.term_to_binary/1 in base64, as the router README's recipe writes them, and reads them back with plain :erlang.binary_to_term/1, as that recipe does. The first commit decoded with :safe, which refuses an atom the node has not created yet: a job queued before a restart can name one that no module has loaded since (a send's owner kind, a struct field), so its POSTs failed and the hold ended unreached with no POST made. The cure commit drops :safe; the jobs table is written only through this app's repo, by DeskPost.new/3. It does not rebuild the POST from url, headers and body: perform/2 takes the payload as an opaque term, and the map inside it is a private type in statifier. This is an engineering choice made in this PR.
  • Retry policy. Three POST attempts, then up to two more attempts that only redeliver the failure without posting again (max_attempts: 5).
  • The location in the job row. The job row holds the POST body, which includes the hold's reply_to location, until the host prunes it. The guide says so.
  • Unchanged. The "never logs it" sentence and the token filter are untouched.

Gate

mix quality was green on each commit's tree: 660 of 660 tests on the first commit, and 661 of 661 on the cure commit, both at 84.7% coverage. The repo gate lock and a machine slot were held from each run through its commit.

Review

This PR touches the hold desk's send path and a behaviour a router record cites, so a cold review pass is requested. Its gate tier is the contract tier.

The hold desk's executor no longer makes its BasicHTTP POST inside the
delivery's transaction. It plans the send with the router's processor
and inserts a HoldDesk.DeskPost job on this app's Oban, in the new
desk_posts queue: the job commits with the step that sent, a delivery
that rolls back takes it with it, and it is unique on the send's dedup
key, so a redriven step inserts one job.

The job makes the POST after the commit, so a slow desk holds no
transaction and no SQLite write lock. A POST the desk refuses is
retried; once the third has failed, the job delivers
error.communication back into the hold through
StatifierRouter.Delivery.deliver_event/4 with create: :never over the
row from Addresses.by_execution/2, as statifier_router's ADR-0002
Amendment on the outbound BasicHTTP send recommends. A failure that
reaches no execution is cancelled and kept as the dead letter.

The controller tests drain the queue, a new test shows the desk is
called after the delivery returned and outside any transaction, and
the refused-desk test reads error.communication from the execution's
input log. The guide says why the example performs after the commit.

Refs: se-g9t4
DeskPost decoded its instruction with :erlang.binary_to_term/2 and
:safe, which refuses an atom the node has not created yet. A job
queued before a restart can name one that no module has loaded since
(a send's owner kind, a struct field), so its POST attempts raised and
the hold ended unreached with no POST made. The job now reads its
instruction with :erlang.binary_to_term/1, as the router README's
recipe does; the jobs table is written only through this app's repo.

A new test writes an instruction naming an atom no code creates and
shows the job still posts.

Refs: se-g9t4
@johnnyt
johnnyt merged commit 3acd4d2 into main Oct 2, 2026
3 checks passed
@johnnyt
johnnyt deleted the se-g9t4-desk-post-after-commit branch October 2, 2026 10:09
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