Skip to content

Latest commit

 

History

History
169 lines (119 loc) · 8.19 KB

File metadata and controls

169 lines (119 loc) · 8.19 KB

Runtime & Execution

The experimental package provides the reference implementation of instrumentation.QuantumMachine. This document explains how it interprets a model and processes events.

Initialization

  • Call qm.Init(ctx, machineContext) to boot every universe referenced in QuantumMachineModel.Initials.
  • Use qm.InitWithEvent(ctx, machineContext, event) to inject a custom event during initialization.
  • A machine can only be initialized once. Calling Init() or InitWithEvent() again returns an error. To restore a previously initialized machine, use LoadSnapshot() instead.
  • Each reference in initials is validated and mapped to an ExUniverse instance.
  • Universes without an initial reality enter superposition. Universes with an initial reality execute:
    1. Machine-level entry constants.
    2. Universe-level entry constants.
    3. Reality always transitions (in order).
    4. Reality entry actions/invokes.

Manual Positioning

Instead of using standard initialization, you can manually position the machine in a specific state:

  • PositionMachine(ctx, machineContext, universeID, realityID, executeFlow) - Position in a specific universe and reality by ID.
  • PositionMachineOnInitial(ctx, machineContext, universeID, executeFlow) - Position in a universe's configured initial state.
  • PositionMachineByCanonicalName(ctx, machineContext, universeCanonicalName, realityID, executeFlow) - Position using the universe's canonical name instead of ID.
  • PositionMachineOnInitialByCanonicalName(ctx, machineContext, universeCanonicalName, executeFlow) - Combine canonical name lookup with initial state positioning.

The executeFlow parameter controls whether to run the full entry flow (actions, transitions) or simply position without execution. This is useful for:

  • Restoring machines to a known state for testing
  • Skipping initialization logic when loading from persistent storage
  • Debugging specific states without executing the full workflow

Event Routing

  1. SendEvent locks the machine and finds universes that can handle the event.
  2. For each active universe:
    • Superposition universes accumulate the event per reality.
    • Non-superposition universes forward the event to the current reality’s transition list (on).
  3. Transitions that target other universes are queued as “external targets” so the machine can fan out events to multiple destinations.

If no universe handles the event, SendEvent returns (false, nil).

Superposition Lifecycle

  • A universe is in superposition when currentReality is nil.
  • Events are stored in an accumulator until a transition is ready to fire.
  • Observers and conditions consult AccumulatorStatistics to decide whether to collapse into a concrete reality.
  • When a new reality becomes active, establishNewReality runs entry logic, updates tracking, and may emit further transitions.

Tip: design observers/conditions to eventually return true; otherwise the universe will remain in superposition indefinitely.

Actions & Invokes

  • Actions run synchronously. Any error stops the transition and the machine remains in the previous state.
  • Invokes run asynchronously on separate goroutines. They are "fire-and-forget" and do not affect control flow.
  • Both receive instrumentation executor arguments including the machine context, universe metadata, event payload, and snapshot accessors.

EmitEvent — Internal Event Emission

Entry actions can emit internal events via args.EmitEvent(eventName, data). After all entry actions complete, emitted events are processed against the current reality's On handlers. If a transition is approved (conditions pass), the machine advances automatically — no external SendEvent needed.

This is similar to XState's raise: an internal event processed within the same operation.

Execution flow:

  1. Reality entry actions run (synchronously, in order).
  2. Emitted events are collected in a FIFO queue.
  3. After all entry actions + invokes complete, each emitted event is matched against On handlers.
  4. The first event that triggers an approved transition wins. Remaining events are discarded.
  5. If the new reality also has entry actions that emit, the process chains recursively (max depth: 10).

Where EmitEvent works:

Context EmitEvent Reason
Entry actions Yes Primary use case
Constants entry actions Yes Same semantics as reality entry
Exit actions No-op + warning Reality is being left, no On handlers apply
Transition actions No-op + warning Target already determined

Example:

builtin.RegisterAction("action:createForm", func(ctx context.Context, args instrumentation.ActionExecutorArgs) error {
    templateId := args.GetAction().Args["templateId"].(string)
    // ... create the form ...

    // Emit event to auto-advance. The existing On handler for "create-form"
    // will evaluate its conditions and transition if approved.
    args.EmitEvent("create-form", map[string]any{"templateId": templateId})
    return nil
})

Zero changes to the JSON definition. The existing on.create-form transition with its conditions does the rest.

Error handling: Chained emits (A emits -> B -> B emits -> C -> ...) are capped at depth 10. Exceeding this limit returns an error and leaves the machine instance in an unrecoverable state. This always indicates a bug in the state machine definition (infinite loop). The caller should discard the machine instance.

Backward compatibility: If no action calls EmitEvent, behavior is identical to before. Zero overhead when unused.

Universal Constants Ordering

  1. Machine-level entry/exit invocations and actions.
  2. Universe-level entry/exit invocations and actions.
  3. Reality-specific entry/exit logic.
  4. Transition-level actions/invokes (after step 2, before the new reality executes its entry logic).

Conditions & Observers

  • Observers run in parallel; the first success wins. Errors are propagated unless another observer has already authorized the transition.
  • TransitionModel.condition and conditions arrays are evaluated sequentially. All must return true for the transition to proceed.

Snapshots

qm.GetSnapshot() returns an instrumentation.MachineSnapshot containing:

  • Resume: active, finalized, and superposition universes grouped by canonical name.
  • Snapshots: serialized per-universe state (including accumulators and metadata).
  • Tracking: ordered history of realities visited per universe.

Use qm.LoadSnapshot(snapshot, machineContext) to restore a machine. Snapshots capture the latest machine context metadata but you must provide any external context objects when reloading.

qm.ReplayOnEntry(ctx) re-executes entry actions and invokes for every active universe without changing reality assignments. This is useful when you need to re-run side effects after downtime.

Snapshot Management

Loading Snapshots:

Use qm.LoadSnapshot(snapshot, machineContext) to restore a machine from a previous snapshot. This replaces:

  • All universe states (current realities, superposition states)
  • Event accumulators and their statistics
  • Tracking history for all universes
  • The machine context (provided as parameter)

Snapshots are useful for:

  • Persisting state to disk or database
  • Recovering from crashes or restarts
  • Creating checkpoints for rollback scenarios
  • Testing with pre-configured states

Error Handling

  • Most runtime errors originate from actions, invokes, observers, or invalid transitions.
  • When an action fails during a transition, the machine rolls back to the previous reality.
  • Errors bubble up to the caller of Init/SendEvent/ReplayOnEntry. Handle them at the application level (retry, alert, compensating transaction, etc.).

Extending the Runtime

The experimental runtime implements all instrumentation interfaces. You can build your own runtime by:

  1. Implementing instrumentation.QuantumMachine (possibly reusing theoretical models).
  2. Providing your own accumulator or metadata strategy.
  3. Re-registering actions/observers/invokes via the builtin package or custom registries.

Consult instrumentation.md for the list of contracts you must satisfy.