|
@@ -0,0 +1,140 @@
|
|
|
+// 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]
|