The GFSM library provides a simple and fast implementation of a Finite State Machine (FSM) for Go. The library adopts C++-style approaches with a primary focus on speed and simplicity, which is the main difference from alternative Go FSM implementations like looplab/fsm.
Two Phase Commit protocol (TPC protocol) is an excellent example of Finite State Machine use case.
NOTE: Full example is in examples/two-phase-commit/main.go folder.
Having the following State Machine:
graph TD;
Init-->Wait;
Wait-->Abort;
Wait-->Commit;
we should:
- Enumerate all possible states
- Describe each states
- Describe a state machine
- Run the state machine
The GFSM library can consume any comparable
interface as a states enumerator, but the recommended approach is using int
-based types. For example, we can define type State
with all TPC protocol-related states.
type State int
const (
Init State = iota
Wait
Abort
Commit
)
Each State is represented by its unique stateID
, action
handler, and list of possible transitions
. Each state can have a unique handler that supports StateAction
interface:
type StateAction[StateIdentifier comparable] interface {
OnEnter(smCtx StateMachineContext)
OnExit(smCtx StateMachineContext)
Execute(smCtx StateMachineContext, eventCtx EventContext) StateIdentifier
}
Where OnEnter
and OnExit
will be called once on the state entering or exiting respectfully, and Execute
is the call that the state machine routes to the current state from StateMachineHandler.ProcessEvent(...)
.
Any State Machine can have its unique context. For the TPC protocol, it's important to have information about the voting process, like the expected voter count and committed ID.
type coordinatorContext struct {
commitID string
partCnt int
}
StateMachineContext
is stored inside the constructed StateMachineHandler
object and will be passed to each state on any calls (OnEnter
, OnExit
, and Execute
).
To create a new state machine, StateMachineBuilder
should be used. To build a TPC protocol state machine, you can use the following approach:
sm := gfsm.NewBuilder[State]().
SetDefaultState(Init).
SetSmContext(&coordinatorContext{partCnt: 3}).
RegisterState(Init, &initState{}, []State{Wait}).
RegisterState(Wait, &waitState{}, []State{Abort, Commit}).
RegisterState(Abort, &responseState{
keepResp: Abort,
}, []State{Init}).
RegisterState(Commit, &responseState{
keepResp: Commit,
}, []State{Init}).
Build()
After the construction, the state machine shall be executed and terminated on exit:
sm.Start()
defer sm.Stop()
To process an event, the state machine object implements a ProcessEvent(eventCtx EventContext) error
call, where eventCtx
can be any data that the state will process internally as primary input data.
err := sm.ProcessEvent(commitRequest{"commit_1"})
During the event processing, the state machine will call Execute
with the passed EventContext
and StateMachineContext
. Based on these data, the state can decide either to keep the current state (Init
in the example below) or make a switch (Wait
) by returning the new expected state.
func (s *initState) Execute(smCtx gfsm.StateMachineContext, eventCtx gfsm.EventContext) State {
cCtx := smCtx.(*coordinatorContext)
req, ok := eventCtx.(commitRequest)
if !ok {
// ...
return Init
}
// ...
return Wait
}