From fe71e1de33451cea1f11e383d9158b7ae34e2f58 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Fri, 2 Oct 2026 03:31:42 -0600 Subject: [PATCH] Closes the BasicHTTP front example's gaps The refused-desk test now asserts that error.communication, with its send id, was re-entered into the execution, the only event that leads to desk_unreached. The executor names the send with no target for what statifier plans it as, an error.communication raise and no request, and fails it as basichttp_send_without_target, which the durable execution takes as the same error.communication. The guide and the controller moduledoc list the front's 500 answer. Refs: se-0tuc --- docs/guides/basichttp-front.md | 5 +- lib/statifier_examples/hold_desk.ex | 26 ++++++++--- .../controllers/basic_http_controller.ex | 3 +- .../basic_http_controller_test.exs | 46 +++++++++++++++++++ 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/docs/guides/basichttp-front.md b/docs/guides/basichttp-front.md index 5d7a417..5667e9f 100644 --- a/docs/guides/basichttp-front.md +++ b/docs/guides/basichttp-front.md @@ -98,7 +98,9 @@ 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 +takes it from `waiting` to its other final state, `desk_unreached`. A send +with no target, which statifier plans as an `error.communication` raise and +no request, fails without a POST and enters the execution the same way. A delayed BasicHTTP send is refused the same way, because its timer would live in the delivering process rather than in the database. @@ -118,6 +120,7 @@ the `scxml-send-key` header - hands it to | 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 | +| any other answer: a delivery that did not settle, which the desk may retry | 500 | A form body is read by `Plug.Parsers` before any action runs, so the endpoint's parsers use `StatifierExamplesWeb.RawBody`, which keeps the raw diff --git a/lib/statifier_examples/hold_desk.ex b/lib/statifier_examples/hold_desk.ex index d4bf267..3906b61 100644 --- a/lib/statifier_examples/hold_desk.ex +++ b/lib/statifier_examples/hold_desk.ex @@ -36,9 +36,11 @@ defmodule StatifierExamples.HoldDesk do before the step commits. A POST the desk does not answer with a 2xx is a failed send, which `statifier_persistence` enters into the execution as `error.communication`, and the chart ends the hold unreached. A - delayed BasicHTTP send, whose timer would live in this process rather - than in the database, is refused, and enters the execution the same - way. + send with no target, which statifier plans as an `error.communication` + raise and no request, fails without a POST and enters the execution the + same way. A delayed BasicHTTP send, whose timer would live in this + process rather than in the database, is refused, and enters the + execution the same way. """ @behaviour StatifierRouter.Resolver @@ -163,8 +165,10 @@ defmodule StatifierExamples.HoldDesk do The executor every create and step hands its effects to. A BasicHTTP `` is planned with `StatifierRouter.BasicHTTP.deliver/3` and each instruction it plans is performed with the processor's `perform/2`; a - delayed one is refused as `{:delayed_basichttp_send, send_id}`. Every - other effect is passed. + send with no target, which statifier plans as an `error.communication` + raise and no request, fails as `{:basichttp_send_without_target, + send_id}`, and a delayed one is refused as `{:delayed_basichttp_send, + send_id}`. Every other effect is passed. """ @spec execute(Statifier.Effect.t(), StatifierPersistence.Executor.context()) :: :ok | {:error, term()} @@ -191,8 +195,16 @@ defmodule StatifierExamples.HoldDesk do {:error, _reason} = error -> {:halt, error} end - _unperformed, :ok -> - {:halt, {:error, {:basichttp_send_not_planned, send.send_id}}} + # Statifier plans a send with no target as a raise of + # error.communication carrying the send id, and no request. There is + # no internal queue to raise on here, so the send fails instead: + # statifier_persistence enters a failed send as the same + # error.communication, from the same send, with the same send id. + {:raise, :platform, "error.communication", _origin, _opts}, :ok -> + {:halt, {:error, {:basichttp_send_without_target, send.send_id}}} + + _unknown, :ok -> + {:halt, {:error, {:basichttp_instruction_unknown, send.send_id}}} end) end diff --git a/lib/statifier_examples_web/controllers/basic_http_controller.ex b/lib/statifier_examples_web/controllers/basic_http_controller.ex index 5183333..a9067b3 100644 --- a/lib/statifier_examples_web/controllers/basic_http_controller.ex +++ b/lib/statifier_examples_web/controllers/basic_http_controller.ex @@ -6,7 +6,8 @@ defmodule StatifierExamplesWeb.BasicHTTPController do status and headers `StatifierRouter.BasicHTTP.Front.response/1` maps its answer to - 204 for a delivered event or a duplicate, 404 for a location that reaches no execution, 405 with `allow: POST` for another - method, 400 for a body the decoder refuses. + method, 400 for a body the decoder refuses, and 500 for any other + answer, a delivery that did not settle, which the desk may retry. The token is a bearer capability (ruled by the operator, 2026-09-30): holding it is the whole of the authorization, so this action checks diff --git a/test/statifier_examples_web/controllers/basic_http_controller_test.exs b/test/statifier_examples_web/controllers/basic_http_controller_test.exs index 455ef75..d1ad8c6 100644 --- a/test/statifier_examples_web/controllers/basic_http_controller_test.exs +++ b/test/statifier_examples_web/controllers/basic_http_controller_test.exs @@ -159,17 +159,63 @@ defmodule StatifierExamplesWeb.BasicHTTPControllerTest do Executions.inputs(FirstWorkflow.store(), execution_id) end + # The chart has two finals, and a finished execution keeps no + # configuration to read the one it took from; the error.communication + # the refused POST re-entered is what names desk_unreached, since only + # that event leads there. # sabotage: execute/2 made to answer :ok whatever perform/2 answered -> # the execution stayed active in waiting and the location still took a # POST, red; restored, green. + # sabotage: perform/3 made to continue past a failed POST -> no + # error.communication was re-entered, red; restored, green. test "a desk that refuses the POST ends the hold unreached", %{conn: conn} do + reentered = [:statifier_persistence, :execution, :step, :reentered] + handler = "hold-desk-reentered-#{System.unique_integer([:positive])}" + test_pid = self() + + forward = fn _event, _measurements, metadata, nil -> + send(test_pid, {:reentered, metadata}) + end + + :ok = :telemetry.attach(handler, reentered, forward, nil) + on_exit(fn -> :telemetry.detach(handler) end) + Process.put(:desk_status, 503) {execution_id, _headers, %{"reply_to" => location}} = placed_hold("hold-0418") + assert_received {:reentered, + %{execution_id: ^execution_id, name: "error.communication", opts: opts}} + + assert is_binary(opts[:sendid]) assert status!(execution_id) == :completed assert conn |> post_event(location, "_scxmleventname=copy.shelved") |> response(404) end + # Statifier plans a send with no target as an error.communication raise + # and no request; the executor fails the send instead of performing it. + # sabotage: perform/3's raise clause made to continue -> execute/2 + # answered :ok, red; restored, green. + test "a send with no target posts nothing and fails, naming the send" do + no_target = %Statifier.Effect.Send{ + type: "basichttp", + event: "hold.placed", + target: nil, + data: %{"hold_id" => "hold-0419"}, + send_id: "send_1", + c_index: 0, + owner: {:transition, 0}, + macrostep: 1, + microstep: 0, + round: 0, + ordinal: 0 + } + + assert HoldDesk.execute({:send, no_target}, %{execution_id: "ex_hold_0419"}) == + {:error, {:basichttp_send_without_target, "send_1"}} + + refute_received {:desk_post, _url, _headers, _body} + end + # sabotage: :basichttp added to RoutedWorkflow's configuration -> red; # restored, green. test "the parcel configuration carries no BasicHTTP location" do