22. Finite state machine (1) go language fsm library

Article directory

  • 1: Introduction
    • 1. Definition
    • 2. Components
    • 3. Advantages
  • 2: github.com/looplab/fsm library
    • 1. FSM in Go
    • 2. Basic use of fsm
    • 3. When is Action (callback function, action) executed in fsm?
    • 4. Callbacks execution sequence written in the complete version
    • 5. Abbreviated version of Callbacks execution sequence
    • 6. Complete example

One: Introduction

1. Definition

Finite-state machine (Finite-state machine, FSM), referred to as state machine, is a mathematical model that represents a limited number of states and behaviors such as transitions and actions between these states.

2. Components

  • Current state (src state): The current state of the transaction.
  • Event (event): An event is a trigger condition for performing an operation. When an event is satisfied, an action will be triggered or a state transition will be performed.
  • Action (action): The action executed after the event is satisfied. After the action is executed, it can be moved to a new state or remain in the original state. Actions are not necessary. When the event is satisfied, you can also directly migrate to the new state without performing any action.
  • Secondary state (dst state): The new state to be moved to after the event is satisfied. “Secondary state” is relative to the “present state”. Once the “secondary state” is activated, it transforms into a new “present state”.
  • State transition (transition): The entire process of things changing from the current state to the next state.

Note: FSM can only be in one state at any time.

Example:

There are three states in the picture above: green, yellow, and red;

  1. State transitions are limited, green can only turn to yellow, yellow can only turn to red, and red can only turn to green; state transitions such as yellow to green are not allowed;
  2. The input condition for state transition is very simple. When 1 (event) is received, it transitions to the next state.

Simply put, a finite state machine is a machine with a set of predefined states. When the machine receives an instruction, it checks a predefined table based on the instruction content:

  1. Check whether the current state is as expected, that is, whether the machine in the current state receives specific instructions
  2. If it is not accepted, for example, if the red light status is 2, then nothing will happen.
  3. If accepted, check the target state corresponding to “current state x this instruction” in the table, and then convert the machine state to the target state (State transfer)

As for when to send instructions to the state machine, that is determined by the external system. For example, in the example of the traffic light, the external system is several timers. When the time is up, it sends a signal to the finite state machine to switch states.

Note: In this traffic light example, when an event is received, there is only a state transition and no action

3. Advantages

  • Code abstraction: Abstract and structure the business process, and divide the complex state transition diagram into the smallest units of adjacent states. This is equivalent to building Lego bricks. This mechanism can be combined into complex state transition diagrams, while hiding the complexity of the system.
  • Simplified process: The business rd only needs to pay attention to the business logic of the current operation (the business callback function during the state transfer process), which greatly decouples the state and business.
  • Easy to expand: When adding a new state or event, there is no need to modify the original state transfer logic, just establish a new state transfer link directly.
  • Business modeling: Through the splicing of adjacent states with the smallest granularity, the overall business graph is finally formed.

Two: github.com/looplab/fsm library

1. FSM in Go

Through the above definition of finite state machine, we roughly know the related concepts of state machine. There are already ready-made open source packages available in Golang.

Installation:

go get github.com/looplab/fsm

Note: The usage methods of different versions of fsm may be different. It is best to read the comments of the NewFSM function for specific details. This article takes: github.com/looplab/[email protected] as an example.

2. Basic use of fsm

Take the simplest switch as an example:

code show as below:

package main
 
import (
"context"
"fmt"
 
"github.com/looplab/fsm"
)
 
type Door struct {<!-- -->
Name string
FSM *fsm.FSM
}
 
func NewDoor(name string) *Door {<!-- -->
d := & amp;Door{<!-- -->
Name: name,
}
 
d.FSM = fsm.NewFSM(
"closed", // initial state
fsm.Events{<!-- --> // Events and state transitions, the secondary state is unique, but there can be multiple source states that can reach the secondary state, so it is the Src array
{<!-- -->Name: "open", Src: []string{<!-- -->"closed"}, Dst: "open"},
{<!-- -->Name: "close", Src: []string{<!-- -->"open"}, Dst: "closed"},
},
fsm.Callbacks{<!-- --> // Callback function: the action to be performed when encountering the corresponding event or state. The execution timing will be introduced in detail below.
"enter_state": func(_ context.Context, e *fsm.Event) {<!-- --> d.enterState(e) },
},
)
 
return d
}
 
func (d *Door) enterState(e *fsm.Event) {<!-- -->
fmt.Printf("The door's name:%s , current state:%s\
", d.Name, e.Dst)
}
 
func main() {<!-- -->
door := NewDoor("Test")
 
fmt.Printf("fsm current state: %s \
", door.FSM.Current())
 
err := door.FSM.Event(context.Background(), "open")
if err != nil {<!-- -->
fmt.Println(err)
}
fmt.Printf("fsm current state: %s \
", door.FSM.Current())
 
err = door.FSM.Event(context.Background(), "close")
if err != nil {<!-- -->
fmt.Println(err)
}
fmt.Printf("fsm current state: %s \
", door.FSM.Current())
}

Results of the:

Here, the status in FSM is changed through Event. The transfer formula is: Src, Event -> Dst, d.enterState. The general idea is that the input Event is received, the State of the state machine is Src->Dst, and Action: d.enterState is executed.

3. When is Action (callback function, action) executed in fsm?

When I first started using it, I was curious about when the callback function d.enterState(e) in the above code was called. Let's take a look at the comments in NewFSM to find out.

// NewFSM constructs a FSM from events and callbacks.
//
// The events and transitions are specified as a slice of Event structs
// specified as Events. Each Event is mapped to one or more internal
// transitions from Event.Src to Event.Dst.
// Callbacks are added as a map specified as Callbacks where the key is parsed
// as the callback event as follows, and called in the same order:
//
// 1. before_<EVENT> - called before event named <EVENT>
//
// 2. before_event - called before all events
//
// 3. leave_<OLD_STATE> - called before leaving <OLD_STATE>
//
// 4. leave_state - called before leaving all states
//
// 5. enter_<NEW_STATE> - called after entering <NEW_STATE>
//
// 6. enter_state - called after entering all states
//
// 7. after_<EVENT> - called after event named <EVENT>
//
// 8. after_event - called after all events
//
// There are also two short form versions for the most commonly used callbacks.
// They are simply the name of the event or state:
//
// 1. <NEW_STATE> - called after entering <NEW_STATE>
//
// 2. <EVENT> - called after event named <EVENT>
//
// If both a shorthand version and a full version is specified it is undefined
// which version of the callback will end up in the internal map. This is due
// to the pseudo random nature of Go maps. No checking for multiple keys is
// currently performed.

From the above we know that d.enterState(e) is executed when called after entering all states.

4. Complete version of the callbacks execution sequence

From the above comments, we can know that the execution sequence of the complete version of Callbacks is as follows:

5. Abbreviated version of Callbacks execution sequence


Note: Although there are two ways to write Callbacks, you cannot use the full version and the abbreviated version at the same time, otherwise it is uncertain which version will be used in the end.

6. Complete example

package main
 
import (
"context"
"fmt"
 
"github.com/looplab/fsm"
)
 
type Door struct {<!-- -->
Name string
FSM *fsm.FSM
}
 
func NewDoor(name string) *Door {<!-- -->
d := & amp;Door{<!-- -->
Name: name,
}
 
d.FSM = fsm.NewFSM(
"closed",
fsm.Events{<!-- -->
{<!-- -->Name: "open", Src: []string{<!-- -->"closed"}, Dst: "open"},
{<!-- -->Name: "close", Src: []string{<!-- -->"open"}, Dst: "closed"},
},
fsm.Callbacks{<!-- -->
"before_open": func(_ context.Context, e *fsm.Event) {<!-- --> d.beforeOpen(e) },
"before_event": func(_ context.Context, e *fsm.Event) {<!-- --> d.beforeEvent(e) },
"leave_closed": func(_ context.Context, e *fsm.Event) {<!-- --> d.leaveClosed(e) },
"leave_state": func(_ context.Context, e *fsm.Event) {<!-- --> d.leaveState(e) },
"enter_open": func(_ context.Context, e *fsm.Event) {<!-- --> d.enterOpen(e) },
"enter_state": func(_ context.Context, e *fsm.Event) {<!-- --> d.enterState(e) },
"after_open": func(_ context.Context, e *fsm.Event) {<!-- --> d.afterOpen(e) },
"after_event": func(_ context.Context, e *fsm.Event) {<!-- --> d.afterEvent(e) },
},
)
 
return d
}
 
func (d *Door) beforeOpen(e *fsm.Event) {<!-- -->
fmt.Printf("beforeOpen, current state:%s, Dst:%s \
", d.FSM.Current(), e.Dst)
}
 
func (d *Door) beforeEvent(e *fsm.Event) {<!-- -->
fmt.Printf("beforeEvent, current state:%s, Dst:%s \
", d.FSM.Current(), e.Dst)
}
 
func (d *Door) leaveClosed(e *fsm.Event) {<!-- -->
fmt.Printf("leaveClosed, current state:%s, Dst:%s \
", d.FSM.Current(), e.Dst)
}
 
func (d *Door) leaveState(e *fsm.Event) {<!-- -->
fmt.Printf("leaveState, current state:%s, Dst:%s \
", d.FSM.Current(), e.Dst)
}
 
 
func (d *Door) enterOpen(e *fsm.Event) {<!-- -->
fmt.Printf("enterOpen, current state:%s, Dst:%s \
", d.FSM.Current(), e.Dst)
}
 
 
func (d *Door) enterState(e *fsm.Event) {<!-- -->
fmt.Printf("enterState, current state:%s, Dst:%s \
", d.FSM.Current(), e.Dst)
}
 
 
func (d *Door) afterOpen(e *fsm.Event) {<!-- -->
fmt.Printf("afterOpen, current state:%s, Dst:%s \
", d.FSM.Current(), e.Dst)
}
 
func (d *Door) afterEvent(e *fsm.Event) {<!-- -->
fmt.Printf("afterEvent, current state:%s, Dst:%s \
", d.FSM.Current(), e.Dst)
}
 
 
 
func main() {<!-- -->
door := NewDoor("Test")
 
fmt.Printf("fsm current state: %s \
", door.FSM.Current())
 
err := door.FSM.Event(context.Background(), "open")
if err != nil {<!-- -->
fmt.Println(err)
}
fmt.Printf("fsm current state: %s \
", door.FSM.Current())
 
err = door.FSM.Event(context.Background(), "close")
if err != nil {<!-- -->
fmt.Println(err)
}
fmt.Printf("fsm current state: %s \
", door.FSM.Current())
}

Execution results: Everyone focuses on when the current state changes.

fsm current state: closed
beforeOpen, current state:closed, Dst:open
beforeEvent, current state:closed, Dst:open
leaveClosed, current state:closed, Dst:open
leaveState, current state:closed, Dst:open
// Note: The status has changed at this time, from closed to open.
enterOpen, current state:open, Dst:open
enterState, current state:open, Dst:open
afterOpen, current state:open, Dst:open
afterEvent, current state:open, Dst:open
fsm current state: open
beforeEvent, current state:open, Dst:closed
leaveState, current state:open, Dst:closed
// Note: The status has changed at this time, from open to closed.
enterState, current state:closed, Dst:closed
afterEvent, current state:closed, Dst:closed
fsm current state: closed