Skip to content

lemenendez/fsm

Repository files navigation

Finite State Machine Implementation (FSM)

The Idea

A Finite State Machine has multiple use cases, the state of an entity for example a User. Another example is the state of a Tenant for a Saas company, the state of the executing of a long-running task, or the state of a money transfer.

Definition

  • The system must be describable by a finite set of states.
  • The system must have a finite set of inputs and/or events that can trigger transitions between states.
  • The behavior of the system at a given point in time depends upon the current state and the input or the event that occurs at that time.
  • For each state the system may be in, the behavior is defined for each possible input or event.
  • The system has a particular initial state.

Naming Rules

  1. The State name must be UPPERCASE
  2. The State name must not contain spaces
  3. The State name must not contain numbers
  4. The Transition name must be UPPERCASE
  5. The Transition name must not contain spaces
  6. The Transition name must not contain numbers

Examples

A complete set of examples are under the test folder.

Simple Enable/Disable

The next code declares 2 states: ENABLED and DISABLED, and 2 transitions ENABLE and DISABLE

const ENABLED = "ENABLED"
const DISABLED = "DISABLED"
const ENABLE = "ENABLE"
const DISABLE = "DISABLE"

The next lines declare a finite state machine (fsm), then it adds its states and its transitions.

f := fsm.NewFSM("Basic Disabled/Enabled")
f.AddState(DISABLED)
f.AddState(ENABLED)
f.Init(DISABLED)

f.AddTrans(ENABLED, DISABLED, DISABLE)
f.AddTrans(DISABLED, ENABLED, ENABLE)

Now we declare the function myFunc, that funcion will be executed after fsm does a transition between the states. Note: It is always a good idea to do this:

err := f.AddState(DISABLE)
if er...
myFunc := func(pre string, cur string, action string) {
    t.Logf("Previous State:%v, New State:%v, Action:%v", pre, cur, action)
}

err := f.Exec(ENABLE, ENABLED, myFunc)

if err != nil {
    t.Log(err)
    t.Errorf("should allow transicion")
}
t.Log(f.GetTrans())

SaaS Plan

A test image

A user starts in Trial, Basic or Premium. Once in Basic can Upgrade to Premium. Once in Premium can Downgrade to Basic. In Trial can upgrade to Basic or Premium. For this example we're not going to use the Expired state.

Define a Plan struct:

type Plan struct {
    Id    int32
    Name  string
    State *fsm.FSM
}

Define the 'Constructor'

func NewPlan(plan string) (*Plan, error) {

    f := fsm.NewFSM("SAAS Account State V1.0")

    f.AddState("TRIAL")
    f.AddState("BASIC")
    f.AddState("PREMIUM")

    f.AddTrans("TRIAL", "BASIC", "UPGRATE")
    f.AddTrans("TRIAL", "PREMIUM", "UPGRATE")
    f.AddTrans("BASIC", "PREMIUM", "UPGRATE")
    f.AddTrans("PREMIUM", "BASIC", "DOWNGRATE")

    c := &Plan{
        Id:    0,
        Name:  "Standard",
        State: f,
    }
    err := f.Init(plan)
    if err == nil {
        return c, nil
    }
    return nil, err
}

We use the Init function to initialize the fsm to its initial state.

Now we define the Upgrade and Downgrade helper functions.

func (a *Plan) Upgrade(name string) bool {
    err := a.State.Exec("UPGRATE", name, a.stateTransitionHanlder)
    if err != nil {
        a.t.Logf(err.Error())
    }
    return err == nil
}
func (a *Plan) Downgrate(name string) bool {
    err := a.State.Exec("DOWNGRATE", name, a.stateTransitionHanlder)
    if err != nil {
        a.t.Logf(err.Error())
    }
    return err == nil
}

The stateTransitionHandler function will be called after the fsm validate the execution.

func (a *Plan) stateTransitionHandler(pre string, cur string, action string) {
    a.t.Logf("Previous State:%v, New State:%v, Action:%v", pre, cur, action)
}

Now finally we use it. We use NewPlan funcion to create and also to initilize our fsm.Then we call the Upgrade funcion to check if we can or cannot upgrade the plan.

    p, err := NewPlan("TRIAL")
    if p != nil {
        if p.Upgrade("BASIC") {
            t.Logf("New State:%v", a.State.GetState())
        } else {
            t.Errorf("cannot updagrade")
        }
    } else {
        t.Errorf(err.Error())
    }

Docker

Build

docker build -t fsm .

Running

docker run -it --rm -v "$PWD":/go/src fsm bash

Testing

go test -v

Testing with coverage: go test -v -coverprofile=cover.out -coverpkg=.

Testing with tool: go tool cover -html=$PWD/cover.out -o $PWD/cover.html

Lint

`# binary will be $(go env GOPATH)/bin/golangci-lint curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.43.0

golangci-lint --version `

Usage

package main

import (
	"fmt"
	"log"

	fsm "github.com/lemenendez/fsm"
)

func main() {
	f := fsm.NewFSM("My Machine")
	f.AddState("ACTIVE")
	f.AddState("INACTIVE")

	f.AddTrans("INACTIVE", "ACTIVE", "UP")
	f.AddTrans("ACTIVE", "INACTIVE", "DOWN")

	f.Init("INACTIVE")

	fmt.Print(f.GetTrans())

	myFunc := func(pre string, cur string, action string) {
		fmt.Printf("Previous State:%v, New State:%v, Action:%v\n", pre, cur, action)
	}

	err := f.Exec("UP", "ACTIVE", myFunc)

	if err != nil {
		log.Fatal(err)
	}

}

Output:

Transitions My Machine:
UP (INACTIVE) -> (ACTIVE)
DOWN (ACTIVE) -> (INACTIVE)
Previous State:INACTIVE, New State:ACTIVE, Action:UP

Math Definition

A finite automaton M is defined by a 5-tuple (Σ, Q, q 0 , F, δ), where

  • Σ is the set of symbols representing input to M
  • Q is the set of states of M
  • q 0 ∈ Q is the start state of M
  • F ⊆ Q is the set of final states of M
  • δ : Q × Σ → Q is the transition function

About

Finite State Machine Implementation (FSM) in Go

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages