FSL.Runner (fsl v0.2.0)

Copy Markdown View Source

Execution engine for FSL.Machine finite state machines.

A scenario module (one that does use FSL.Machine) compiles each state block into a function __state_<name>/1 that takes the context (FSL.Context, or whatever struct the application extended it into) and returns a transition descriptor:

  • {:goto, target, desc, ctx} — move to another state
  • {:terminal, :success, r, ctx} — scenario completed successfully
  • {:terminal, :failure, r, ctx} — scenario failed

target is either an explicit state name, the atom :next (the next state declared in the module), :loop (re-enter the current state) or :__back__ (the state entered before this one, read from ctx.laststate). The runner resolves those, logs the transition and calls the next state function — this is the "handled by the runner, not a direct recursive call" contract from the README, which keeps the call stack flat across an arbitrary number of transitions.

Entry points

  • FSL.Host.bootstrap/0 — start whatever the binding needs (idempotent). Called once, through run/2 with start_stack = true.
  • run_instance/1 — run a single scenario instance in the calling process (an application typically binds its own events to self(), so the whole FSM must run where run_instance/1 is called).
  • run/2 — convenience used by the generated run/1: optionally bootstrap the stack, then run one instance.

Summary

Functions

Run one instance of module, optionally bootstrapping its host first.

Build the initial context from the scenario config block and run the FSM from initial_state until a terminal state is reached. Returns :ok on success or {:error, reason} on failure.

Start one machine to serve one inbound session, and monitor it.

Functions

run(module, bool)

@spec run(module(), boolean()) :: :ok | {:error, term()}

Run one instance of module, optionally bootstrapping its host first.

true is the one-shot mode a CLI or a mix task uses: bootstrap whatever the application needs (FSL.Host.bootstrap/0), then run. false assumes that has already happened, which is what lets many instances run in parallel over one set of resources.

Returns :ok, {:error, reason} or {:aborted, reason}.

run_instance(module, opts \\ [])

@spec run_instance(module(), keyword()) :: :ok | {:aborted, term()} | {:error, term()}

Build the initial context from the scenario config block and run the FSM from initial_state until a terminal state is reached. Returns :ok on success or {:error, reason} on failure.

spawn_uas_instance(target, opts \\ [])

@spec spawn_uas_instance(module() | Path.t(), keyword()) :: {pid(), reference()}

Start one machine to serve one inbound session, and monitor it.

This is the server-side entry point: something outside FSL accepts a new session — a connection, a call, a request — and needs a machine bound to it, plus a way to know when that machine is done.

{pid, ref} = FSL.Runner.spawn_uas_instance(MyApp.Greeter, parent_pid: self())

It returns {pid, ref}, where pid is the process running the machine and ref is a monitor reference: the caller receives {:DOWN, ref, :process, pid, reason} when the run ends, which is where a pool frees the slot it charged for this session.

opts are forwarded to run_instance/2. :parent_pid names who is told about the outcome; :dialog_pid and :inbound_request are the two slots a binding uses to hand the machine the session it is being started for, and what goes in them is the binding's business.

target is a module or a path, resolved relative to the current directory — unlike spawn_fsm, which resolves relative to the file that declares it. A server target comes from an operator (a command line, a configuration file), so it is read the way any path a person types is read.

Example: a SIP registrar

In Elixip, a registration module calls this from on_new_registration/3 with the dialog and the REGISTER it just received, and returns the pid as {:accept, pid}; the ref frees one slot of the concurrency quota when the scenario ends.