machine

package
v0.7.0-pre1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2024 License: MIT Imports: 18 Imported by: 8

README

/pkg/machine

-> go back to monorepo /

asyncmachine-go is a minimal implementation of AsyncMachine (2012-2019) in Golang using channels and context. It aims at simplicity and speed, while maintaining and extending ideas of the original. It delivers a solid toolset and conventions for reliable state machines.

asyncmachine can transform blocking APIs into controllable state machines with ease. It shares similarities with Ergo's actor model, but focuses on workflows like Temporal. Unlike both mentioned frameworks, it's lightweight and adds features progressively.

Comparison

Common differences between asyncmachine and other state machines:

  • many states can be active at the same time
  • transitions between all the states are allowed
  • states are connected by relations
  • every transition can be rejected
  • every state has a clock
  • error is a state

Buzzwords

AM tech: event emitter, queue, dependency graph, AOP, logical clocks, 3k LoC, stdlib-only

AM provides: states, events, thread-safety, logging, metrics, traces, debugger, history, flow constraints, scheduler

Flow constraints: state mutations, negotiation, relations, "when" methods, state contexts, external contexts

Basic Usage

// ProcessingFile -> FileProcessed (1 async and 1 sync state)
package main

import am "github.com/pancsta/asyncmachine-go/pkg/machine"

func main() {
    // init the state machine
    mach := am.New(nil, am.Struct{
        "ProcessingFile": { // async
            Add: am.S{"InProgress"},
            Remove: am.S{"FileProcessed"},
        },
        "FileProcessed": { // async
            Remove: am.S{"ProcessingFile", "InProgress"},
        },
        "InProgress": {}, // sync
    }, nil)
    mach.BindHandlers(&Handlers{
        Filename: "README.md",
    })
    // change the state
    mach.Add1("ProcessingFile", nil)
    // wait for completed
    select {
    case <-time.After(5 * time.Second):
        println("timeout")
    case <-mach.WhenErr(nil):
        println("err:", mach.Err)
    case <-mach.When1("FileProcessed", nil):
        println("done")
    }
}

type Handlers struct {
    Filename string
}

// negotiation handler
func (h *Handlers) ProcessingFileEnter(e *am.Event) bool {
    // read-only ops
    // decide if moving fwd is ok
    // no blocking
    // lock-free critical zone
    return true
}

// final handler
func (h *Handlers) ProcessingFileState(e *am.Event) {
    // read & write ops
    // no blocking
    // lock-free critical zone
    mach := e.Machine
    // tick-based context
    stateCtx := mach.NewStateCtx("ProcessingFile")
    go func() {
        // block in the background, locks needed
        if stateCtx.Err() != nil {
            return // expired
        }
        // blocking call
        err := processFile(h.Filename, stateCtx)
        if err != nil {
            mach.AddErr(err)
            return
        }
        // re-check the tick ctx after a blocking call
        if stateCtx.Err() != nil {
            return // expired
        }
        // move to the next state in the flow
        mach.Add1("FileProcessed", nil)
    }()
}

Waiting

// wait until FileDownloaded becomes active
<-mach.When1("FileDownloaded", nil)

// wait until FileDownloaded becomes inactive
<-mach.WhenNot1("DownloadingFile", args, nil)

// wait for EventConnected to be activated with an arg ID=123
<-mach.WhenArgs("EventConnected", am.A{"ID": 123}, nil)

// wait for Foo to have a tick >= 6 and Bar tick >= 10
<-mach.WhenTime(am.S{"Foo", "Bar"}, am.Time{6, 10}, nil)

// wait for DownloadingFile to have a tick increased by 2 since now
<-mach.WhenTicks("DownloadingFile", 2, nil)

// wait for an error
<-mach.WhenErr()

See docs/cookbook.md for more snippets.

Demo

Play with a live debugging session - interactively use the TUI debugger with data pre-generated by libp2p-pubsub-simulator in:

Examples

All examples and benchmarks can be found in /examples, including ports of Temporal workflows.

Tools

am-dbg am-dbg

Case Studies

Several case studies are available to show how to implement various types of state machines, measure performance and produce a lot of inspectable data.

Documentation

API

Most common API methods are listed below. There's more for local state machines, but all of these are also implemented in the transparent RPC layer.

Expand MachineApi
// MachineApi is a subset of `pkg/machine#Machine` for alternative
// implementations.
type MachineApi interface {

    // ///// REMOTE

    // Mutations (remote)

    Add1(state string, args am.A) am.Result
    Add(states am.S, args am.A) am.Result
    Remove1(state string, args am.A) am.Result
    Remove(states am.S, args am.A) am.Result
    Set(states am.S, args am.A) am.Result
    AddErr(err error, args am.A) am.Result
    AddErrState(state string, err error, args am.A) am.Result

    // Waiting (remote)

    WhenArgs(state string, args am.A, ctx context.Context) <-chan struct{}

    // Getters (remote)

    Err() error

    // Misc (remote)

    Log(msg string, args ...any)

    // ///// LOCAL

    // Checking (local)

    IsErr() bool
    Is(states am.S) bool
    Is1(state string) bool
    Not(states am.S) bool
    Not1(state string) bool
    Any(states ...am.S) bool
    Any1(state ...string) bool
    Has(states am.S) bool
    Has1(state string) bool
    IsTime(time am.Time, states am.S) bool
    IsClock(clock am.Clock) bool

    // Waiting (local)

    When(states am.S, ctx context.Context) <-chan struct{}
    When1(state string, ctx context.Context) <-chan struct{}
    WhenNot(states am.S, ctx context.Context) <-chan struct{}
    WhenNot1(state string, ctx context.Context) <-chan struct{}
    WhenTime(
        states am.S, times am.Time, ctx context.Context) <-chan struct{}
    WhenTicks(state string, ticks int, ctx context.Context) <-chan struct{}
    WhenTicksEq(state string, tick uint64, ctx context.Context) <-chan struct{}
    WhenErr(ctx context.Context) <-chan struct{}

    // Getters (local)

    StateNames() am.S
    ActiveStates() am.S
    Tick(state string) uint64
    Clock(states am.S) am.Clock
    Time(states am.S) am.Time
    TimeSum(states am.S) uint64
    NewStateCtx(state string) context.Context
    Export() *am.Serialized
    GetStruct() am.Struct

    // Misc (local)

    String() string
    StringAll() string
    Inspect(states am.S) string
    Index(state string) int
    Dispose()
    WhenDisposed() <-chan struct{}
}

Tests

It's very easy to get a grasp of how asyncmachine works by reading the idiomatic test suite. Consider the example below of a method used to wait for certain arguments passing via a state activation:

func TestWhenArgs(t *testing.T) {
    // init
    m := NewRels(t, nil)

    // bind
    whenCh := m.WhenArgs("B", A{"foo": "bar"}, nil)

    // incorrect args
    m.Add1("B", A{"foo": "foo"})
    select {
    case <-whenCh:
        t.Fatal("when shouldnt be resolved")
    default:
        // pass
    }

    // correct args
    m.Add1("B", A{"foo": "bar"})
    select {
    case <-whenCh:
        // pass
    default:
        t.Fatal("when should be resolved")
    }

    // dispose
    m.Dispose()
    <-m.WhenDisposed()
}

monorepo

Go back to the monorepo root to continue reading.

Documentation

Overview

Package machine is a general purpose state machine for managing complex async workflows in a safe and structured way.

It's a dependency-free implementation of AsyncMachine in Golang using channels and context. It aims at simplicity and speed.

Index

Examples

Constants

View Source
const (
	// Exception is a name the Exception state.
	Exception = "Exception"
	// Any is a name of a meta state used in catch-all handlers.
	Any = "Any"
)

Variables

View Source
var (
	// ErrStateUnknown indicates that the state is unknown.
	ErrStateUnknown = errors.New("state unknown")
	// ErrCanceled can be used to indicate a canceled Transition. Not used ATM.
	ErrCanceled = errors.New("transition canceled")
	// ErrQueued can be used to indicate a queued Transition. Not used ATM.
	ErrQueued = errors.New("transition queued")
	// ErrInvalidArgs can be used to indicate invalid arguments. Not used ATM.
	ErrInvalidArgs = errors.New("invalid arguments")
	// ErrHandlerTimeout can be used to indicate timed out mutation. Not used ATM.
	// TODO pass to AddErr() when a handler timeout happens
	ErrHandlerTimeout = errors.New("handler timeout")
)

Functions

func IsActiveTick added in v0.5.0

func IsActiveTick(tick uint64) bool

IsActiveTick returns true if the tick represents an active state (odd number).

func IsTimeAfter

func IsTimeAfter(time1, time2 Time) bool

IsTimeAfter checks if time1 is after time2. Requires a deterministic states order, e.g. by using Machine.VerifyStates.

func MockClock added in v0.7.0

func MockClock(mach *Machine, clock Clock)

MockClock mocks the internal clock of the machine. Only for testing.

func NewArgsMapper added in v0.5.0

func NewArgsMapper(names []string, maxlen int) func(args A) map[string]string

NewArgsMapper returns a matcher function for LogArgs. Useful for debugging untyped argument maps.

maxlen: maximum length of the string representation of the argument (default=20).

func NormalizeID added in v0.5.0

func NormalizeID(id string) string

Types

type A

type A map[string]any

A (arguments) is a map of named arguments for a Mutation.

type Clock added in v0.7.0

type Clock map[string]uint64

Clock is a map of state names to their clock tick values. It's like Time, but indexed by string, instead of int.

type DefaultRelationsResolver

type DefaultRelationsResolver struct {
	Machine    *Machine
	Transition *Transition
}

DefaultRelationsResolver is the default implementation of the RelationsResolver with Add, Remove, Require and After. It can be overridden using Opts.Resolver.

func (*DefaultRelationsResolver) GetAutoMutation

func (rr *DefaultRelationsResolver) GetAutoMutation() *Mutation

GetAutoMutation implements RelationsResolver.GetAutoMutation.

func (*DefaultRelationsResolver) GetRelationsBetween added in v0.5.0

func (rr *DefaultRelationsResolver) GetRelationsBetween(
	fromState, toState string,
) ([]Relation, error)

GetRelationsBetween returns a list of directional relation types between the given states. Not thread safe.

func (*DefaultRelationsResolver) GetRelationsOf added in v0.5.0

func (rr *DefaultRelationsResolver) GetRelationsOf(fromState string) (
	[]Relation, error,
)

GetRelationsOf returns a list of relation types of the given state. Not thread safe.

func (*DefaultRelationsResolver) GetTargetStates

func (rr *DefaultRelationsResolver) GetTargetStates(
	t *Transition, calledStates S,
) S

GetTargetStates implements RelationsResolver.GetTargetStates.

func (*DefaultRelationsResolver) SortStates

func (rr *DefaultRelationsResolver) SortStates(states S)

SortStates implements RelationsResolver.SortStates.

type Event

type Event struct {
	// Name of the event / handler
	Name string
	// Machine is the machine that the event belongs to, it can be used to access
	// the current Transition and Mutation.
	Machine *Machine
	// Args is a map of named arguments for a Mutation.
	Args A
	// contains filtered or unexported fields
}

Event struct represents a single event of a Mutation withing a Transition. One event can have 0-n handlers.

func (*Event) Mutation added in v0.5.0

func (e *Event) Mutation() *Mutation

Mutation returns the Mutation of an Event.

func (*Event) Transition added in v0.5.0

func (e *Event) Transition() *Transition

Transition returns the Transition of an Event.

type ExceptionArgsPanic

type ExceptionArgsPanic struct {
	CalledStates S
	StatesBefore S
	Transition   *Transition
	LastStep     *Step
	StackTrace   string
}

ExceptionArgsPanic is an optional argument ["panic"] for the Exception state which describes a panic within a Transition handler.

type ExceptionHandler

type ExceptionHandler struct{}

ExceptionHandler provide a basic Exception state support, as should be embedded into handler structs in most of the cases.

func (*ExceptionHandler) ExceptionState

func (eh *ExceptionHandler) ExceptionState(e *Event)

ExceptionState is a final entry handler for the Exception state. Args: - err error: The error that caused the Exception state. - panic *ExceptionArgsPanic: Optional details about the panic.

type IndexStateCtx added in v0.7.0

type IndexStateCtx map[string][]context.CancelFunc

IndexStateCtx is a map of (single) state names to a context cancel function

type IndexWhen added in v0.7.0

type IndexWhen map[string][]*WhenBinding

IndexWhen is a map of (single) state names to a list of activation or de-activation bindings

type IndexWhenArgs added in v0.7.0

type IndexWhenArgs map[string][]*WhenArgsBinding

IndexWhenArgs is a map of (single) state names to a list of args value bindings

type IndexWhenTime added in v0.7.0

type IndexWhenTime map[string][]*WhenTimeBinding

IndexWhenTime is a map of (single) state names to a list of time bindings

type LogEntry added in v0.6.0

type LogEntry struct {
	Level LogLevel
	Text  string
}

type LogLevel

type LogLevel int

LogLevel enum

const (
	// LogNothing means no logging, including external msgs.
	LogNothing LogLevel = iota
	// LogChanges means logging state changes and external msgs.
	LogChanges
	// LogOps means LogChanges + logging all the operations.
	LogOps
	// LogDecisions means LogOps + logging all the decisions behind them.
	LogDecisions
	// LogEverything means LogDecisions + all event and handler names, and more.
	LogEverything
)

func EnvLogLevel added in v0.7.0

func EnvLogLevel(name string) LogLevel

EnvLogLevel returns a log level from an environment variable, AM_LOG by default.

func (LogLevel) String added in v0.3.0

func (l LogLevel) String() string

type Logger

type Logger func(level LogLevel, msg string, args ...any)

Logger is a logging function for the machine.

type Machine

type Machine struct {
	// Unique ID of this machine. Default: random ID. Read-only.
	ID string
	// Time for a handler to execute. Default: time.Second
	HandlerTimeout time.Duration
	// If true, the machine will print all exceptions to stdout. Default: true.
	// Requires an ExceptionHandler binding and Machine.PanicToException set.
	LogStackTrace bool
	// If true, the machine will catch panic and trigger the Exception state.
	// Default: true.
	PanicToException bool
	// If true, logs will start with machine's ID (5 chars).
	// Default: true.
	LogID bool
	// Ctx is the context of the machine. Read-only.
	Ctx context.Context
	// Maximum number of mutations that can be queued. Default: 1000.
	QueueLimit int
	// Confirms the states have been ordered using VerifyStates. Read-only.
	StatesVerified bool
	// Tracers are optional tracers for telemetry integrations.
	Tracers []Tracer
	// If true, the machine has been Disposed and is no-op. Read-only.
	Disposed atomic.Bool
	// ParentID is the ID of the parent machine (if any).
	ParentID string
	// Tags are optional tags for telemetry integrations etc.
	Tags []string
	// contains filtered or unexported fields
}

Machine represent states, provides mutation methods, helpers methods and info about the current and scheduled transitions (if any).

func New

func New(ctx context.Context, statesStruct Struct, opts *Opts) *Machine

New creates a new Machine instance, bound to context and modified with optional Opts.

Example
t := &testing.T{} // Replace this with actual *testing.Time in real test cases
initialState := S{"A"}

mach := NewNoRels(t, initialState)

// machine mach ready
_ = mach
Output:

func NewCommon added in v0.5.0

func NewCommon(
	ctx context.Context, id string, statesStruct Struct, stateNames S,
	handlers any, parent *Machine, opts *Opts,
) (*Machine, error)

NewCommon creates a new Machine instance with all the common options set.

Example
t := &testing.T{} // Replace this with actual *testing.Time in real test cases
initialState := S{"A"}

mach := NewNoRels(t, initialState)

// machine mach ready
_ = mach
Output:

func (*Machine) ActiveStates

func (m *Machine) ActiveStates() S

ActiveStates returns a copy of the currently active states.

func (*Machine) Add

func (m *Machine) Add(states S, args A) Result

Add activates a list of states in the machine, returning the result of the transition (Executed, Queued, Canceled). Like every mutation method, it will resolve relations and trigger handlers.

func (*Machine) Add1 added in v0.5.0

func (m *Machine) Add1(state string, args A) Result

Add1 is a shorthand method to add a single state with the passed args.

func (*Machine) AddErr

func (m *Machine) AddErr(err error, args A) Result

AddErr is a dedicated method to add the Exception state with the passed error and optional arguments. Like every mutation method, it will resolve relations and trigger handlers. AddErr produces a stack trace of the error, if LogStackTrace is enabled.

func (*Machine) AddErrState added in v0.7.0

func (m *Machine) AddErrState(state string, err error, args A) Result

AddErrState adds a dedicated error state, along with the build in Exception state. Like every mutation method, it will resolve relations and trigger handlers. AddErrState produces a stack trace of the error, if LogStackTrace is enabled.

func (*Machine) AddFromEv added in v0.5.0

func (m *Machine) AddFromEv(states S, event *Event, args A) Result

AddFromEv - planned. TODO AddFromEv TODO AddFromCtx using state ctx?

func (*Machine) Any

func (m *Machine) Any(states ...S) bool

Any is group call to Is, returns true if any of the params return true from Is.

machine.StringAll() // ()[Foo:0 Bar:0 Baz:0]
machine.Add(S{"Foo"})
// is(Foo, Bar) or is(Bar)
machine.Any(S{"Foo", "Bar"}, S{"Bar"}) // false
// is(Foo) or is(Bar)
machine.Any(S{"Foo"}, S{"Bar"}) // true

func (*Machine) Any1 added in v0.5.0

func (m *Machine) Any1(states ...string) bool

Any1 is group call to Is1(), returns true if any of the params return true from Is1().

func (*Machine) BindHandlers

func (m *Machine) BindHandlers(handlers any) error

BindHandlers binds a struct of handler methods to the machine's states. Returns a HandlerBinding object, which signals when the binding is ready. TODO detect handlers for unknown states

func (*Machine) CanAdd added in v0.5.0

func (m *Machine) CanAdd(states S) bool

CanAdd TODO CanAdd Planned.

func (*Machine) CanRemove added in v0.5.0

func (m *Machine) CanRemove(states S) bool

CanRemove TODO CanRemove Planned.

func (*Machine) CanSet added in v0.5.0

func (m *Machine) CanSet(states S) bool

CanSet TODO CanSet Planned.

func (*Machine) Clock

func (m *Machine) Clock(states S) Clock

Clock returns current machine's clock, a state-keyed map of ticks. If states are passed, only the ticks of the passed states are returned.

func (*Machine) Dispose

func (m *Machine) Dispose()

Dispose disposes the machine and all its emitters. You can wait for the completion of the disposal with `<-mach.WhenDisposed`.

func (*Machine) DisposeForce added in v0.5.0

func (m *Machine) DisposeForce()

DisposeForce disposes the machine and all its emitters, without waiting for the queue to drain. Will cause panics.

func (*Machine) Err

func (m *Machine) Err() error

Err returns the last error.

func (*Machine) Eval added in v0.5.0

func (m *Machine) Eval(source string, fn func(), ctx context.Context) bool

Eval executes a function on the machine's queue, allowing to avoid using locks for non-handler code. Blocking code should NOT be scheduled here. Eval cannot be called within a handler's critical section, as both are using the same serial queue and will deadlock. Eval has a timeout of HandlerTimeout/2 and will return false in case it happens.

ctx: nil context defaults to machine's context.

Note: usage of Eval is discouraged. Consider using AM_DETECT_EVAL in tests.

func (*Machine) Export added in v0.6.4

func (m *Machine) Export() *Serialized

Export exports the machine state: ID, time and state names.

func (*Machine) GetLogArgs added in v0.5.0

func (m *Machine) GetLogArgs() func(args A) map[string]string

GetLogArgs returns the current log args function.

func (*Machine) GetLogLevel added in v0.3.0

func (m *Machine) GetLogLevel() LogLevel

GetLogLevel returns the log level of the machine.

func (*Machine) GetLogger added in v0.3.0

func (m *Machine) GetLogger() *Logger

GetLogger returns the current custom logger function, or nil.

func (*Machine) GetStruct added in v0.5.0

func (m *Machine) GetStruct() Struct

GetStruct returns a copy of machine's state structure.

func (*Machine) Has

func (m *Machine) Has(states S) bool

Has return true is passed states are registered in the machine. TODO test

func (*Machine) Has1 added in v0.5.0

func (m *Machine) Has1(state string) bool

Has1 is a shorthand for Has. It returns true if the passed state is registered in the machine.

func (*Machine) Import added in v0.6.4

func (m *Machine) Import(data *Serialized) error

Import imports the machine state: ID, time and state names. It's not safe to import into a machine which has already produces transitions and/or has telemetry connected.

func (*Machine) Index added in v0.7.0

func (m *Machine) Index(state string) int

Index returns the index of a state in the machine's StateNames() list.

func (*Machine) Inspect

func (m *Machine) Inspect(states S) string

Inspect returns a multi-line string representation of the machine (states, relations, clocks). states: param for ordered or partial results.

func (*Machine) Is

func (m *Machine) Is(states S) bool

Is checks if all the passed states are currently active.

machine.StringAll() // ()[Foo:0 Bar:0 Baz:0]
machine.Add(S{"Foo"})
machine.Is(S{"Foo"}) // true
machine.Is(S{"Foo", "Bar"}) // false

func (*Machine) Is1 added in v0.5.0

func (m *Machine) Is1(state string) bool

Is1 is a shorthand method to check if a single state is currently active. See Is().

func (*Machine) IsClock

func (m *Machine) IsClock(clock Clock) bool

IsClock checks if the machine has changed since the passed clock. Returns true if at least one state has changed.

func (*Machine) IsErr

func (m *Machine) IsErr() bool

IsErr checks if the machine has the Exception state currently active.

func (*Machine) IsQueued

func (m *Machine) IsQueued(mutationType MutationType, states S,
	withoutArgsOnly bool, statesStrictEqual bool, startIndex int,
) int

IsQueued checks if a particular mutation has been queued. Returns an index of the match or -1 if not found.

mutationType: add, remove, set

states: list of states used in the mutation

withoutParamsOnly: matches only mutation without the arguments object

statesStrictEqual: states of the mutation have to be exactly like `states` and not a superset. TODO test

func (*Machine) IsTime added in v0.7.0

func (m *Machine) IsTime(t Time, states S) bool

IsTime checks if the machine has changed since the passed time (list of ticks). Returns true if at least one state has changed. The states param is optional and can be used to check only a subset of states.

func (*Machine) Log

func (m *Machine) Log(msg string, args ...any)

Log logs an [extern] message unless LogNothing is set (default). Optionally redirects to a custom logger from SetLogger.

func (*Machine) MustParseStates

func (m *Machine) MustParseStates(states S) S

MustParseStates parses the states and returns them as a list. Panics when a state is not defined. It's an usafe equivalent of VerifyStates.

func (*Machine) NewStateCtx added in v0.5.0

func (m *Machine) NewStateCtx(state string) context.Context

NewStateCtx returns a new sub-context, bound to the current clock's tick of the passed state.

Context cancels when the state has been de-activated, or right away, if it isn't currently active.

State contexts are used to check state expirations and should be checked often inside goroutines. TODO reuse existing ctxs

func (*Machine) Not

func (m *Machine) Not(states S) bool

Not checks if **none** of the passed states are currently active.

machine.StringAll()
// -> ()[A:0 B:0 C:0 D:0]
machine.Add(S{"A", "B"})

// not(A) and not(C)
machine.Not(S{"A", "C"})
// -> false

// not(C) and not(D)
machine.Not(S{"C", "D"})
// -> true

func (*Machine) Not1 added in v0.5.0

func (m *Machine) Not1(state string) bool

Not1 is a shorthand method to check if a single state is currently inactive. See Not().

func (*Machine) PanicToErr added in v0.7.0

func (m *Machine) PanicToErr(args A)

PanicToErr will catch a panic and add the Exception state. Needs to be called in a defer statement, just like a recover() call.

func (*Machine) PanicToErrState added in v0.7.0

func (m *Machine) PanicToErrState(state string, args A)

PanicToErrState will catch a panic and add the Exception state, along with the passed state. Needs to be called in a defer statement, just like a recover() call.

func (*Machine) Queue

func (m *Machine) Queue() []*Mutation

Queue returns a copy of the currently active states.

func (*Machine) RegisterDisposalHandler added in v0.5.0

func (m *Machine) RegisterDisposalHandler(fn func())

RegisterDisposalHandler adds a function to be called when the machine is disposed. This function will block before mach.WhenDispose is closed.

func (*Machine) Remove

func (m *Machine) Remove(states S, args A) Result

Remove de-activates a list of states in the machine, returning the result of the transition (Executed, Queued, Canceled). Like every mutation method, it will resolve relations and trigger handlers.

func (*Machine) Remove1 added in v0.5.0

func (m *Machine) Remove1(state string, args A) Result

Remove1 is a shorthand method to remove a single state with the passed args. See Remove().

func (*Machine) RemoveFromEv added in v0.5.0

func (m *Machine) RemoveFromEv(states S, event *Event, args A) Result

RemoveFromEv TODO RemoveFromEv Planned.

func (*Machine) Resolver

func (m *Machine) Resolver() RelationsResolver

Resolver return the relation resolver, used to produce target states of transitions.

func (*Machine) Set

func (m *Machine) Set(states S, args A) Result

Set de-activates a list of states in the machine, returning the result of the transition (Executed, Queued, Canceled). Like every mutation method, it will resolve relations and trigger handlers.

func (*Machine) SetFromEv added in v0.5.0

func (m *Machine) SetFromEv(states S, event *Event, args A) Result

SetFromEv TODO SetFromEv Planned.

func (*Machine) SetLogArgs added in v0.5.0

func (m *Machine) SetLogArgs(matcher func(args A) map[string]string)

SetLogArgs accepts a function which decides which mutation arguments to log. See NewArgsMapper or create your own manually.

func (*Machine) SetLogLevel

func (m *Machine) SetLogLevel(level LogLevel)

SetLogLevel sets the log level of the machine.

func (*Machine) SetLogger

func (m *Machine) SetLogger(fn Logger)

SetLogger sets a custom logger function.

func (*Machine) SetLoggerEmpty added in v0.7.0

func (m *Machine) SetLoggerEmpty(level LogLevel)

SetLoggerEmpty creates an empty logger that does nothing and sets the log level in one call. Useful when combined with am-dbg. Requires LogChanges log level to produce any output.

func (*Machine) SetLoggerSimple added in v0.7.0

func (m *Machine) SetLoggerSimple(
	logf func(format string, args ...any), level LogLevel,
)

SetLoggerSimple takes log.Printf and sets the log level in one call. Useful for testing. Requires LogChanges log level to produce any output.

func (*Machine) SetStruct added in v0.5.0

func (m *Machine) SetStruct(statesStruct Struct, names S) error

SetStruct sets the machine's state structure. It will automatically call VerifyStates with the names param and handle EventStructChange if successful. Note: it's not recommended to change the states structure of a machine which has already produced transitions.

func (*Machine) StateNames

func (m *Machine) StateNames() S

StateNames returns a copy of all the state names.

func (*Machine) String

func (m *Machine) String() string

String returns a one line representation of the currently active states, with their clock values. Inactive states are omitted. Eg: (Foo:1 Bar:3)

func (*Machine) StringAll

func (m *Machine) StringAll() string

StringAll returns a one line representation of all the states, with their clock values. Inactive states are in square brackets. Eg: (Foo:1 Bar:3)[Baz:2]

func (*Machine) Switch added in v0.3.0

func (m *Machine) Switch(states ...string) string

Switch returns the first state from the passed list that is currently active, making it useful for switch statements.

switch mach.Switch(ss.GroupPlaying...) {
case "Playing":
case "Paused":
case "Stopped":
}

func (*Machine) Tick added in v0.7.0

func (m *Machine) Tick(state string) uint64

Tick return the current tick for a given state.

func (*Machine) Time

func (m *Machine) Time(states S) Time

Time returns machine's time, a list of ticks per state. Returned value includes the specified states, or all the states if nil.

func (*Machine) TimeSum

func (m *Machine) TimeSum(states S) uint64

TimeSum returns the sum of machine's time (ticks per state). Returned value includes the specified states, or all the states if nil. It's a very inaccurate, yet simple way to measure the machine's time. TODO handle overflow

func (*Machine) Transition

func (m *Machine) Transition() *Transition

Transition returns the current transition, if any.

func (*Machine) VerifyStates added in v0.3.0

func (m *Machine) VerifyStates(states S) error

VerifyStates verifies an array of state names and returns an error in case at least one isn't defined. It also retains the order and uses it for StateNames (only if all states have been passed). Not thread-safe.

func (*Machine) When

func (m *Machine) When(states S, ctx context.Context) <-chan struct{}

When returns a channel that will be closed when all the passed states become active or the machine gets disposed.

ctx: optional context that will close the channel when done. Useful when listening on 2 When() channels within the same `select` to GC the 2nd one. TODO re-use channels with the same state set and context

func (*Machine) When1 added in v0.5.0

func (m *Machine) When1(state string, ctx context.Context) <-chan struct{}

When1 is an alias to When() for a single state. See When.

func (*Machine) WhenArgs added in v0.5.0

func (m *Machine) WhenArgs(
	state string, args A, ctx context.Context,
) <-chan struct{}

WhenArgs returns a channel that will be closed when the passed state becomes active with all the passed args. Args are compared using the native '=='. It's meant to be used with async Multi states, to filter out a specific completion.

func (*Machine) WhenDisposed added in v0.5.0

func (m *Machine) WhenDisposed() <-chan struct{}

WhenDisposed returns a channel that will be closed when the machine is disposed. Requires bound handlers. Use Machine.Disposed in case no handlers have been bound.

func (*Machine) WhenErr

func (m *Machine) WhenErr(ctx context.Context) <-chan struct{}

WhenErr returns a channel that will be closed when the machine is in the Exception state.

ctx: optional context defaults to the machine's context.

func (*Machine) WhenNot

func (m *Machine) WhenNot(states S, ctx context.Context) <-chan struct{}

WhenNot returns a channel that will be closed when all the passed states become inactive or the machine gets disposed.

ctx: optional context that will close the channel when done. Useful when listening on 2 WhenNot() channels within the same `select` to GC the 2nd one.

func (*Machine) WhenNot1 added in v0.5.0

func (m *Machine) WhenNot1(state string, ctx context.Context) <-chan struct{}

WhenNot1 is an alias to WhenNot() for a single state. See WhenNot.

func (*Machine) WhenQueueEnds added in v0.5.0

func (m *Machine) WhenQueueEnds(ctx context.Context) <-chan struct{}

WhenQueueEnds closes every time the queue ends, or the optional ctx expires.

func (*Machine) WhenTicks added in v0.5.0

func (m *Machine) WhenTicks(
	state string, ticks int, ctx context.Context,
) <-chan struct{}

WhenTicks waits N ticks of a single state (relative to now). Uses WhenTime underneath.

func (*Machine) WhenTicksEq added in v0.5.0

func (m *Machine) WhenTicksEq(
	state string, ticks uint64, ctx context.Context,
) <-chan struct{}

WhenTicksEq waits till ticks for a single state equal the given absolute value (or more). Uses WhenTime underneath.

func (*Machine) WhenTime added in v0.5.0

func (m *Machine) WhenTime(
	states S, times Time, ctx context.Context,
) <-chan struct{}

WhenTime returns a channel that will be closed when all the passed states have passed the specified time. The time is a logical clock of the state. Machine time can be sourced from the Time() method, or Clock() for a specific state.

type Mutation

type Mutation struct {
	// add, set, remove
	Type MutationType
	// states explicitly passed to the mutation method
	CalledStates S
	// argument map passed to the mutation method (if any).
	Args A
	// this mutation has been triggered by an auto state
	Auto bool
	// specific context for this mutation (optional)
	Ctx context.Context
	// optional eval func, only for MutationEval
	Eval func()
}

Mutation represents an atomic change (or an attempt) of machine's active states. Mutation causes a Transition.

func (Mutation) StateWasCalled added in v0.5.0

func (m Mutation) StateWasCalled(state string) bool

StateWasCalled returns true if the Mutation was called (directly) with the passed state (in opposite to it coming from an `Add` relation). TODO change to CalledIs(), CalledIs1(), CalledAny(), CalledAny1()

type MutationType

type MutationType int

MutationType enum

const (
	MutationAdd MutationType = iota
	MutationRemove
	MutationSet
	MutationEval
)

func (MutationType) String

func (m MutationType) String() string

type NoOpTracer added in v0.6.0

type NoOpTracer struct{}

NoOpTracer is a no-op implementation of Tracer, used for embedding.

func (*NoOpTracer) End added in v0.6.0

func (t *NoOpTracer) End()

func (*NoOpTracer) HandlerEnd added in v0.6.0

func (t *NoOpTracer) HandlerEnd(
	transition *Transition, emitter string, handler string)

func (*NoOpTracer) HandlerStart added in v0.6.0

func (t *NoOpTracer) HandlerStart(
	transition *Transition, emitter string, handler string)

func (*NoOpTracer) Inheritable added in v0.6.0

func (t *NoOpTracer) Inheritable() bool

func (*NoOpTracer) MachineDispose added in v0.6.0

func (t *NoOpTracer) MachineDispose(machID string)

func (*NoOpTracer) MachineInit added in v0.6.0

func (t *NoOpTracer) MachineInit(machine *Machine)

func (*NoOpTracer) NewSubmachine added in v0.6.0

func (t *NoOpTracer) NewSubmachine(parent, machine *Machine)

func (*NoOpTracer) QueueEnd added in v0.6.0

func (t *NoOpTracer) QueueEnd(machine *Machine)

func (*NoOpTracer) StructChange added in v0.7.0

func (t *NoOpTracer) StructChange(machine *Machine, old Struct)

func (*NoOpTracer) TransitionEnd added in v0.6.0

func (t *NoOpTracer) TransitionEnd(transition *Transition)

func (*NoOpTracer) TransitionInit added in v0.6.0

func (t *NoOpTracer) TransitionInit(transition *Transition)

func (*NoOpTracer) TransitionStart added in v0.7.0

func (t *NoOpTracer) TransitionStart(transition *Transition)

func (*NoOpTracer) VerifyStates added in v0.7.0

func (t *NoOpTracer) VerifyStates(machine *Machine)

type Opts

type Opts struct {
	// Unique ID of this machine. Default: random ID.
	ID string
	// Time for a handler to execute. Default: time.Second
	HandlerTimeout time.Duration
	// If true, the machine will NOT print all exceptions to stdout.
	DontLogStackTrace bool
	// If true, the machine will die on panics.
	DontPanicToException bool
	// If true, the machine will NOT prefix its logs with its ID.
	DontLogID bool
	// Custom relations resolver. Default: *DefaultRelationsResolver.
	Resolver RelationsResolver
	// Log level of the machine. Default: LogNothing.
	LogLevel LogLevel
	// Tracer for the machine. Default: nil.
	Tracers []Tracer
	// LogArgs matching function for the machine. Default: nil.
	LogArgs func(args A) map[string]string
	// Parent machine, used to inherit certain properties, e.g. tracers.
	// Overrides ParentID. Default: nil.
	Parent *Machine
	// ParentID is the ID of the parent machine. Default: "".
	ParentID string
	// Tags is a list of tags for the machine. Default: nil.
	Tags []string
	// QueueLimit is the maximum number of mutations that can be queued.
	// Default: 1000.
	// TODO per-state QueueLimit
	QueueLimit int
	// DetectEval will detect Eval call from handlers, which causes a deadlock.
	// It works in similar way as -race flag in Go and can also be triggered by
	// setting env var AM_DETECT_EVAL to "1". Default: false.
	DetectEval bool
}

Opts struct is used to configure a new Machine.

func OptsWithDebug added in v0.5.0

func OptsWithDebug(opts *Opts) *Opts

OptsWithDebug returns Opts with debug settings (DontPanicToException, long HandlerTimeout).

func OptsWithParentTracers added in v0.5.0

func OptsWithParentTracers(opts *Opts, parent *Machine) *Opts

OptsWithParentTracers returns Opts with the parent's Opts.Tracers and Opts.LogArgs.

func OptsWithTracers added in v0.5.0

func OptsWithTracers(opts *Opts, tracers ...Tracer) *Opts

OptsWithTracers returns Opts with the given tracers. Tracers are inherited by submachines (via Opts.Parent) when env.AM_DEBUG is set.

type Relation

type Relation int

Relation enum

const (
	RelationAfter Relation = iota
	RelationAdd
	RelationRequire
	RelationRemove
)

func (Relation) String

func (r Relation) String() string

type RelationsResolver

type RelationsResolver interface {
	// GetTargetStates returns the target states after parsing the relations.
	GetTargetStates(t *Transition, calledStates S) S
	// GetAutoMutation returns an (optional) auto mutation which is appended to
	// the queue after the transition is executed.
	GetAutoMutation() *Mutation
	// SortStates sorts the states in the order their handlers should be
	// executed.
	SortStates(states S)
	// GetRelationsOf returns a list of relation types of the given state.
	GetRelationsOf(fromState string) ([]Relation, error)
	// GetRelationsBetween returns a list of relation types between the given
	// states.
	GetRelationsBetween(fromState, toState string) ([]Relation, error)
}

RelationsResolver is an interface for parsing relations between states. Not thread-safe. TODO support custom relation types and additional state properties.

type Result

type Result int

Result enum is the result of a state Transition

const (
	// Executed means that the transition was executed immediately and not
	// canceled.
	Executed Result = 1 << iota
	// Canceled means that the transition was canceled, by either relations or a
	// handler.
	Canceled
	// Queued means that the transition was queued for later execution. The
	// following methods can be used to wait for the results:
	// - Machine.When
	// - Machine.WhenNot
	// - Machine.WhenArgs
	// - Machine.WhenTime
	// - Machine.WhenTicks
	// - Machine.WhenTicksEq
	Queued
	// ResultNoOp means that the transition was a no-op, i.e. the state was
	// already active. ResultNoOp is only used by helpers, and never returned by
	// the machine itself.
	ResultNoOp
)

func (Result) String

func (r Result) String() string

type S

type S []string

S (state names) is a string list of state names.

func DiffStates

func DiffStates(states1 S, states2 S) S

DiffStates returns the states that are in states1 but not in states2.

func SAdd added in v0.7.0

func SAdd(states ...S) S

SAdd concatenates multiple state lists into one, removing duplicates. Useful for merging lists of states, eg a state group with other states involved in a relation.

type Serialized added in v0.7.0

type Serialized struct {
	ID         string `json:"id"`
	Time       Time   `json:"time"`
	StateNames S      `json:"state_names"`
}

Serialized is a machine state serialized to a JSON compatible struct.

type State

type State struct {
	Auto    bool
	Multi   bool
	Require S
	Add     S
	Remove  S
	After   S
	Tags    []string
}

State defines a single state of a machine, its properties and relations.

func StateAdd added in v0.7.0

func StateAdd(source State, overlay State) State

StateAdd adds new states to relations of the source state, without removing existing ones. Useful for adjusting shared stated to a specific machine.

func StateSet added in v0.7.0

func StateSet(source State, auto, multi bool, overlay State) State

StateSet replaces passed relations and properties of the source state. Only relations in the overlay state are replaced, the rest is preserved.

type StateIsActive added in v0.7.0

type StateIsActive map[string]bool

type Step added in v0.5.0

type Step struct {
	Type StepType
	// optional, unless no ToState
	FromState string
	// optional, unless no FromState
	ToState string
	// eg a transition method name, relation type
	Data any
	// marks a final handler (FooState, FooEnd)
	IsFinal bool
	// marks a self handler (FooFoo)
	IsSelf bool
	// marks an enter handler (FooEnter). Requires IsFinal.
	IsEnter bool
}

Step struct represents a single step within a Transition, either a relation resolving step or a handler call.

type StepType added in v0.5.0

type StepType int

StepType enum

const (
	StepRelation StepType = 1 << iota
	StepHandler
	StepSet
	// StepRemove indicates a step where a state goes active->inactive
	StepRemove
	// StepRemoveNotActive indicates a step where a state goes inactive->inactive
	StepRemoveNotActive
	StepRequested
	StepCancel
)

func (StepType) String added in v0.5.0

func (tt StepType) String() string

type Struct added in v0.5.0

type Struct = map[string]State

Struct is a map of state names to state definitions.

func CloneStates added in v0.5.0

func CloneStates(stateStruct Struct) Struct

CloneStates deep clones the states struct and returns a copy.

func StructMerge added in v0.7.0

func StructMerge(stateStructs ...Struct) Struct

StructMerge merges multiple state structs into one, overriding the previous state definitions. No relation-level merging takes place.

type Time added in v0.7.0

type Time []uint64

Time is an ordered list of state ticks. It's like Clock, but indexed by int, instead of string. TODO use math/big?

func (Time) Is added in v0.7.0

func (t Time) Is(idxs []int) bool

Is checks if all the passed states were active at a given time, via indexes. See Machine.Index().

func (Time) Is1 added in v0.7.0

func (t Time) Is1(idx int) bool

Is1 checks if a state is active at a given time, via its index. See Machine.Index().

type Tracer added in v0.5.0

type Tracer interface {
	TransitionInit(transition *Transition)
	TransitionStart(transition *Transition)
	TransitionEnd(transition *Transition)
	HandlerStart(transition *Transition, emitter string, handler string)
	HandlerEnd(transition *Transition, emitter string, handler string)
	End()
	MachineInit(machine *Machine)
	MachineDispose(machID string)
	NewSubmachine(parent, machine *Machine)
	Inheritable() bool
	QueueEnd(machine *Machine)
	StructChange(machine *Machine, old Struct)
	VerifyStates(machine *Machine)
}

Tracer is an interface for logging machine transitions and events, used by Opts.Tracers.

type Transition

type Transition struct {
	// ID is a unique identifier of the transition.
	ID string
	// Steps is a list of steps taken by this transition (so far).
	Steps []*Step
	// StatesBefore is a list of states before the transition.
	StatesBefore S
	// TimeBefore is the machine time from before the transition.
	TimeBefore Time
	// TimeAfter is the machine time from after the transition. If the transition
	// has been canceled, this will be the same as TimeBefore.
	TimeAfter Time
	// Enters is a list of states with enter handlers in this transition.
	Enters S
	// Enters is a list of states with exit handlers in this transition.
	Exits S
	// TargetStates is a list of states after parsing the relations.
	TargetStates S
	// Accepted tells if the transition was accepted during the negotiation phase.
	Accepted bool
	// Mutation call which caused this transition
	Mutation *Mutation
	// Machine is the parent machine of this transition.
	Machine *Machine
	// LogEntries are log msgs produced during the transition.
	LogEntries []*LogEntry
	// PreLogEntries are log msgs produced before during the transition.
	PreLogEntries []*LogEntry
	// QueueLen is the length of the queue after the transition.
	QueueLen int
	// contains filtered or unexported fields
}

Transition represents processing of a single mutation within a machine.

func (*Transition) Args

func (t *Transition) Args() A

Args returns the argument map passed to the mutation method (or an empty one).

func (*Transition) CalledStates

func (t *Transition) CalledStates() S

CalledStates return explicitly called / requested states of the transition.

func (*Transition) ClockAfter added in v0.7.0

func (t *Transition) ClockAfter() Clock

ClockAfter return the Clock from before the transition.

func (*Transition) ClockBefore added in v0.7.0

func (t *Transition) ClockBefore() Clock

ClockBefore return the Clock from before the transition.

func (*Transition) IsAuto

func (t *Transition) IsAuto() bool

IsAuto returns true if the transition was triggered by an auto state. Thus, it cant trigger any other auto state mutations.

func (*Transition) IsCompleted

func (t *Transition) IsCompleted() bool

func (*Transition) LogArgs added in v0.5.0

func (t *Transition) LogArgs() string

LogArgs returns a text snippet with arguments which should be logged for this Mutation.

func (*Transition) String

func (t *Transition) String() string

String representation of the transition and the steps taken so far.

func (*Transition) Type

func (t *Transition) Type() MutationType

Type returns the type of the mutation (add, remove, set).

type WhenArgsBinding added in v0.7.0

type WhenArgsBinding struct {
	// contains filtered or unexported fields
}

type WhenBinding added in v0.7.0

type WhenBinding struct {
	Ch chan struct{}
	// means states are required to NOT be active
	Negation bool
	States   StateIsActive
	Matched  int
	Total    int
}

type WhenTimeBinding added in v0.7.0

type WhenTimeBinding struct {
	Ch chan struct{}
	// map of completed to their index positions
	Index map[string]int
	// number of matches so far
	Matched int
	// number of total matches needed
	Total int
	// optional Time to match for completed from Index
	Times     Time
	Completed StateIsActive
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL