hsm

package module
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Feb 19, 2025 License: MIT Imports: 14 Imported by: 0

README

go-hsm PkgGoDev

Warning This package is currently in alpha stage. While it has test coverage, the API is subject to breaking changes between minor versions until we reach v1.0.0. Please pin your dependencies to specific versions.

Package go-hsm provides a powerful hierarchical state machine (HSM) implementation for Go. State machines help manage complex application states and transitions in a clear, maintainable way.

Installation

go get github.com/stateforward/go-hsm

Key Features

  • Hierarchical state organization
  • Entry, exit, and activity actions for states
  • Guard conditions and transition effects
  • Event-driven transitions
  • Time-based transitions
  • Concurrent state execution
  • Event queuing with completion event priority
  • Multiple state machine instances with broadcast support
  • Event completion tracking with Done channels
  • Tracing support for state transitions

Core Concepts

A state machine is a computational model that defines how a system behaves and transitions between different states. Here are key concepts:

  • State: A condition or situation of the system at a specific moment. For example, a traffic light can be in states like "red", "yellow", or "green".
  • Event: A trigger that can cause the system to change states. Events can be external (user actions) or internal (timeouts).
  • Transition: A change from one state to another in response to an event.
  • Guard: A condition that must be true for a transition to occur.
  • Action: Code that executes when entering/exiting states or during transitions.
  • Hierarchical States: States that contain other states, allowing for complex behavior modeling with inheritance.
  • Initial State: The starting state when the machine begins execution.
  • Final State: A state indicating the machine has completed its purpose.
Why Use State Machines?

State machines are particularly useful for:

  • Managing complex application flows
  • Handling user interactions
  • Implementing business processes
  • Controlling system behavior
  • Modeling game logic
  • Managing workflow states

Usage Guide

Basic State Machine Structure

All state machines must embed the hsm.HSM struct and can add their own fields:

type MyHSM struct {
    hsm.HSM // Required embedded struct
    counter int
    status  string
}
Creating and Starting a State Machine
// Define your state machine type
type MyHSM struct {
    hsm.HSM
    counter int
}

// Create the state machine model
model := hsm.Define(
    "example",
    hsm.State("foo"),
    hsm.State("bar"),
    hsm.Transition(
        hsm.Trigger("moveToBar"),
        hsm.Source("foo"),
        hsm.Target("bar")
    ),
    hsm.Initial("foo")
)

// Create and start the state machine
sm := hsm.Start(context.Background(), &MyHSM{}, &model)

// Create event with completion channel
done := make(chan struct{})
event := hsm.Event{
    Name: "moveToBar",
    Done: done,
}

// Dispatch event and wait for completion
sm.Dispatch(event)
<-done
State Actions

States can have three types of actions:

type MyHSM struct {
    hsm.HSM
    status string
}

hsm.State("active",
    // Entry action - runs once when state is entered
    hsm.Entry(func(ctx context.Context, hsm *MyHSM, event hsm.Event) {
        log.Println("Entering active state")
    }),

    // Activity action - long-running operation with context
    hsm.Activity(func(ctx context.Context, hsm *MyHSM, event hsm.Event) {
        for {
            select {
            case <-ctx.Done():
                return
            case <-time.After(time.Second):
                log.Println("Activity tick")
            }
        }
    }),

    // Exit action - runs when leaving the state
    hsm.Exit(func(ctx context.Context, hsm *MyHSM, event hsm.Event) {
        log.Println("Exiting active state")
    })
)
Choice States

Choice pseudo-states allow dynamic branching based on conditions:

type MyHSM struct {
    hsm.HSM
    score int
}

hsm.State("processing",
    hsm.Transition(
        hsm.Trigger("decide"),
        hsm.Target(
            hsm.Choice(
                // First matching guard wins
                hsm.Transition(
                    hsm.Target("approved"),
                    hsm.Guard(func(ctx context.Context, hsm *MyHSM, event hsm.Event) bool {
                        return hsm.score > 700
                    }),
                ),
                // Default transition (no guard)
                hsm.Transition(
                    hsm.Target("rejected")
                ),
            ),
        ),
    ),
)
Event Broadcasting

Multiple state machine instances can receive broadcasted events:

type MyHSM struct {
    hsm.HSM
    id string
}

sm1 := hsm.Start(context.Background(), &MyHSM{id: "sm1"}, &model)
sm2 := hsm.Start(context.Background(), &MyHSM{id: "sm2"}, &model)

// Dispatch event to all state machines
hsm.DispatchAll(sm1, hsm.NewEvent("globalEvent"))
Transitions

Transitions define how states change in response to events:

type MyHSM struct {
    hsm.HSM
    data []string
}

hsm.Transition(
    hsm.Trigger("submit"),
    hsm.Source("draft"),
    hsm.Target("review"),
    hsm.Guard(func(ctx context.Context, hsm *MyHSM, event hsm.Event) bool {
        return len(hsm.data) > 0
    }),
    hsm.Effect(func(ctx context.Context, hsm *MyHSM, event hsm.Event) {
        log.Println("Transitioning from draft to review")
    })
)
Hierarchical States

States can be nested to create hierarchical state machines:

type MachineHSM struct {
    hsm.HSM
    status string
}

model := hsm.Model(
    hsm.State("operational",
        hsm.State("idle"),
        hsm.State("running"),
        hsm.Initial("idle"),
        hsm.Transition(
            hsm.Trigger("start"),
            hsm.Source("idle"),
            hsm.Target("running")
        )
    ),
    hsm.State("maintenance"),
    hsm.Initial("operational")
)
Time-Based Transitions

Create transitions that occur after a time delay:

type TimerHSM struct {
    hsm.HSM
    timeout time.Duration
}

hsm.Transition(
    hsm.After(func(ctx context.Context, hsm *TimerHSM) time.Duration {
        return hsm.timeout
    }),
    hsm.Source("active"),
    hsm.Target("timeout")
)
Event Completion Tracking

Track event completion using Done channels:

type ProcessHSM struct {
    hsm.HSM
    result string
}

// Create event with completion channel
done := make(chan struct{})
event := hsm.Event{
    Name: "process",
    Data: payload,
    Done: done,
}

// Dispatch event
sm.Dispatch(event)

// Wait for processing to complete
select {
case <-done:
    log.Println("Event processing completed")
case <-time.After(time.Second):
    log.Println("Timeout waiting for event processing")
}
Tracing Support

Enable tracing for debugging state transitions:

type TracedHSM struct {
    hsm.HSM
    id string
}

// Create tracer
trace := func(ctx context.Context, step string, data ...any) (context.Context, func(...any)) {
    log.Printf("[TRACE] %s: %+v", step, data)
    return ctx, func(...any) {}
}

// Start state machine with tracing
sm := hsm.Start(ctx, &TracedHSM{id: "machine-1"}, &model, hsm.Config{
    Trace: trace,
    Id:    "machine-1",
})
OpenTelemetry Integration

The package's Trace interface can be used to integrate with OpenTelemetry:

type TelemetryHSM struct {
    hsm.HSM
    serviceName string
}

// Example implementation of hsm.Trace interface using OpenTelemetry
func NewOTelTracer(name string) hsm.Trace {
    provider := initTracerProvider(name)
    tracer := provider.Tracer(name)

    return func(ctx context.Context, step string, data ...any) (context.Context, func(...any)) {
        attrs := []attribute.KeyValue{
            attribute.String("step", step),
        }

        ctx, span := tracer.Start(ctx, step, trace.WithAttributes(attrs...))
        return ctx, func(...any) {
            span.End()
        }
    }
}

// Usage with state machine
sm := hsm.Start(ctx, &TelemetryHSM{serviceName: "payment"}, &model, hsm.Config{
    Trace: NewOTelTracer("payment_processor"),
    Id:    "payment-1",
})

Roadmap

Current and planned features:

  • Event-driven transitions
  • Time-based transitions with delays
  • Hierarchical state nesting
  • Entry/exit/activity actions
  • Guard conditions
  • Transition effects
  • Choice pseudo-states
  • Event broadcasting
  • Concurrent activities
  • Scheduled transitions (at specific dates/times)
    hsm.Transition(
        hsm.At(time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)),
        hsm.Source("active"),
        hsm.Target("expired")
    )
    
  • History support (shallow and deep)
    hsm.State("parent",
        hsm.History(), // Shallow history
        hsm.DeepHistory(), // Deep history
        hsm.State("child1"),
        hsm.State("child2")
    )
    

Learn More

For deeper understanding of state machines:

License

MIT - See LICENSE file

Contributing

Contributions are welcome! Please ensure:

  • Tests are included
  • Code is well documented
  • Changes maintain backward compatibility
  • Signature changes follow the new context+event pattern

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	Kinds             = kind.Kinds()
	ErrNilHSM         = errors.New("hsm is nil")
	ErrInvalidState   = errors.New("invalid state")
	ErrMissingHSM     = errors.New("missing hsm in context")
	ErrInvalidPattern = errors.New("invalid pattern")
)
View Source
var InitialEvent = Event{}
View Source
var Keys = struct {
	All key[*sync.Map]
	HSM key[HSM]
}{
	All: key[*sync.Map]{},
	HSM: key[HSM]{},
}

Functions

func Dispatch added in v0.8.0

func Dispatch(ctx context.Context, event Event) <-chan struct{}

Dispatch sends an event to a specific state machine instance. Returns a channel that closes when the event has been fully processed.

Example:

sm := hsm.Start(...)
done := sm.Dispatch(hsm.Event{Name: "start"})
<-done // Wait for event processing to complete

func DispatchAll added in v0.8.0

func DispatchAll(ctx context.Context, event Event) <-chan struct{}

DispatchAll sends an event to all state machine instances in the current context. Returns a channel that closes when all instances have processed the event.

Example:

sm1 := hsm.Start(...)
sm2 := hsm.Start(...)
done := hsm.DispatchAll(context.Background(), hsm.Event{Name: "globalEvent"})
<-done // Wait for all instances to process the event

func DispatchTo added in v0.9.7

func DispatchTo(ctx context.Context, id string, event Event) <-chan struct{}

func IsAncestor added in v0.2.5

func IsAncestor(current, target string) bool

func LCA added in v0.2.5

func LCA(a, b string) string

LCA finds the Lowest Common Ancestor between two qualified state names in a hierarchical state machine. It takes two qualified names 'a' and 'b' as strings and returns their closest common ancestor.

For example: - LCA("/s/s1", "/s/s2") returns "/s" - LCA("/s/s1", "/s/s1/s11") returns "/s/s1" - LCA("/s/s1", "/s/s1") returns "/s/s1"

func Match added in v1.0.1

func Match(state, pattern string) bool

func Start added in v0.9.0

func Start[T Context](ctx context.Context, sm T, model *Model, config ...Config) T

Start creates and starts a new state machine instance with the given model and configuration. The state machine will begin executing from its initial state.

Example:

model := hsm.Define(...)
sm := hsm.Start(context.Background(), &MyHSM{}, &model, hsm.Config{
    Trace: func(ctx context.Context, step string, data ...any) (context.Context, func(...any)) {
        log.Printf("Step: %s, Data: %v", step, data)
        return ctx, func(...any) {}
    },
    Id: "my-hsm-1",
})

func Stop added in v0.9.0

func Stop(ctx context.Context)

Stop gracefully stops a state machine instance. It cancels any running activities and prevents further event processing.

Example:

sm := hsm.Start(...)
// ... use state machine ...
hsm.Stop(sm)

Types

type Config added in v0.9.0

type Config struct {
	// Trace is a function that receives state machine execution events for debugging or monitoring.
	Trace Trace
	// Id is a unique identifier for the state machine instance.
	Id string
	// TerminateTimeout is the timeout for the state activity to terminate.
	TerminateTimeout time.Duration
	// Name is the name of the state machine.
	Name string
}

Config provides configuration options for state machine initialization.

type Context

type Context interface {
	Element

	// State returns the current state's qualified name.
	State() string
	// Dispatch sends an event to the state machine and returns a channel that closes when processing completes.
	Dispatch(ctx context.Context, event Event) <-chan struct{}
	Stop(ctx context.Context) <-chan struct{}
	// contains filtered or unexported methods
}

Context represents an active state machine instance that can process events and track state. It provides methods for event dispatch and state management.

func FromContext added in v0.5.0

func FromContext(ctx context.Context) (Context, bool)

FromContext retrieves a state machine instance from a context. Returns the instance and a boolean indicating whether it was found.

Example:

if sm, ok := hsm.FromContext(ctx); ok {
    log.Printf("Current state: %s", sm.State())
}

type DecodedEvent added in v0.9.99

type DecodedEvent[T any] struct {
	Event
	Data T
}

func DecodeEvent added in v0.9.99

func DecodeEvent[T any](event Event) (DecodedEvent[T], bool)

type Element added in v0.3.0

type Element = elements.NamedElement

Element represents a named element in the state machine hierarchy. It provides basic identification and naming capabilities.

type Event

type Event = elements.Event

Event represents a trigger that can cause state transitions in the state machine. Events can carry data and have completion tracking through the Done channel.

type HSM

type HSM struct {
	Context
}

HSM is the base type that should be embedded in custom state machine types. It provides the core state machine functionality.

Example:

type MyHSM struct {
    hsm.HSM
    counter int
}

func (HSM) Dispatch

func (hsm HSM) Dispatch(ctx context.Context, event Event) <-chan struct{}

func (HSM) State

func (hsm HSM) State() string

func (HSM) Stop added in v0.9.92

func (hsm HSM) Stop(ctx context.Context) <-chan struct{}

type Model

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

Model represents the complete state machine model definition. It contains the root state and maintains a namespace of all elements.

func Define added in v0.2.2

func Define[T interface{ RedefinableElement | string }](nameOrRedefinableElement T, redefinableElements ...RedefinableElement) Model

Define creates a new state machine model with the given name and elements. The first argument can be either a string name or a RedefinableElement. Additional elements are added to the model in the order they are specified.

Example:

model := hsm.Define(
    "traffic_light",
    hsm.State("red"),
    hsm.State("yellow"),
    hsm.State("green"),
    hsm.Initial("red")
)

func (*Model) Activity added in v0.2.2

func (state *Model) Activity() string

func (*Model) Entry added in v0.2.2

func (state *Model) Entry() string

func (*Model) Exit added in v0.2.2

func (state *Model) Exit() string

func (*Model) Namespace added in v0.2.2

func (model *Model) Namespace() map[string]elements.NamedElement

func (*Model) Push added in v0.2.2

func (model *Model) Push(partial RedefinableElement)

type RedefinableElement added in v0.9.6

type RedefinableElement = func(model *Model, stack []elements.NamedElement) elements.NamedElement

RedefinableElement is a function type that modifies a Model by adding or updating elements. It's used to build the state machine structure in a declarative way.

func Activity

func Activity[T Context](fn func(ctx context.Context, hsm T, event Event), maybeName ...string) RedefinableElement

Activity defines a long-running action that is executed while in a state. The activity is started after the entry action and stopped before the exit action.

Example:

hsm.Activity(func(ctx context.Context, hsm *MyHSM, event Event) {
    for {
        select {
        case <-ctx.Done():
            return
        case <-time.After(time.Second):
            log.Println("Activity tick")
        }
    }
})

func After

func After[T Context](expr func(ctx context.Context, hsm T, event Event) time.Duration, maybeName ...string) RedefinableElement

After creates a time-based transition that occurs after a specified duration. The duration can be dynamically computed based on the state machine's context.

Example:

hsm.Transition(
    hsm.After(func(ctx context.Context, hsm *MyHSM, event Event) time.Duration {
        return time.Second * 30
    }),
    hsm.Source("active"),
    hsm.Target("timeout")
)

func Choice

func Choice[T interface{ RedefinableElement | string }](elementOrName T, partialElements ...RedefinableElement) RedefinableElement

Choice creates a pseudo-state that enables dynamic branching based on guard conditions. The first transition with a satisfied guard condition is taken.

Example:

hsm.Choice(
    hsm.Transition(
        hsm.Target("approved"),
        hsm.Guard(func(ctx context.Context, hsm *MyHSM, event Event) bool {
            return hsm.score > 700
        })
    ),
    hsm.Transition(
        hsm.Target("rejected")
    )
)

func Defer

func Defer(events ...uint64) RedefinableElement

func Effect

func Effect[T Context](fn func(ctx context.Context, hsm T, event Event), maybeName ...string) RedefinableElement

Effect defines an action to be executed during a transition. The effect function is called after exiting the source state and before entering the target state.

Example:

hsm.Effect(func(ctx context.Context, hsm *MyHSM, event Event) {
    log.Printf("Transitioning with event: %s", event.Name)
})

func Entry

func Entry[T Context](fn func(ctx context.Context, hsm T, event Event), maybeName ...string) RedefinableElement

Entry defines an action to be executed when entering a state. The entry action is executed before any internal activities are started.

Example:

hsm.Entry(func(ctx context.Context, hsm *MyHSM, event Event) {
    log.Printf("Entering state with event: %s", event.Name)
})

func Exit

func Exit[T Context](fn func(ctx context.Context, hsm T, event Event), maybeName ...string) RedefinableElement

Exit defines an action to be executed when exiting a state. The exit action is executed after any internal activities are stopped.

Example:

hsm.Exit(func(ctx context.Context, hsm *MyHSM, event Event) {
    log.Printf("Exiting state with event: %s", event.Name)
})

func Final

func Final(name string) RedefinableElement

Final creates a final state that represents the completion of a composite state or the entire state machine. When a final state is entered, a completion event is generated.

Example:

hsm.State("process",
    hsm.State("working"),
    hsm.Final("done"),
    hsm.Transition(
        hsm.Source("working"),
        hsm.Target("done")
    )
)

func Guard

func Guard[T Context](fn func(ctx context.Context, hsm T, event Event) bool, maybeName ...string) RedefinableElement

Guard defines a condition that must be true for a transition to be taken. If multiple transitions are possible, the first one with a satisfied guard is chosen.

Example:

hsm.Guard(func(ctx context.Context, hsm *MyHSM, event Event) bool {
    return hsm.counter > 10
})

func Initial

func Initial[T interface{ string | RedefinableElement }](elementOrName T, partialElements ...RedefinableElement) RedefinableElement

Initial defines the initial state for a composite state or the entire state machine. When a composite state is entered, its initial state is automatically entered.

Example:

hsm.State("operational",
    hsm.State("idle"),
    hsm.State("running"),
    hsm.Initial("idle")
)

func Source

func Source[T interface{ RedefinableElement | string }](nameOrPartialElement T) RedefinableElement

Source specifies the source state of a transition. It can be used within a Transition definition.

Example:

hsm.Transition(
    hsm.Source("idle"),
    hsm.Target("running")
)

func State

func State(name string, partialElements ...RedefinableElement) RedefinableElement

State creates a new state element with the given name and optional child elements. States can have entry/exit actions, activities, and transitions.

Example:

hsm.State("active",
    hsm.Entry(func(ctx context.Context, hsm *MyHSM, event Event) {
        log.Println("Entering active state")
    }),
    hsm.Activity(func(ctx context.Context, hsm *MyHSM, event Event) {
        // Long-running activity
    }),
    hsm.Exit(func(ctx context.Context, hsm *MyHSM, event Event) {
        log.Println("Exiting active state")
    })
)

func Target

func Target[T interface{ RedefinableElement | string }](nameOrPartialElement T) RedefinableElement

Target specifies the target state of a transition. It can be used within a Transition definition.

Example:

hsm.Transition(
    hsm.Source("idle"),
    hsm.Target("running")
)

func Transition

func Transition[T interface{ RedefinableElement | string }](nameOrPartialElement T, partialElements ...RedefinableElement) RedefinableElement

Transition creates a new transition between states. Transitions can have triggers, guards, and effects.

Example:

hsm.Transition(
    hsm.Trigger("submit"),
    hsm.Source("draft"),
    hsm.Target("review"),
    hsm.Guard(func(ctx context.Context, hsm *MyHSM, event Event) bool {
        return hsm.IsValid()
    }),
    hsm.Effect(func(ctx context.Context, hsm *MyHSM, event Event) {
        log.Println("Transitioning from draft to review")
    })
)

func Trigger

func Trigger[T interface{ string | *Event | Event }](events ...T) RedefinableElement

Trigger defines the events that can cause a transition. Multiple events can be specified for a single transition.

Example:

hsm.Transition(
    hsm.Trigger("start", "resume"),
    hsm.Source("idle"),
    hsm.Target("running")
)

type Trace added in v0.3.0

type Trace func(ctx context.Context, step string, data ...any) (context.Context, func(...any))

Trace is a function type for tracing state machine execution. It receives the current context, a step description, and optional data. It returns a modified context and a completion function.

Directories

Path Synopsis
pkg
plantuml
TODO: This code is hacked together and needs to be refactored.
TODO: This code is hacked together and needs to be refactored.

Jump to

Keyboard shortcuts

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