Skip to content

Adds the BasicHTTP front for durable executions - #138

Merged
johnnyt merged 3 commits into
mainfrom
sr-xgi8-basichttp-front
Sep 30, 2026
Merged

johnnyt merged 3 commits into
mainfrom
sr-xgi8-basichttp-front

Conversation

@johnnyt

@johnnyt johnnyt commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Implements the two ADR-0002 Amendments of 2026-09-30 (docs/adr/0002-addressing.md). The first is "Amendment (2026-09-30, sr-xgi8): a durable execution's BasicHTTP location is a rotatable token the router mints, and the front that answers at it", merged in PR 137. The second is "Amendment (2026-09-30, sr-xgi8): the location table is opt-in, outside the version walk", merged in PR 139. This is the code half of sr-xgi8. The location's shape and the front's authentication were ruled by the operator, 2026-09-30; everything else follows the two Amendments.

What each decision of the first Amendment became

  • Decision 1, the token. The first Amendment's anchors are "What it is", "Where it is stored" and "When it is minted".
    • StatifierRouter.BasicHTTP.mint_token/0 mints 32 bytes from :crypto.strong_rand_bytes/1 as unpadded URL-safe base64, 43 characters.
    • StatifierRouter.Migrations.V04 creates the locations table: address_id references the address row's id with ON DELETE CASCADE, and there are unique indexes on address_id and token. The reference follows the :primary_key type.
    • V04 is opt-in and outside the version walk, as the second Amendment decides. A host runs it with StatifierRouter.Migrations.up_locations/1 and down_locations/1, which take the storage options, the layout options and :primary_key, and no :from or :version. up_locations/1 creates only what is missing; down_locations/1 drops the table only if it is there.
    • StatifierRouter.Schema.Location is the table's schema.
    • StatifierRouter.Delivery's private locate/3 inserts the location beside the address row that an :if_absent insert wrote. It runs only when :basichttp is set, in the same transaction and savepoint, before create/4.
    • An always_new execution gets no location, and its entry is %{}.
  • Decision 2, rotation. StatifierRouter.BasicHTTP.rotate_location/2 makes one insert-or-replace on address_id. It answers {:error, {:no_address, id}} when no address row names the execution. location/2 answers {:error, :no_location} when there is no location. A rotation does not reach the chart's _ioprocessors, as the record says.
  • Decision 3, resolution. The front's private resolve/2 reads the address row through its location. A token that is not token-shaped, an unknown token and a row stamped terminal_seen_at are each {:error, :unknown_location}. Delivery goes through StatifierRouter.Delivery.deliver_event/4 with the plan basichttp, create: :never and the 259_200_000 ms horizon. The binding id basichttp is reserved only on a configuration that sets the key; StatifierRouter.Config's private refuse_reserved_id/2 does it.
  • Decision 4, the location string.
    • StatifierRouter.BasicHTTP implements Statifier.Send.Processor. ioprocessors_entry/2 answers base_url <> "/" <> token. deliver/3, cancel/2 and perform/2 delegate to Statifier.Send.BasicHTTP.
    • StatifierRouter.Config gains the :basichttp key. It is refused with {:declared_send_types, type} when a registration clashes and with {:exclusive_keys, :basichttp, :send_types} beside a host's own :send_types.
    • Config.create_persistence_options/2 rebuilds the create's snapshot with location_token: added. Steps carry the configuration's own snapshot.
  • Decision 5, the front. StatifierRouter.BasicHTTP.Front has handle/3 and response/1. It is Plug-shaped with no process. The request map is :token, :method, :content_type, :body, :query and :send_key. A refused request is {:invalid_request, keys} and never carries the token. It refuses while a route runs in the calling process. The message id is execution_id/send_key with a key, and minted fresh without one. The status table is the record's.
  • Decision 6, the bearer-capability statement. It is in the moduledocs of StatifierRouter.BasicHTTP and StatifierRouter.BasicHTTP.Front and in the README's new "A BasicHTTP front" section.

What a host sees

  • A host that does not set :basichttp: nothing changes, the migrations included. No location row is written, the snapshot is built exactly as before, and no binding id is refused beyond execution. StatifierRouter.Migrations.up/1 and down/1, capped or uncapped, answer exactly as on main: they never create, drop or require the location table, and they refuse no leading-column name they accepted before. The existing migration tests (host_columns_test, index_names_test, migrations_test, primary_key_test) are unchanged from main and pass against this code. The route rules are unchanged, and route.ex, routes.ex, send_handler.ex and webhook.ex are untouched, so a URL in a route target stays refused. The test "without :basichttp mints nothing and builds the snapshot as before" pins the configuration half.
  • A host that sets the key: it adds a migration of its own after the ones it has, calling up_locations/1 and down_locations/1 with the same :table_prefix, :prefix, layout options and :primary_key its earlier migrations pass. The README's "Upgrading the tables" and the Migrations moduledoc, "The location table, V04, is opt-in", show it. Because the table references the address table, that migration rolls back before the one that created V01's tables, which is the order Ecto's rollback takes for a later migration.

Provenance

  • Cure 1 (commit "Makes the location table opt-in"). The first review found that V04 in the default walk changed what up/1 and down/1 answer for existing hosts: an uncapped down/1 on a pre-V04 database, uncapped from: 2 or from: 3 migrations, and the leading-column refusal. The cure takes V04 out of the walk and gives it the two opt-in calls. V04.down/1 drops the table only if it is there, as V03 renames only what it finds. V01.down/1 is unchanged: on a database without V04 it answers as before, and on one with V04 the opt-in migration rolls back first. This follows the first Amendment's "a host that never sets the key does not need V04".
  • Cure 2 (record PR 139, then commit "Cites the opt-in location table Amendment"). The second review found the two opt-in calls were surface the first Amendment did not decide. The second Amendment, "the location table is opt-in, outside the version walk", now decides them: V04 outside the walk, up_locations/1 and down_locations/1 with their options and refusals, tolerance in both directions, and the rollback order. It merged first. This branch was rebased onto it. The moduledocs of StatifierRouter.Migrations and StatifierRouter.Migrations.V04, and the README's "Upgrading the tables", cite it where they name the calls. No code changed in this cure.
  • The whole of mix.lock changed. mix deps.update statifier moved statifier 2.9.0 -> 2.10.0 and its dependency predicator 9.4.1 -> 9.4.2.
  • Postgres for the tests was a local server on 5432 (Homebrew PostgreSQL 17), reached through the PG* defaults in config/test.exs.

Gate

Full mix quality, quoted whole:

Running quality checks...
✓ Format: No changes needed (246ms)
✓ Compile: dev + test compiled (warnings as errors) (431ms)
Running analysis stages in parallel...
○ Doctor: skipped (:doctor not installed)
○ Gettext: skipped (:gettext not installed)
○ Sobelow: skipped (:sobelow not installed)
✓ Isolated tests: Passed (1.9s)
✓ Doc links: 1 link checked (13ms)
✓ Dependencies: No unused dependencies (438ms)
✓ Credo: No issues (1.5s)
✓ Docs: No warnings (1.5s)
✓ Tests: 420 of 420 passed, 97.7% coverage (3.2s)
✓ Dialyzer: No warnings (3.7s)
✓ All quality checks passed!

The committed tree is byte-identical to the tree this run was green on. The gate lock and a machine slot were held across both, so the commit was made without a second run.

Every new test in test/statifier_router/basic_http_test.exs and test/statifier_router/locations_migration_test.exs carries a sabotage note. The second file covers an uncapped first migration's rollback on a pre-V04 database, the documented opt-in migration's full rollback, a host that opts in from one migration (with the cascade and the unique token), a tolerant down_locations/1, and the refusals of up_locations/1. Each mutation was run against the lines this PR adds. Each one failed its test on an assertion, and the file was restored byte-equal before the next.

A changelog.d/sr-xgi8.md fragment is included under Added.

Implements the ADR-0002 Amendment of 2026-09-30. A configuration that
sets :basichttp registers StatifierRouter.BasicHTTP under the W3C
processor URI and basichttp, and each execution created under a new
address row gets a location: the base URL and a minted 43-character
token, stored in the new V04 locations table and handed to the create
so the execution's _ioprocessors carries it. location/2 reads it and
rotate_location/2 replaces it. StatifierRouter.BasicHTTP.Front decodes
a POST with statifier's decoder, delivers through deliver_event/4
under the name basichttp with create: :never, deduplicates per
execution on scxml-send-key, and maps the answer to 204, 404, 405, 400
or 500. Without the key nothing changes. statifier ~> 2.10.

Refs: sr-xgi8
Cures the review finding that V04 in the version walk changed what
up/1 and down/1 answer for every host. V04 leaves the walk: up/1 and
down/1, capped or not, answer exactly as before this branch, and a host
that sets :basichttp runs the location table with the new
Migrations.up_locations/1 and down_locations/1 in a later migration of
its own. V04's down drops the table only if it is there. The existing
migration tests are back to main's, unchanged; a new module covers the
pre-V04 rollback, the documented opt-in migration's full rollback and a
host that opts in from one migration.

Refs: sr-xgi8
The moduledocs of StatifierRouter.Migrations and
StatifierRouter.Migrations.V04, and the README's upgrade section, now
cite ADR-0002's Amendment of 2026-09-30 that decides the location table
is opt-in, outside the version walk, where they name up_locations/1 and
down_locations/1.

Refs: sr-xgi8
@johnnyt
johnnyt force-pushed the sr-xgi8-basichttp-front branch from ad08a23 to f84c1b1 Compare September 30, 2026 16:43
@johnnyt
johnnyt merged commit 888381d into main Sep 30, 2026
1 check passed
@johnnyt
johnnyt deleted the sr-xgi8-basichttp-front branch September 30, 2026 16:46
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