Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ execution its key names and handing the finished execution's answer to a sink.
[`docs/guides/migrating-waiting-executions.md`](docs/guides/migrating-waiting-executions.md)
moves waiting library loans onto a new revision of their document and back,
and `mix statifier_examples.migrate_waiting` runs it.
[`docs/guides/basichttp-front.md`](docs/guides/basichttp-front.md) gives a
library hold its own HTTP location through `statifier_router`'s BasicHTTP
front, and answers the branch desk's POST at it.

## Opening a document in the editor

Expand Down
4 changes: 4 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ config :logger, :default_formatter,
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# A BasicHTTP location's token is a bearer capability, so a params log
# names it filtered, beside Phoenix's own default.
config :phoenix, :filter_parameters, ["password", "token"]

# OpenTelemetry. `opentelemetry_statifier` brings only the API, so the SDK's
# exporter is this app's choice - and the default choice is none. An example
# app that shipped an OTLP exporter on by default would spend every boot
Expand Down
5 changes: 5 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ config :phoenix_live_view,
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
sort_verified_routes_query_params: true

# The hold desk's outbound BasicHTTP POSTs go to a transport that hands
# each one to the process that made it, so a test reads what the desk was
# sent instead of reaching a branch desk over the network.
config :statifier_examples, StatifierExamples.HoldDesk, transport: StatifierExamples.DeskTransport
138 changes: 138 additions & 0 deletions docs/guides/basichttp-front.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# A durable execution with an HTTP location: the BasicHTTP front

A patron places a hold on a copy at the Riverside branch. The hold is a
durable execution, and it needs to hear back from the branch desk when the
copy is on the holds shelf. With `statifier_router`'s BasicHTTP front, the
execution gets an HTTP location of its own - a URL the desk POSTs an event
to - and the router delivers that POST into the execution through the same
path every routed event takes.

This guide walks the pieces as this app wires them:
`StatifierExamples.HoldDesk` (the router configuration, the chart's
resolver and the executor), `priv/library/hold_desk.scxml` (the chart),
`StatifierExamplesWeb.BasicHTTPController` (the front's action) and
`test/statifier_examples_web/controllers/basic_http_controller_test.exs`,
which drives the whole of it through the controller.

## A location is a bearer capability

Anyone who holds a location can post events to that execution, and the
router authenticates nothing beyond possession of it (ruled by the
operator, 2026-09-30). The hold hands its location to the one desk its
request names and to nobody else. No request line or dispatch log carries
it: the endpoint's `Plug.Telemetry` logs no request line under
`/basichttp` (`StatifierExamplesWeb.Endpoint.log_level/1`), the route is
`log: false`, and `:filter_parameters` names `token`. Ecto's query log at
`:debug` prints bound parameters, and the router binds the token to look
a location up and to store it, so a host keeps `:debug` out of
production; at `:info`, the production level here, no query is logged.
A host serves
the base URL over TLS and rotates a location that may have leaked with
`StatifierRouter.BasicHTTP.rotate_location/2`, after which the old one
answers 404.

## The pins

| Package | Version | What this guide uses it for |
|---|---|---|
| `statifier_router` | 0.9.2 | the `:basichttp` key, the location, `StatifierRouter.BasicHTTP.Front`, the location table |
| `statifier` | 2.10.0 | the Basic HTTP Event I/O Processor and its decoder |
| `statifier_persistence` | 0.24.0 | the chart registry, the execution, the input log |

0.9.2 is the floor because it is the first `statifier_router` release
whose migrations and address reaper both run on this app's SQLite
database.

## The chart

`priv/library/hold_desk.scxml` waits in `requested` for the hold request.
On `hold.requested` it sends `hold.placed` to the desk the request names,
with a `<send type="basichttp">` whose `targetexpr` is the desk's URL, and
passes three parameters: the hold, the copy, and `reply_to`, its own
location, read from `_ioprocessors['basichttp']['location']`. It then waits
in `waiting` for `copy.shelved` and finishes in `shelved`.

## The configuration

`StatifierExamples.HoldDesk.config/0` is a router configuration of its own:
one binding, `hold_requests`, that routes each hold request to the
execution keyed by its hold id, and the `:basichttp` key:

```elixir
basichttp: [base_url: StatifierExamplesWeb.Endpoint.url() <> "/basichttp"]
```

Setting the key does two things. It registers the router's processor under
the processor's URI and its short form `basichttp`, which is what lets the
chart's `<send type="basichttp">` pass the engine's type check. And it
gives every execution created under a new address row a location: the base
URL, `/`, and a 43-character token the router mints, never the execution
id. The parcel recipe's configuration,
`StatifierExamples.RoutedWorkflow.config/0`, does not set the key, so its
executions get no location.

The location token lives in the router's location table, which only a
configuration with `:basichttp` needs. This app creates it in
`priv/repo/migrations/20260930120001_add_statifier_router_locations.exs`:

```elixir
def up, do: StatifierRouter.Migrations.up_locations(@opts)
def down, do: StatifierRouter.Migrations.down_locations(@opts)
```

with the same `depot_id` leading column the app's first router migration
gives every router table.

## The outbound send runs in the executor

A durable execution has no session to perform its sends: the step hands
each effect to the configuration's executor. `StatifierExamples.HoldDesk.execute/2`
plans a BasicHTTP send with `StatifierRouter.BasicHTTP.deliver/3` and
performs what it planned with the processor's `perform/2`, which POSTs a
form body to the desk with the send's `scxml-send-key` header. The POST
goes through the configuration's `:transport`: statifier's default,
on OTP's `:httpc`, in the dev app, and a transport under test that hands
the POST back to the test instead of sending it.

The POST is made inside the delivery's transaction, before the step
commits. A desk that does not answer 2xx, or does not answer at all, is a
failed send: `statifier_persistence` enters `error.communication`,
carrying the send id, into the execution in the same step, and the chart
takes it from `waiting` to its other final state, `desk_unreached`. A
delayed BasicHTTP send is refused the same way, because its timer would
live in the delivering process rather than in the database.

## The front

The router answers `/basichttp/:token` for every method with
`StatifierExamplesWeb.BasicHTTPController.event/2`, outside the browser
pipeline. The action builds the request map the front reads - the token,
the method, the content type, the body as it arrived, the query string and
the `scxml-send-key` header - hands it to
`StatifierRouter.BasicHTTP.Front.handle/3` and answers the status
`StatifierRouter.BasicHTTP.Front.response/1` maps the answer to:

| The request | The answer |
|---|---|
| a POST the execution takes, or a repeat of a send key it already took | 204 |
| a POST at a location that reaches no execution: unknown, rotated away, or finished | 404 |
| any other method | 405, with `allow: POST` |
| a body or a send key the decoder refuses | 400 |

A form body is read by `Plug.Parsers` before any action runs, so the
endpoint's parsers use `StatifierExamplesWeb.RawBody`, which keeps the raw
body of a request under `/basichttp` for the action to hand on.

## Driving it

The controller test routes a hold request, reads the `hold.placed` POST the
desk was sent, takes `reply_to` from it and POSTs
`_scxmleventname=copy.shelved` at that location through the endpoint:

```elixir
post(conn, "/basichttp/" <> token, "_scxmleventname=copy.shelved")
```

The answer is 204 and the execution is completed; the same POST again is
404, because the hold has finished. In the dev app, a desk that holds the
location does the same with any HTTP client.
30 changes: 24 additions & 6 deletions docs/guides/first-workflow-routed.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ The recipe is written and checked against these releases, which are what

| Package | Version | What the recipe uses it for |
|---|---|---|
| `statifier_router` | 0.6.0 | the binding, the delivery, the address and dedupe tables, the route, the reapers, the migration's `:leading_columns`, `:on_create` and `:on_step` |
| `statifier_persistence` | 0.21.0 | the chart registry, the execution, the input log, `ended_at` |
| `statifier_router` | 0.9.2 | the binding, the delivery, the address and dedupe tables, the route, the reapers, the migration's `:leading_columns`, `:on_create` and `:on_step` |
| `statifier_persistence` | 0.24.0 | the chart registry, the execution, the input log, `ended_at` |
| `statifier_blocks` | 0.41.0 | the document and the compile |
| `statifier` | 2.9.0 | compiling the chart and running it |
| `statifier` | 2.10.0 | compiling the chart and running it |

`statifier_router` 0.6.0 requires `statifier ~> 2.9` and
`statifier_router` 0.9.2 requires `statifier ~> 2.10` and
`statifier_persistence ~> 0.18`, which is what moved those two with it.
`mix.exs` asks for `statifier_persistence ~> 0.20` all the same: 0.20.0
adds `Executions.migrate_batch/3`, which
`docs/guides/migrating-waiting-executions.md` walks, and `mix.lock`
resolves 0.21.0, the release this table names. The jobs run on this
resolves 0.24.0, the release this table names. The jobs run on this
app's own Oban. The first line the command prints names the versions it
actually loaded.

Expand Down Expand Up @@ -341,8 +341,26 @@ Each of these is in `statifier_router`'s README and not needed here:

## Moving the first-workflow host to these pins

The latest move, from statifier_router 0.6.0 to 0.9.2, is the one that
touched a migration and the reapers. 0.8.0 added the router's third migration version,
V03, which renames the subscription table's unique index on Postgres. The
migration this recipe runs calls `StatifierRouter.Migrations.up/1` with no
version, so on a fresh database it runs V03 too, and V03's `ALTER INDEX`
failed on SQLite until 0.9.1 made it do nothing there; a database that
already ran V02 needs no new migration of its own, since V03 has nothing
to rename on SQLite. 0.8.0 also wrote the address reaper's stamp and
delete with Postgres's `= ANY(...)`, so the `reaped` step failed on SQLite
until 0.9.2 wrote them as an IN list. 0.9.2 is therefore the floor. The same move took statifier
to 2.10.0 and statifier_persistence to 0.24.0, which changed nothing here.
0.9.0's BasicHTTP front is not part of this recipe; its configuration sets
no `:basichttp`, so its executions get no location.
`docs/guides/basichttp-front.md` walks the front.

The move before it, to the first pins this recipe was written against:

This app moved from statifier 2.8.1, statifier_persistence 0.17.0 and
statifier_router 0.4.1 to the pins above, and nothing in it changed but the
statifier_router 0.4.1 to statifier 2.9.0, statifier_persistence 0.19.0 and
statifier_router 0.6.0, and nothing in it changed but the
requirements:

- **statifier 2.9.0** adds `Statifier.Publish.findings/2` and
Expand Down
13 changes: 10 additions & 3 deletions docs/guides/first-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ The recipe is written and checked against these releases, which are what

| Package | Version | What the recipe uses it for |
|---|---|---|
| `statifier` | 2.9.0 | compiling the chart and running it |
| `statifier` | 2.10.0 | compiling the chart and running it |
| `statifier_blocks` | 0.41.0 | the document, `Plan.expressible/3`, the compile |
| `statifier_persistence` | 0.21.0 | the chart registry, the execution, the input log, `ended_at` |
| `statifier_persistence` | 0.24.0 | the chart registry, the execution, the input log, `ended_at` |
| `statifier_oban` | 0.13.0 | the invoke job and its `:invoke_timeout`, the timer job |
| `statifier_router` | 0.6.0 | two of the publish-time checks |
| `statifier_router` | 0.9.2 | two of the publish-time checks |

`statifier_datamodel` 0.5.0 arrives through `statifier_blocks`. The first
line the command prints names the versions it actually loaded.
Expand Down Expand Up @@ -237,6 +237,13 @@ The move after it, to `statifier_persistence ~> 0.20` with `mix.lock` at
nothing in this recipe calls back into the execution it is stepping, which
is the one thing 0.21.0 refuses that it used to take.

The move to statifier 2.10.0, statifier_router 0.9.2 and, with them,
statifier_persistence 0.24.0 needed nothing in this recipe either. The
router releases add a migration version and the BasicHTTP front, which
`docs/guides/basichttp-front.md` walks; none of the persistence releases
adds a migration, and none of what they refuse is something this recipe
does.

The statifier_blocks moves after 0.35.0, one minor at a time to 0.41.0,
needed nothing in this recipe either. statifier_blocks' `docs/upgrading.md`
says what each asks of a host; this recipe calls `Decode.decode/1`,
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/migrating-waiting-executions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ opened. The next run starts from an empty chart.

The migration surface is `statifier_persistence`'s. `mix.exs` asks for
`~> 0.20`, the release that adds `Executions.migrate_batch/3`, and
`mix.lock` resolves 0.21.0. The first line the command prints names the
`mix.lock` resolves 0.24.0. The first line the command prints names the
version it loaded.

The migration surface is the verb and its report. A dry run is the
Expand Down
Loading
Loading