// Package fsm implements a finite state machine with specific features: // // # Operation // // - All data types are parametric and checked: state key, event key, and event data // - Low-level (push) mode: clients call its Handle method, feeding events to these calls // - High-level (pull) mode: machine receives events from a Feed // This enables automatic progress on clock ticks, generating tick events which can be Transition triggers. // - Feed for high-level mode is injectable, defaulting to a channel. // // # Hooks // // - Events support guards and action hooks. // - States support enter and leave hooks // - Hooks have a unified signature // // # Event payloads // // - Events can carry a typed payload, available to all hooks. // - All machine types are parameterized on it, enabling strong typing. // // # Contexts // // - Machine carries a context enabling global cancellation // - Handlers all take a per-event context enabling individual cancellation // - Handler contexts should be either nil, which uses the machine context, or based on that context. package fsm import ( "context" "time" ) type Hook[K comparable, EV any] func(ctx context.Context, event Event[K, EV]) error type State[SK, EK comparable, EV any] interface { Name() SK SetAfterEnter([]Hook[EK, EV]) AfterEnter() []Hook[EK, EV] SetBeforeLeave([]Hook[EK, EV]) BeforeLeave() []Hook[EK, EV] } type Event[EK comparable, EV any] interface { Name() EK Data() EV } type RetryableError interface { error RetryableError() } type Transition[SK, EK comparable, EV any] interface { SetGuards([]Hook[EK, EV]) Guards() []Hook[EK, EV] SetActions([]Hook[EK, EV]) Actions() []Hook[EK, EV] Next() SK } // Matrix describes the set of Transition defining an FSM. type Matrix[SK, EK comparable, EV any] map[SK]map[EK]Transition[SK, EK, EV] // BackoffFunc returning <0 means the retry limit has been reached. type BackoffFunc func() time.Duration type FSM[SK, EK comparable, EV any] interface { Context() context.Context Name() string SetBackoff(fn BackoffFunc) // SetFeed allows the FSM to support pull mode. SetFeed(feed Feed[SK, EK, EV]) // SetMatrix defines the Transition matrix on an FSM. SetMatrix(mx Matrix[SK, EK, EV]) // SetOnUnavailable defines optional behaviour when receiving an event not accepted by the current state. // Default is OnErrorIgnore. SetOnUnavailable(OnError[SK, EK]) // SetOnGuardFailure defines optional behaviour when a guard rejects a transition attempt. // Default is OnErrorReturn. SetOnGuardFailure(OnError[SK, EK]) // SetOnActionFailure defines optional behaviour when failing an event action. // Default is OnErrorReturn. SetOnActionFailure(OnError[SK, EK]) // SetOnEnterFailure defines optional behaviour when a state entry fails. // Default is OnErrorReturn. SetOnEnterFailure(OnError[SK, EK]) // SetOnLeaveFailure defines optional behaviour when a state leave fails. // Default is OnErrorReturn. SetOnLeaveFailure(OnError[SK, EK]) StartState() State[SK, EK, EV] EndState() State[SK, EK, EV] ErrorState() State[SK, EK, EV] } type PushMachine[SK, EK comparable, EV any] interface { FSM[SK, EK, EV] Handle(context.Context, Event[EK, EV]) error // If ctx == nil, handling uses the machine context. } type PullMachine[SK, EK comparable, EV any] interface { FSM[SK, EK, EV] Start(ctx context.Context, events chan<- Event[EK, EV]) State[SK, EK, EV] } var ( BackoffInitialDelay = time.Millisecond BackoffMaxAttempts = 10 DefaultBackoffAttempts = 0 // NoBackoff retries without delay, but only up to the maximum number of retries. NoBackoff BackoffFunc = func() time.Duration { DefaultBackoffAttempts++ if DefaultBackoffAttempts > BackoffMaxAttempts { return -1 } return YoloBackoff() } // ExponentialBackoff retries with an exponential delay and maximum number of retries. ExponentialBackoff BackoffFunc = func() time.Duration { d := NoBackoff() if d < 0 { return d } return (1 << DefaultBackoffAttempts) * BackoffInitialDelay } // YoloBackoff retries indefinitely without delay. Not a good idea outside specific test cases. YoloBackoff BackoffFunc = func() time.Duration { return 0 } ) type Feed[SK, EK comparable, EV any] func(fsm FSM[SK, EK, EV]) Event[EK, EV]