Behaviour for a durable FSM — or, in its degenerate one-step form, a durable job.
A module defines either step/2 (a state machine) or perform/1/perform/2
(a one-shot job), never both.
Job form (one step)
Define perform/1 or perform/2 and you get a durable job — no step names, no
outcome tuples:
defmodule Cleanup do
use GenDurable.FSM
@impl true
def perform(args, _ctx) do
File.rm_rf!(args["path"])
:ok
end
end
GenDurable.insert(Cleanup, args: %{"path" => "/tmp/x"})perform receives the instance args (the plain map passed as :args/:state,
or the typed struct if a State schema is declared) and, in the /2 form, the
GenDurable.Context.t/0. It returns:
:ok/{:ok, result_map}— the job isdone.{:error, reason}— retried withbackoff/1until:max_attempts, thenfailed.{:cancel, reason}—failedimmediately, no retry.- a raised exception — treated as
{:error, exception}(routed throughhandle/2).
:max_attempts (default 20) and backoff/1 (default a capped exponential)
tune retries. Override backoff/1 to change the schedule.
FSM form (many steps)
defmodule Checkout do
use GenDurable.FSM, version: 1, queue: "checkout"
defmodule State do
use GenDurable.State
embedded_schema do
field :n, :integer, default: 0
end
end
@impl true
def step("start", %{state: s}), do: {:next, "await_pay", %{s | n: s.n + 1}}
def step("await_pay", ctx) do
case Enum.find(ctx.signals, & &1.name == "payment_confirmed") do
nil -> {:await, "payment_confirmed", ctx.state}
sig -> {:next, "ship", apply_payment(ctx.state, sig)}
end
end
def step("ship", _ctx), do: {:done, %{"shipped" => true}}
@impl true
def handle(reason, ctx) do
if ctx.attempt < 5, do: {:replay, ctx.state, backoff(ctx.attempt)}, else: {:stop, reason}
end
enduse options
:name— FSM name stored in thefsmcolumn (default:inspect(module)).:version—fsm_version(default1). Old versions coexist as separate registered modules; seeGenDurable.Registry.:queue— default queue for instances (default"default").:state— theGenDurable.Stateembedded-schema module. Optional: if a nestedStateschema module is defined inside the FSM (as above) it is picked up by convention, so you rarely pass this. Omit both for plain-map state.:initial— initial step forGenDurable.insert/2(default"start").:max_attempts— job retry cap (default20). Job form only.
For the FSM form, handle/2 defaults to {:stop, reason} and is overridable.
For the job form, both handle/2 (retry-with-backoff) and backoff/1 are
generated and overridable; overriding handle/2 drops to the FSM outcome contract.
Summary
Types
Callbacks
@callback backoff(attempt :: non_neg_integer()) :: non_neg_integer()
@callback handle(reason :: term(), ctx :: GenDurable.Context.t()) :: GenDurable.Outcome.t() | term()
@callback perform(args :: term(), ctx :: GenDurable.Context.t()) :: result()
@callback step(step :: String.t(), ctx :: GenDurable.Context.t()) :: GenDurable.Outcome.t() | term()