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
5 changes: 4 additions & 1 deletion docs/guides/basichttp-front.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
26 changes: 19 additions & 7 deletions lib/statifier_examples/hold_desk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -163,8 +165,10 @@ defmodule StatifierExamples.HoldDesk do
The executor every create and step hands its effects to. A BasicHTTP
`<send>` 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()}
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading