Documentation
¶
Overview ¶
Package state provides a minimal, concurrency-safe finite state machine.
Wiring and running are separate phases with separate types. Define builds a Spec — an immutable description of the states, handlers, activities and children of one kind of machine — and Run stamps a running Machine out of it. A Spec is wired once, is safe to share, and can be Run any number of times; a Machine is one execution: a current state and a context value.
The context is a value of any type C, created at Run and passed (by pointer) to every handler, so it can accumulate and share data across states.
Actions are plain values of any type; the action's type identifies which handler runs. Handlers are registered per (state, action type) with On and applied with Do. An action only advances the machine if a handler is registered for it in the current state and that handler returns no error.
A state may own background work: Enter registers an activity that runs in its own goroutine while the machine stays in that state, with a context that is canceled when the machine leaves it. Activities report outcomes back through Do; if the machine has moved on, the stale action is rejected like any other invalid action.
Completing ¶
Final declares the states that complete a machine. Entering one cancels the machine's activities, stops its children, closes Done and rejects further actions with ErrStopped; Result then reports where it came to rest. Stop does the same without a transition. The difference matters to a parent: completing is reported upward, being stopped is not.
Resuming ¶
Because the wiring lives in the Spec and the execution lives in the Machine, a machine is a resumable value: Result (or Read) snapshots the state and context, and Run reconstructs a machine at exactly that point. Persist the pair, restart the process, Run it again — the Spec is code, so only the (S, C) pair ever needs to be stored.
Children ¶
A child machine is just another Machine, held by pointer. Invoke binds one to a state: it is built from the parent's context on entry, stopped on exit, and its completion is a transition of the parent. Spawn binds one to the machine itself, for a dynamic number of children that outlive transitions. Stopping a machine stops its children, and theirs, all the way down.
Concurrency ¶
All methods are safe for concurrent use by multiple goroutines. Handlers (and the done callbacks of Invoke and Spawn) run with the machine locked, and locks are only ever taken from a parent toward its children. So from inside a handler:
- Send to any machine — up, down or sideways. It queues and returns.
- Do or Stop your own children, but never a parent or a sibling.
- Never Wait.
Activities hold no lock and may call anything.
Index ¶
- Variables
- type Builder
- func (b *Builder[S, C]) After[A any](s S, d time.Duration, a A)
- func (b *Builder[S, C]) Enter(s S, fn func(ctx context.Context, m *Machine[S, C]))
- func (b *Builder[S, C]) Final(states ...S)
- func (b *Builder[S, C]) Invoke[CS comparable, CC any](at S, start func(*C) *Machine[CS, CC], done func(*C, CS, CC) (S, error))
- func (b *Builder[S, C]) On[A any](from S, fn func(*C, A) (S, error))
- type ChildDone
- type Machine
- func (m *Machine[S, C]) Do[A any](a A) (S, error)
- func (m *Machine[S, C]) Done() <-chan struct{}
- func (m *Machine[S, C]) Read[R any](fn func(S, *C) R) R
- func (m *Machine[S, C]) Result() (S, C)
- func (m *Machine[S, C]) Send[A any](a A)
- func (m *Machine[S, C]) Spawn[CS comparable, CC any](child *Machine[CS, CC], done func(*C, CS, CC) (S, error))
- func (m *Machine[S, C]) Stop()
- func (m *Machine[S, C]) Trace(fn func(from S, action any, to S))
- func (m *Machine[S, C]) Wait(ctx context.Context, cond func(S, *C) bool) error
- type Spec
Constants ¶
This section is empty.
Variables ¶
var ErrInvalid = errors.New("action not valid in current state")
ErrInvalid is wrapped by the error Do returns when no handler is registered for the action's type in the machine's current state.
var ErrStopped = errors.New("machine stopped")
ErrStopped is wrapped by the error Do returns once the machine has completed or been stopped, and returned by Wait if that happens before its condition holds.
Functions ¶
This section is empty.
Types ¶
type Builder ¶ added in v0.0.3
type Builder[S comparable, C any] struct { // contains filtered or unexported fields }
Builder wires a Spec inside Define. It is only valid during that call: using a Builder that escaped Define panics, so a Spec can never change after it exists.
func (*Builder[S, C]) After ¶ added in v0.0.3
After applies a once, d after the machine enters s, unless it left s first. It is Enter with a timer: a state that advances itself after a dwell.
func (*Builder[S, C]) Enter ¶ added in v0.0.3
Enter registers fn as an entry activity of state s. Each time a machine transitions into s from a different state, fn runs in its own goroutine with a context that is canceled when the machine next leaves s, and the machine itself, so the activity can report back with Do and observe with Wait or Read. Activities accumulate: every activity registered for s runs on entry, alongside the children Invoke binds to it.
A handler returning the state it is in is an internal transition: activities are neither canceled nor restarted, so they can keep running while actions mutate the context (progress updates, coalesced input, ...).
fn typically does work and reports the outcome with Do. The cancellation happens under the machine's lock, before the next state's handlers can run, so an activity that has been left behind either sees its context done or has its action rejected — never both accepted. fn must return once its context is done, or the goroutine leaks.
The initial state's activities run at Run. A final state's never run.
func (*Builder[S, C]) Final ¶ added in v0.0.3
func (b *Builder[S, C]) Final(states ...S)
Final declares states that complete the machine. Entering one cancels the current activities, stops every child, closes Done and stops the machine for good: later actions are rejected with ErrStopped and Result reports the final state and context. A machine with no final states runs until Stop.
func (*Builder[S, C]) Invoke ¶ added in v0.0.3
func (b *Builder[S, C]) Invoke[CS comparable, CC any]( at S, start func(*C) *Machine[CS, CC], done func(*C, CS, CC) (S, error), )
Invoke binds a child machine to state at.
On entering at, start builds the child from the parent's context — this is how a parent hands state down — usually by Running the child's own Spec. Leaving at stops the child, whatever the reason.
If the child completes during that same stay in at, done runs like a handler: it receives the parent's context along with the child's final state and a copy of its context, and the state it returns is the parent's next state. A child that completes after the parent has moved on, or that is stopped rather than completing, reports nothing.
start runs with the parent locked, so it may stash the child in the parent's context to send to it later. Registering Invoke more than once for the same state gives that state several children, each with its own done.
func (*Builder[S, C]) On ¶ added in v0.0.3
On registers fn to handle actions of type A while the machine is in state from. fn returns the next state; if it returns an error the action is rejected and the state is unchanged. Each (from, A) pair may be wired once: registering it again panics, so a duplicate is caught at Define, not discovered as a silently shadowed handler at runtime. Handlers registered in a final state never run.
type ChildDone ¶ added in v0.0.3
ChildDone is the action value Trace reports for a transition caused by a child completing (an Invoke or Spawn done callback) rather than by an action. State is the child's final state and Ctx a copy of its context, both type-erased — Trace is the one observation channel that crosses machines of different types.
type Machine ¶
type Machine[S comparable, C any] struct { // contains filtered or unexported fields }
Machine is one execution of a Spec: a current state S and a context value C.
func (*Machine[S, C]) Do ¶
Do applies action a. If no handler is registered for A in the current state, Do reports ErrInvalid; if the machine has stopped, ErrStopped. If the handler fails, its error is returned and the state is unchanged. Otherwise the machine advances to the returned state and blocked Wait calls are re-evaluated. If the state changed, the old state's activities and invoked children are canceled and the new state's are started. Do returns the state the machine is in afterwards.
Do runs the handler on the calling goroutine with the machine locked. Never call it from a handler; see Send.
func (*Machine[S, C]) Done ¶ added in v0.0.3
func (m *Machine[S, C]) Done() <-chan struct{}
Done returns a channel that is closed once the machine has completed (entered a final state) or been stopped.
func (*Machine[S, C]) Read ¶ added in v0.0.3
Read projects the state and context under the machine's lock and returns the result: a synchronized read that does not pretend to wait for anything. fn must not call other Machine methods.
func (*Machine[S, C]) Result ¶ added in v0.0.3
func (m *Machine[S, C]) Result() (S, C)
Result reports the state the machine came to rest in and a copy of its context. It is meaningful once Done is closed; before that it is a snapshot — which is exactly what Run needs to resume the machine later.
func (*Machine[S, C]) Send ¶ added in v0.0.3
Send queues a and returns immediately, taking no lock on its target. It is how machines talk to each other from inside handlers — a child reporting progress to its parent, a parent nudging a child — where Do would take a second lock and could deadlock. Queued actions are applied in order, on a goroutine of the target's own, and are rejected like any other action if they no longer fit the state they arrive in.
func (*Machine[S, C]) Spawn ¶ added in v0.0.3
func (m *Machine[S, C]) Spawn[CS comparable, CC any]( child *Machine[CS, CC], done func(*C, CS, CC) (S, error), )
Spawn attaches child to the machine itself rather than to one of its states: it survives transitions and is stopped when the parent stops. done runs when the child completes, whatever state the parent is in then, and works like Invoke's. Spawn takes no lock on the parent's state, so it is safe to call from a handler — which is where a dynamic number of children usually comes from.
func (*Machine[S, C]) Stop ¶ added in v0.0.3
func (m *Machine[S, C]) Stop()
Stop halts the machine without a transition: activities are canceled, children are stopped recursively, Done closes and later actions are rejected with ErrStopped. Result reports the state it was in. A stopped machine reports nothing to its parent — only completing does that.
func (*Machine[S, C]) Trace ¶ added in v0.0.3
Trace registers fn to observe the machine: it runs after every accepted action — including internal transitions, where from and to are equal — with the state the machine left, the action that moved it, and the state it entered. A transition caused by a child completing reports a ChildDone as its action.
Trace callbacks are delivered in acceptance order on the machine's mailbox goroutine — never under the machine's lock, and never on the goroutine that called Do — so fn may call any Machine method. They share that goroutine with Send deliveries and child reports, so keep fn fast.
Trace is the machine's journal: log it, feed it to metrics, assert on it in tests — or persist it, since a machine is Result and Run away from being snapshotted and resumed.
func (*Machine[S, C]) Wait ¶
Wait blocks until cond returns true, or reports why it never will: ctx.Err() if ctx is done first, ErrStopped if the machine completes or is stopped first — a waiter is never stranded, and never outlives its caller's deadline. cond runs with the machine locked, so it may freely read the state and context (or copy values out of them); it must not call other Machine methods. cond is evaluated immediately and then again after every successful Do, so a cond that always returns true acts as a synchronized read — though Read says that more plainly.
type Spec ¶ added in v0.0.3
type Spec[S comparable, C any] struct { // contains filtered or unexported fields }
Spec is the immutable wiring of one kind of machine: which handler runs for each (state, action type) pair, which activities and children each state owns, and which states are final. Define builds it, Run executes it. A Spec is safe to share between goroutines and to Run any number of times.
func Define ¶ added in v0.0.3
func Define[S comparable, C any](wire func(*Builder[S, C])) *Spec[S, C]
Define wires a Spec: it hands wire a Builder, and the registrations wire makes — On, Final, Enter, After, Invoke — become the Spec's permanent shape. The Builder is dead once Define returns, so the result is immutable.
The type parameters are inferred from wire's signature:
spec := state.Define(func(b *state.Builder[Phase, Job]) {
b.Final(Done)
b.On(Idle, func(j *Job, a Start) (Phase, error) { ... })
})
func (*Spec[S, C]) Run ¶ added in v0.0.3
Run stamps a running Machine out of the Spec: the machine starts in initial holding ctx as its context, and initial's activities and children run before Run returns. Running a Spec whose initial state is final completes the machine immediately — which is also how a persisted machine that had already finished resumes.
Run is also resume: pass a previously snapshotted (state, context) pair — see Result — and the machine continues exactly where it left off.
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
01-trafficlight
command
The smallest possible machine: three states cycling on a single action.
|
The smallest possible machine: three states cycling on a single action. |
|
02-vending
command
A vending machine: actions with payloads and every way a machine says no.
|
A vending machine: actions with payloads and every way a machine says no. |
|
03-retry
command
A job that retries on failure: error-driven flow and terminal states.
|
A job that retries on failure: error-driven flow and terminal states. |
|
04-progress
command
A download with live observers: every Wait notification pattern.
|
A download with live observers: every Wait notification pattern. |
|
05-once
command
Racing goroutines: exactly-once claim, first Do wins.
|
Racing goroutines: exactly-once claim, first Do wins. |
|
06-cardgame
command
Command cardgame demonstrates ella.to/state with a 4-player trick-taking game.
|
Command cardgame demonstrates ella.to/state with a 4-player trick-taking game. |
|
07-async
command
Async work owned by a state: Enter runs the fetch, transitions cancel it.
|
Async work owned by a state: Enter runs the fetch, transitions cancel it. |
|
08-timers
command
Timers: states that advance themselves after a dwell.
|
Timers: states that advance themselves after a dwell. |
|
09-typeahead
command
Search-as-you-type: one activity serving many queries, stale results dropped, in-flight fetches canceled.
|
Search-as-you-type: one activity serving many queries, stale results dropped, in-flight fetches canceled. |
|
10-supervisor
command
A supervision tree: a pool machine spawns one worker machine per task, workers report progress up while they run, and stopping the pool halts every worker — recursively, and without any bookkeeping.
|
A supervision tree: a pool machine spawns one worker machine per task, workers report progress up while they run, and stopping the pool halts every worker — recursively, and without any bookkeeping. |
|
11-pipeline
command
A three-step pipeline driven by child machines: each parent step hands its payload to a fresh child machine, and the child completing is what advances the parent to the next step.
|
A three-step pipeline driven by child machines: each parent step hands its payload to a fresh child machine, and the child completing is what advances the parent to the next step. |
|
12-journal
command
A durable order: Trace journals every transition, and a snapshot survives a process restart because Run is also resume.
|
A durable order: Trace journals every transition, and a snapshot survives a process restart because Run is also resume. |