class
FSM::AsyncService(T)
- FSM::AsyncService(T)
- Reference
- Object
Overview
The fiber-and-mailbox interpreter.
AsyncService(T) is a second interpreter beside Service(T), not a replacement. It serializes by ownership: exactly one fiber ever executes the machine's code, and other fibers reach it only by putting a message in the mailbox. This is the load-bearing difference from Service, which serializes by lock. The three separable concerns land here as follows: the machine definition is immutable after build and safe to share (sealing); the interpreter's own state advances atomically because only the owning fiber touches it; and nothing is guaranteed about T, which is the caller's responsibility. Service arranges the second concern with a per-instance mutex over interpreter state; AsyncService arranges it by fiber ownership instead.
Planning happens on the owning fiber. This is the purity contract: a guard reads context T while planning, and a guard on the owning fiber never sees a T some other fiber is mutating mid-plan, because the owning fiber is the only one that ever runs the machine's code. Purity is what the library asks of a guard; ownership is what the interpreter supplies.
#post is fire-and-forget and returns immediately; #send blocks the CALLING
fiber, not a thread, and reads the reply off a channel. The
rule that comes with that: post from inside a callback, send from outside. A send
from inside a callback of the same instance deadlocks, because it would be waiting
on itself; post from inside a callback remains safe. See
#send.
AsyncService exposes no context getter. Reads go through #send on the
owning fiber, which is the one place structural enforcement of context isolation
is available. That absence is deliberate and is why no such method appears below.
Included Modules
Defined in:
fsm/async_service.crClass Method Summary
-
.start(machine : Machine(T), initial : String, context : T) : AsyncService(T)
Start an async service interpreting the machine from an initial state, registering no observers.
-
.start(machine : Machine(T), initial : String, context : T, & : ObserverRegistrar -> ) : AsyncService(T)
Start an async service and register observers through a builder block.
Instance Method Summary
-
#error : Exception | Nil
The exception that errored the async service, or nil when it has not errored.
-
#errored? : Bool
Whether a callback raised under post and poisoned the async service.
-
#post(event : String) : Nil
Enqueue an event to the mailbox and return immediately.
-
#running? : Bool
Whether the owning fiber is still draining the mailbox.
-
#send(event : String) : State
Enqueue an event and block the CALLING fiber until the owning fiber replies with the State snapshot built for the step.
-
#stop : Nil
Stop the async service and return once it has stopped.
-
#stopped? : Bool
Whether the async service was stopped through #stop.
Class Method Detail
Start an async service interpreting the machine from an initial state,
registering no observers. The name pairs with
#stop and the lifecycle predicates and avoids colliding with Crystal's builtin
fiber spawn. The owning fiber and mailbox are private; the caller drives
the async service only through post, send, and stop.
Start an async service and register observers through a builder block. The block receives an ObserverRegistrar, mirroring Service.interpret: on_transition and on_event_processed handlers are copied once into the async service and the registrar is sealed before start returns.
Instance Method Detail
The exception that errored the async service, or nil when it has not errored. This is the exception a callback raised under post, recorded because post has no return value to carry the failure back.
Whether a callback raised under post and poisoned the async service. An errored async service has stopped draining its mailbox.
Enqueue an event to the mailbox and return immediately. Fire-and-forget: the work happens on the owning fiber.
A post from inside a callback is the safe direction: it is detected as running on the owning fiber and queued as a cascade, drained within the current step. A post from any other fiber goes to the mailbox.
A callback that raises under post poisons the async service: post has already returned, so there is no snapshot to hand back and the failure is recorded in the interpreter instead. A post into a stopped or errored async service is dropped silently, because there is no return value to carry a refusal.
Whether the owning fiber is still draining the mailbox. A separate concept from State#status, which describes one transaction.
Enqueue an event and block the CALLING fiber until the owning fiber replies with the State snapshot built for the step. A callback that raises under send yields a Failed snapshot returned as a value; no exception crosses the reply channel.
Send from outside a callback only. A send from inside a callback of the same instance deadlocks: the callback runs on the owning fiber, and the reply it waits on can only be produced by that same owning fiber, so it waits on itself forever. From inside a callback, post instead. A cross-fiber send made while holding a context lock the owning fiber's callback wants is the same deadlock seen through that lock, since the caller blocks on the reply while the callback blocks on the lock.
A send into a stopped or errored async service short-circuits before touching the mailbox and returns a Failed snapshot as a value: the owning fiber is no longer draining, so enqueuing and waiting for a reply would deadlock. An errored async service returns the recorded poisoning exception; a stopped one returns a StoppedError.
Stop the async service and return once it has stopped.
Synchronous: #stopped? is deterministically true after this returns. The stop
request travels through the mailbox behind any events already queued, so those
events drain before it stops; nothing queued after the stop is processed. On an
async service that has already stopped or errored the mailbox is closed, and
stop returns without effect.