StatifierRouter.SourceInvoke (StatifierRouter v0.9.2)

Copy Markdown View Source

The source invoke's two calls, as a host's invoke handler makes them: start/3 turns a %Statifier.Effect.Invoke{} into StatifierRouter.subscribe/3, and cancel/3 turns the %Statifier.Effect.CancelInvoke{} the engine emits on state exit into StatifierRouter.cancel/2 (ADR-0007, section 2). An invocation's lifetime is a subscription's, and the chart writes no cleanup for it.

The chart's half is one element, whose params name the binding and nothing else (ADR-0007, section 1):

<invoke type="myapp:source">
  <param name="binding" expr="'clicks_to_join'"/>
</invoke>

The type is the host's own string, registered in the Statifier.Invoke.Types.t/0 snapshot it stamps; this module never reads it, so a host may serve as many source invoke types as it likes with one delegate.

Why this is a delegate and not a @behaviour

It does not implement Statifier.Invoke.Handler (statifier 2.6.0, the version mix.lock resolves), and it cannot. That behaviour's Statifier.Invoke.Handler.start/2 and Statifier.Invoke.Handler.cancel/2 are pure planning callbacks, called from Statifier.Session.Effects.plan/2's own fold with "no process, no clock, and no I/O"; they return instructions for an executor to perform. Subscribing writes a row, so it belongs in the impure half. The callbacks also carry no slot for it: the plan context is %{session_id: _, invoke_types: _, invoke_handlers: _} and carries "no pid, no %MachineState{}, and no session struct", so neither a StatifierRouter.Config nor the execution id can reach a planning callback at all.

So a host that runs a live Statifier.Session writes a handler whose Statifier.Invoke.Handler.start/2 returns {:ok, [{:handler, __MODULE__, payload}]} and whose Statifier.Invoke.Handler.perform/2 calls start/3 here. A durable host - the mode ADR-0007, section 5 specifies, and the only one - has an executor rather than a session: both effects arrive at the executor seam this package already hands StatifierPersistence.Executions.create/4 and step/5, where the execution id is in the context and the configuration is in hand, and that handler calls straight into these two functions.

Idempotency

Both calls are idempotent, which is the contract either door needs. Statifier.Invoke.Handler says a Statifier.Invoke.Handler.perform/2 "MUST be idempotent on invoke_id", because a host that crashes between performing an instruction and recording that it ran replays the same drive; and it says a cancel "MAY be planned for an invocation a host has already reported complete", so a handler "MUST tolerate cancelling an invoke_id it no longer knows". start/3 answers {:ok, :already_subscribed} for the second call and cancel/3 {:ok, :not_subscribed}; neither is an error.

What it refuses

An execution created under :always_new has no address row, so it has no key to subscribe under and its source invoke is refused rather than subscribed under an invented key (ADR-0007, section 6). That reaches a caller as {:error, {:unaddressed_execution, execution_id}} from StatifierRouter.subscribe/3. An invoke whose params do not carry a binding is refused here, before any read.

Summary

Types

Why a source invoke was refused rather than subscribed.

Functions

Cancels the subscription the matching start/3 created, naming it by execution_id and the cancellation's invoke_id.

Subscribes execution_id to the binding named by invoke's binding param, under invoke's own invoke_id.

Types

refusal()

@type refusal() ::
  {:missing_binding_param, term()}
  | {:unknown_binding, String.t()}
  | {:unaddressed_execution, String.t()}

Why a source invoke was refused rather than subscribed.

Functions

cancel(config, execution_id, cancel_invoke)

@spec cancel(
  StatifierRouter.Config.t(),
  String.t(),
  Statifier.Effect.CancelInvoke.t()
) ::
  {:ok, :cancelled | :not_subscribed}

Cancels the subscription the matching start/3 created, naming it by execution_id and the cancellation's invoke_id.

The engine's %Statifier.Effect.CancelInvoke{} carries an invoke_id and the exiting state's index and no binding - deliberately, since widening that callback was refused upstream - so this reads the binding back off the subscription row before calling StatifierRouter.cancel/2. A cancellation for an invocation with no row is {:ok, :not_subscribed}.

start(config, execution_id, invoke)

@spec start(StatifierRouter.Config.t(), String.t(), Statifier.Effect.Invoke.t()) ::
  {:ok, :subscribed | :already_subscribed} | {:error, refusal()}

Subscribes execution_id to the binding named by invoke's binding param, under invoke's own invoke_id.

Returns what StatifierRouter.subscribe/3 returns, or {:error, {:missing_binding_param, params}} when the invoke carries no binding param of its own. Statifier.Effect.Invoke's params is the resolved <param>/namelist payload as a string-keyed map, or :undefined when the element has none (statifier 2.6.0).