fsm

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 1, 2025 License: Apache-2.0 Imports: 7 Imported by: 0

README

go-fsm

Go Reference Go Report Card Coverage License

A thread-safe finite state machine implementation for Go that supports custom states, transitions, and state change notifications.

Features

  • Define custom states and allowed transitions
  • Thread-safe state management using atomic operations
  • Subscribe to state changes via channels with context support
  • Structured logging via log/slog

Installation

go get github.com/robbyt/go-fsm

Quick Start

package main

import (
	"context"
	"log/slog"
	"os"
	"time"

	"github.com/robbyt/go-fsm"
)

func main() {
	// Create a logger
	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

	// Create a new FSM with initial state and predefined transitions
	machine, err := fsm.New(logger.Handler(), fsm.StatusNew, fsm.TypicalTransitions)
	if err != nil {
		logger.Error("failed to create FSM", "error", err)
		return
	}

	// Subscribe to state changes
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	
	stateChan := machine.GetStateChan(ctx)
	
	go func() {
		for state := range stateChan {
			logger.Info("state changed", "state", state)
		}
	}()

	// Perform state transitions- they must follow allowed transitions
	// booting -> running -> stopping -> stopped
	if err := machine.Transition(fsm.StatusBooting); err != nil {
		logger.Error("transition failed", "error", err)
		return
	}

	if err := machine.Transition(fsm.StatusRunning); err != nil {
		logger.Error("transition failed", "error", err)
		return
	}

	time.Sleep(time.Second)
	
	if err := machine.Transition(fsm.StatusStopping); err != nil {
		logger.Error("transition failed", "error", err)
		return
	}
	
	if err := machine.Transition(fsm.StatusStopped); err != nil {
		logger.Error("transition failed", "error", err)
		return
	}
}

Usage

Defining Custom States and Transitions
// Define custom states
const (
	StatusOnline  = "StatusOnline"
	StatusOffline = "StatusOffline"
	StatusUnknown = "StatusUnknown"
)

// Define allowed transitions
var customTransitions = fsm.TransitionsConfig{
	StatusOnline:  []string{StatusOffline, StatusUnknown},
	StatusOffline: []string{StatusOnline, StatusUnknown},
	StatusUnknown: []string{},
}
Creating an FSM
// Create with default options
machine, err := fsm.New(slog.Default().Handler(), StatusOnline, customTransitions)
if err != nil {
	// Handle error
}

// Or with custom logger options
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
	Level: slog.LevelDebug,
})
machine, err := fsm.New(handler, StatusOnline, customTransitions)
State Transitions
// Simple transition
err := machine.Transition(StatusOffline)

// Conditional transition
err := machine.TransitionIfCurrentState(StatusOnline, StatusOffline)

// Get current state
currentState := machine.GetState()
State Change Notifications
// Get notification channel
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

stateChan := machine.GetStateChan(ctx)

// Process state changes
go func() {
	for state := range stateChan {
		// Handle state change
		fmt.Println("State changed to:", state)
	}
}()

// Alternative: Add a direct subscriber channel
stateCh := make(chan string, 1)
unsubscribe := machine.AddSubscriber(stateCh)

// Process state updates in a separate goroutine
go func() {
	for state := range stateCh {
		fmt.Println("State updated:", state)
	}
}()

defer unsubscribe() // Call to stop receiving updates

Complete Example

See example/main.go for a complete example application.

Thread Safety

All operations on the FSM are thread-safe and can be used concurrently from multiple goroutines.

License

Apache License 2.0 - See LICENSE for details.

Documentation

Overview

Package fsm provides a thread-safe finite state machine implementation. It allows defining custom states and transitions, managing state changes, subscribing to state updates via channels, and persisting/restoring state via JSON.

Example usage:

logger := slog.Default()
machine, err := fsm.New(logger.Handler(), fsm.StatusNew, fsm.TypicalTransitions)
if err != nil {
    logger.Error("Failed to create FSM", "error", err)
    return
}

err = machine.Transition(fsm.StatusRunning)
if err != nil {
    logger.Error("Transition failed", "error", err)
}

// Persist state jsonData, err := json.Marshal(machine)

if err != nil {
    logger.Error("Failed to marshal FSM state", "error", err)
}

// Restore state restoredMachine, err := fsm.NewFromJSON(logger.Handler(), jsonData, fsm.TypicalTransitions)

if err != nil {
    logger.Error("Failed to restore FSM from JSON", "error", err)
} else {

    logger.Info("Restored FSM state", "state", restoredMachine.GetState())
}

Index

Constants

View Source
const (
	StatusNew       = "New"
	StatusBooting   = "Booting"
	StatusRunning   = "Running"
	StatusReloading = "Reloading"
	StatusStopping  = "Stopping"
	StatusStopped   = "Stopped"
	StatusError     = "Error"
	StatusUnknown   = "Unknown"
)

Collection of common statuses

Variables

View Source
var ErrAvailableStateData = errors.New("available state data is malformed")

ErrAvailableStateData is returned when the available state data is not correctly formatted

View Source
var ErrCurrentStateIncorrect = errors.New("current state is incorrect")

ErrCurrentStateIncorrect is returned when the current state does not match with expectations

View Source
var ErrInvalidState = errors.New("state is invalid")

ErrInvalidState is returned when the state is somehow invalid

View Source
var ErrInvalidStateTransition = errors.New("state transition is invalid")

ErrInvalidStateTransition is returned when the state transition is invalid

TypicalTransitions is a common set of transitions, useful as a guide. Each key is the current state, and the value is a list of valid next states the FSM can transition to.

Functions

This section is empty.

Types

type Machine

type Machine struct {
	// contains filtered or unexported fields
}

Machine represents a finite state machine that tracks its current state and manages state transitions.

func New

func New(
	handler slog.Handler,
	initialState string,
	allowedTransitions TransitionsConfig,
) (*Machine, error)

New initializes a new finite state machine with the specified initial state and allowed state transitions.

Example of allowedTransitions:

allowedTransitions := TransitionsConfig{
    StatusNew:       {StatusBooting, StatusError},
    StatusBooting:   {StatusRunning, StatusError},
    StatusRunning:   {StatusReloading, StatusExited, StatusError},
    StatusReloading: {StatusRunning, StatusError},
    StatusError:     {StatusNew, StatusExited},
    StatusExited:    {StatusNew},
}

func NewFromJSON added in v1.1.0

func NewFromJSON(
	handler slog.Handler,
	jsonData []byte,
	allowedTransitions TransitionsConfig,
) (*Machine, error)

NewFromJSON creates a new finite state machine by unmarshaling JSON data. It requires the logging handler and the original transitions configuration used when the state was marshaled.

func (*Machine) AddSubscriber

func (fsm *Machine) AddSubscriber(ch chan string) func()

AddSubscriber adds your channel to the internal list of broadcast targets. It will receive the current state if the channel (if possible), and will also receive future state changes when the FSM state is updated. A callback function is returned that should be called to remove the channel from the list of subscribers when this is no longer needed.

func (*Machine) GetState

func (fsm *Machine) GetState() string

GetState returns the current state of the finite state machine. This read is lock-free due to the use of atomic.Value.

func (*Machine) GetStateChan

func (fsm *Machine) GetStateChan(ctx context.Context) <-chan string

GetStateChan returns a channel that will receive the current state of the FSM immediately.

func (*Machine) MarshalJSON added in v1.1.0

func (fsm *Machine) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface. It returns the current state of the FSM as a JSON object: {"state": "CURRENT_STATE"}.

func (*Machine) SetState

func (fsm *Machine) SetState(state string) error

SetState updates the FSM's state to the provided state, bypassing the usual transition rules. It only succeeds if the requested state is defined as a valid *source* state in the allowedTransitions configuration.

func (*Machine) Transition

func (fsm *Machine) Transition(toState string) error

Transition changes the FSM's state to toState. It ensures that the transition adheres to the allowed transitions defined during initialization. Returns ErrInvalidState if the current state is somehow invalid or ErrInvalidStateTransition if the transition is not allowed.

func (*Machine) TransitionBool

func (fsm *Machine) TransitionBool(toState string) bool

TransitionBool is similar to Transition, but returns a boolean indicating whether the transition was successful. It suppresses the specific error reason.

func (*Machine) TransitionIfCurrentState

func (fsm *Machine) TransitionIfCurrentState(fromState, toState string) error

TransitionIfCurrentState changes the FSM's state to toState only if the current state matches fromState. This returns an error if the current state does not match or if the transition is not allowed from fromState to toState.

type TransitionsConfig

type TransitionsConfig map[string][]string

TransitionsConfig represents a configuration for allowed transitions the FSM.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL