Issues HTTP requests from a machine, as events.
A state must never block: an await or a blocking call would stop the machine
from handling anything else, including its own deadline. use FSL.HTTP brings
in http_GET/3, which sends the request and returns immediately, so a goto
placed right after it works. The result arrives later, as a single tagged
message the machine matches in on_events:
{ tag, {:ok, %Req.Response{}} }
{ tag, {:error, reason} }where reason is one of:
:timeout— the totaltimeoutelapsed; the request was cancelled;- a
Reqexception — a network / transport error (%Req.TransportError{}, …); {:crash, reason}— the worker process died before producing a result.
Example
state query_backend do
http_GET("https://backend/api/x", 10_000, :provisioning)
on_events do
{ :provisioning, {:ok, %Req.Response{status: 200, body: b}}} ->
appdata_set(:data, b); goto next, "backend OK"
{ :provisioning, {:ok, %Req.Response{status: c}}} ->
scenario_failure("backend HTTP #{c}")
{ :provisioning, {:error, :timeout}} ->
scenario_failure("backend timeout")
{ :provisioning, {:error, r}} ->
scenario_failure("backend error: #{inspect(r)}")
end
endBecause exactly one message always arrives — including on timeout — the
machine needs no after clause for the timeout case: it comes back as
{:error, :timeout} like any other outcome. A wider after remains possible
as a safety net, but is not required.
Requirements
Req is an optional dependency of this package. Add it to your own
dependencies to use this module:
{:req, "~> 0.5"}The rest of FSL needs nothing beyond Logger and OTP.
Timeout & cancellation — the coordinator pattern
http_GET never calls receive in the machine's process. It spawns a
disposable coordinator, which in turn spawn_monitors a worker that
runs Req.get/2. The coordinator arbitrates time with a single
receive/after, so exactly one of three things happens:
- the worker returns in time → the coordinator forwards the result;
- the worker crashes → the coordinator reports
{:error, {:crash, reason}}; - the
timeoutfires → the coordinator kills the worker withProcess.exit(worker, :kill), so the request is genuinely cancelled and no late reply can ever be produced, then reports{:error, :timeout}.
The receive/after serialises the timer against the worker's result, so
there is no race between them, and the coordinator sends exactly one
{tag, …} message before terminating. No late message can reach a subsequent
on_events and be mistaken for something else.
Killing the worker tears down the HTTP request in flight: the socket it had checked out of the Finch/NimblePool pool is reclaimed when the process dies. That is the intended behaviour here — cancelling a timed-out request must not leave a connection lingering to deliver a response nobody is listening for.
Summary
Functions
@spec get_async(binary(), pos_integer(), term(), keyword()) :: pid()
Launch an asynchronous HTTP GET. Spawns the disposable coordinator process
(which owns the worker and the timeout) and returns its pid immediately; the
calling process is never blocked. The result is delivered to the caller as
a single {tag, result} message (FSL.Valet captures self()).
req_opts is forwarded to Req.get/2 — normally empty from the FSL macro,
but used by the tests to inject a Req.Test stub / a fake :adapter.