GithubHelp home page GithubHelp logo

qmuntal / stateless Goto Github PK

View Code? Open in Web Editor NEW
919.0 11.0 47.0 431 KB

Go library for creating finite state machines

License: BSD 2-Clause "Simplified" License

Go 100.00%
go golang state-machine dot-graph state-diagram fsm statechart

stateless's Introduction

Stateless logo. Fire gopher designed by https://www.deviantart.com/quasilyte

go.dev Build Status Code Coverage Go Report Card Licenses Mentioned in Awesome Go

Stateless

Create state machines and lightweight state machine-based workflows directly in Go code:

phoneCall := stateless.NewStateMachine(stateOffHook)

phoneCall.Configure(stateOffHook).Permit(triggerCallDialed, stateRinging)

phoneCall.Configure(stateRinging).
  OnEntryFrom(triggerCallDialed, func(_ context.Context, args ...any) error {
    onDialed(args[0].(string))
    return nil
  }).
  Permit(triggerCallConnected, stateConnected)

phoneCall.Configure(stateConnected).
  OnEntry(func(_ context.Context, _ ...any) error {
    startCallTimer()
    return nil
  }).
  OnExit(func(_ context.Context, _ ...any) error {
    stopCallTimer()
    return nil
  }).
  Permit(triggerLeftMessage, stateOffHook).
  Permit(triggerPlacedOnHold, stateOnHold)

// ...

phoneCall.Fire(triggerCallDialed, "qmuntal")

This project, as well as the example above, is almost a direct, yet idiomatic, port of dotnet-state-machine/stateless, which is written in C#.

The state machine implemented in this library is based on the theory of UML statechart. The concepts behind it are about organizing the way a device, computer program, or other (often technical) process works such that an entity or each of its sub-entities is always in exactly one of a number of possible states and where there are well-defined conditional transitions between these states.

Features

Most standard state machine constructs are supported:

  • Support for states and triggers of any comparable type (int, strings, boolean, structs, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Thread-safe
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(stateOnHold).
  SubstateOf(stateConnected).
  Permit(triggerTakenOffHold, stateConnected).
  Permit(triggerPhoneHurledAgainstWall, statePhoneDestroyed)

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

Initial state transitions

A substate can be marked as initial state. When the state machine enters the super state it will also automatically enter the substate. This can be configured like this:

sm.Configure(State.B)
  .InitialTransition(State.C);

sm.Configure(State.C)
  .SubstateOf(State.B);

External State Storage

Stateless is designed to be embedded in various application models. For example, some ORMs place requirements upon where mapped data may be stored, and UI frameworks often require state to be stored in special "bindable" properties. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

machine := stateless.NewStateMachineWithExternalStorage(func(_ context.Context) (stateless.State, error) {
  return myState.Value, nil
}, func(_ context.Context, state stateless.State) error {
  myState.Value  = state
  return nil
}, stateless.FiringQueued)

In this example the state machine will use the myState object for state storage.

Activation / Deactivation

It might be necessary to perform some code before storing the object state, and likewise when restoring the object state. Use Deactivate and Activate for this. Activation should only be called once before normal operation starts, and once before state storage.

Introspection

The state machine can provide a list of the triggers that can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(stateOffHook).
  Permit(triggerCallDialled, stateRinging, func(_ context.Context, _ ...any) bool {
    return IsValidNumber()
  }).
  Permit(triggerCallDialled, stateBeeping, func(_ context.Context, _ ...any) bool {
    return !IsValidNumber()
  })

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time). Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

The guard clauses will be evaluated whenever a trigger is fired. Guards should therefor be made side effect free.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

stateMachine.SetTriggerParameters(triggerCallDialed, reflect.TypeOf(""))

stateMachine.Configure(stateRinging).
  OnEntryFrom(triggerCallDialed, func(_ context.Context, args ...any) error {
    fmt.Println(args[0].(string))
    return nil
  })

stateMachine.Fire(triggerCallDialed, "qmuntal")

It is runtime safe to cast parameters to the ones specified in SetTriggerParameters. If the parameters passed in Fire do not match the ones specified it will panic.

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause a panic to be thrown.

To ignore triggers within certain states, use the Ignore(Trigger) directive:

phoneCall.Configure(stateConnected).
  Ignore(triggerCallDialled)

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(stateAssigned).
  PermitReentry(triggerAssigned).
  OnEntry(func(_ context.Context, _ ...any) error {
    startCallTimer()
    return nil
  })

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing a panic when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger( func (_ context.Context, state State, _ Trigger, _ []string) {})

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

sm := stateMachine.Configure(stateOffHook).
  Permit(triggerCallDialed, stateRinging, isValidNumber)
graph := sm.ToGraph()

The StateMachine.ToGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
  OffHook -> Ringing [label="CallDialled [isValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

This is the complete Phone Call graph as builded in example_test.go.

Phone Call graph

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

stateless's People

Contributors

cezarsa avatar gerardvivancos avatar okhowang avatar qmuntal avatar rickardgranberg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stateless's Issues

OnExit got no arguments, but OnEntry can get arguments

reproducible example

package main

import (
	"context"
	"fmt"
	"github.com/qmuntal/stateless"
)

func main() {
	sm := stateless.NewStateMachine("a")
	sm.Configure("a").OnExit(func(ctx context.Context, i ...interface{}) error {
		fmt.Printf("exit A:%+v\n", i)
		return nil
	}).Permit("toB", "b")
	sm.Configure("b").OnEntry(func(ctx context.Context, i ...interface{}) error {
		fmt.Printf("enter B:%+v\n", i)
		return nil
	})
	sm.Fire("toB", 1)
}

expected output

exit A:[1]
enter B:[1]

actual output

exit A:[]
enter B:[1]

Passing arguments in state accessor and mutator

Hi @qmuntal,

We're implementing a state machine that uses Postgres as a backend for external storage. As a solution for the concurrent writes issue, we needed to use row-level locks in Postgres. For this to work, we have to make use of the same transaction object on both state accessor and mutator.

We currently have no way to explicitly pass this via state accessor and mutator so we're injecting the transaction object in the context.

// inject tx to context
tx, _ := db.BeginTx()
ctx := context.WithValue(ctx, "ctxKey", tx)

// access tx from context
func stateAccessor(ctx context.Context) (interface{}, error) {
    tx, ok := ctx.Value("ctxKey").(Tx)
    // read the current state using the tx object
}

// access the same  tx from context
func stateMutator(ctx context.Context, nextState interface{}) error {
    tx, ok := ctx.Value("ctxKey").(Tx)
    // write the next state and commit using the tx object
}

The above solution can work but is not ideal since we're injecting a complex object into the context.

What I propose is to extend the signature of state accessor and mutator to also accept the variadic arguments from the StateMachine.FireCtx method.

func stateMutator(ctx context.context, args ...interface{}) (interface{}, error) { }

func stateAccessor(ctx context.Context, nextState interface{}, args ...interface{}) error { }

With this change, the expected behavior is, when I call StateMachine.FireCtx with argument tx, I should be able to access that argument from both state accessor and mutator.

// Fire an event
stateMachine.Fire(ctx, someTrigger, tx)

// access tx from from args
func stateAccessor(ctx context.Context, args ...interface{}) (interface{}, error) {
    tx, ok := args[0].(Tx)
    // read the current state
}

// access the same  tx from args
func stateMutator(ctx context.Context, nextState interface{}, args ...interface{}) error {
    tx, ok := args[0].(Tx)
    // write the next state and commit using the tx object
}

I'm not sure how this would affect the existing API, but I'm willing to help with the change if needed. Please let me know if you need more clarification regarding this.

Thanks a lot in advance!

stateless.GetTransition can panic

stateless/config.go

Lines 14 to 18 in d977316

// GetTransition returns the transition from the context.
// If there is no transition the returned value is empty.
func GetTransition(ctx context.Context) Transition {
return ctx.Value(transitionKey{}).(Transition)
}

If GetTransition is passed a context without transition value, ctx.Value() returns nil, and the type assertion fail, resulting in a panic. It does not match the documentation of the function.

panic: interface conversion: interface {} is nil, not stateless.Transition [recovered]
	panic: interface conversion: interface {} is nil, not stateless.Transition

goroutine 594 [running]:
testing.tRunner.func1.2({0xa59e00, 0xc000d9a9c0})
	/usr/local/go/src/testing/testing.go:1209 +0x24e
testing.tRunner.func1()
	/usr/local/go/src/testing/testing.go:1212 +0x218
panic({0xa59e00, 0xc000d9a9c0})
	/usr/local/go/src/runtime/panic.go:1038 +0x215
github.com/qmuntal/stateless.GetTransition(...)
	/root/go/pkg/mod/github.com/qmuntal/[email protected]/config.go:17

Optimizing findHandler

Hi,

We are extensively using this FSM library in our codebase and thanks for the same.

While profiling the code, we found that the findHandler function allocates lot of objects on heap and which later get recycled by garbage collector, however that takes CPU cycles and affects the overall performance.

(pprof) list findHandler
Total: 345800393
ROUTINE ======================== github.com/qmuntal/stateless.(*stateRepresentation).findHandler in mobility/vendor/github.com/qmuntal/stateless/states.go
  16884695   16884695 (flat, cum)  4.88% of Total
         .          .     76:		possibleBehaviours []triggerBehaviour
         .          .     77:	)
         .          .     78:	if possibleBehaviours, ok = sr.TriggerBehaviours[trigger]; !ok {
         .          .     79:		return
         .          .     80:	}
   5598416    5598416     81:	allResults := make([]triggerBehaviourResult, 0, len(possibleBehaviours))
         .          .     82:	for _, behaviour := range possibleBehaviours {
         .          .     83:		allResults = append(allResults, triggerBehaviourResult{
         .          .     84:			Handler:              behaviour,
         .          .     85:			UnmetGuardConditions: behaviour.UnmetGuardConditions(ctx, args...),
         .          .     86:		})
         .          .     87:	}
   5624428    5624428     88:	metResults := make([]triggerBehaviourResult, 0, len(allResults))
   5661851    5661851     89:	unmetResults := make([]triggerBehaviourResult, 0, len(allResults))
         .          .     90:	for _, result := range allResults {
         .          .     91:		if len(result.UnmetGuardConditions) == 0 {
         .          .     92:			metResults = append(metResults, result)
         .          .     93:		} else {
         .          .     94:			unmetResults = append(unmetResults, result)

I think for each trigger, all the possible behaviours are known during configuration (i.e. call to Configure/PermitDynamic/....). So can we not populate what is required once and use it every time during findHandler.

The other two slices (metResults, unmetResults) can also be pre-allocated/pre-populated during configuration.

Let us know as it will save a lot of memory allocations and thereby improve performance.

Thanks.

ToGraph work incorrectly with embed substate

exmaple code

package main

import (
	"fmt"

	"github.com/qmuntal/stateless"
)

func main() {
	sm := stateless.NewStateMachine("a")
	sm.Configure("a").Permit("toB", "b")
	sm.Configure("b").InitialTransition("b-1")
	sm.Configure("b-1").SubstateOf("b").InitialTransition("b-1-1").Permit("toB2", "b-2")
	sm.Configure("b-2").SubstateOf("b")
	sm.Configure("b-1-1").SubstateOf("b-1")
	fmt.Println(sm.ToGraph())
}

which output

digraph {
	compound=true;
	node [shape=Mrecord];
	rankdir="LR";

	b-2 [label="b-2"];
	b-1-1 [label="b-1-1"];
	a [label="a"];

subgraph cluster_b {
	label="b";
	b-1 [label="b-1"];
	b-2 [label="b-2"];
}
	b-1 [label="b-1"];

a -> b [style="solid", label="toB"];
b-1 -> b-2 [style="solid", label="toB2"];
 init [label="", shape=point];
 init -> {a}[style = "solid"]
}

Export to DOT graph doesn't produce correct string representation of the state machine

Unfortunately, ToGraph doesn't generate correct output for phoneCall state machine from statemachine_test.go example.

Online visualizer(http://www.webgraphviz.com/) reports the following error

Warning: :7: string ran past end of line Error: :8: syntax error near line 8 context: exit >>> / <<< func2"; Warning: :8: string ran past end of line Warning: OnHold -> OnHold: head is inside tail cluster cluster_Connected Warning: OnHold -> OnHold: tail is inside head cluster cluster_Connected

image

digraph {
	compound=true;
	node [shape=Mrecord];
	rankdir="LR";

	subgraph cluster_Connected {
		label="Connected\n----------\nentry / startCallTimer
exit / func2";
		OnHold [label="OnHold"];
	}
	OffHook [label="OffHook"];
	Ringing [label="Ringing"];
	OnHold -> OffHook [label="LeftMessage", ltail="cluster_Connected"];
	Connected -> Connected [label="MuteMicrophone"];
	OnHold -> OnHold [label="PlacedOnHold", ltail="cluster_Connected"];
	Connected -> Connected [label="SetVolume"];
	Connected -> Connected [label="UnmuteMicrophone"];
	OffHook -> Ringing [label="CallDialed / func1"];
	OnHold -> PhoneDestroyed [label="PhoneHurledAgainstWall"];
	OnHold -> OnHold [label="TakenOffHold", lhead="cluster_Connected"];
	Ringing -> OnHold [label="CallConnected", lhead="cluster_Connected"];
	init [label="", shape=point];
	init -> OffHook
}

Can you, please, take a look?

Cheers,
Taras.

Allow adding actions for leaving a state with a specific trigger

Currently it is possible to configure states to perform an action when entering them, as well as setting a specific action when entering them from a specific transition (config.go:145):

func (sc *StateConfiguration) OnEntryFrom(trigger Trigger, action ActionFunc) *StateConfiguration {
	sc.sr.EntryActions = append(sc.sr.EntryActions, actionBehaviour{
		Action:      action,
		Description: newinvocationInfo(action),
		Trigger:     &trigger,
	})
	return sc
}

However this is not possible when leaving states with a specific transition. I propose to amend this feature.

Please add the ability to reuse a state machine

I would like to use a state machine for a complicated logic flow for every request of a server - mainly to self-document (the graphviz stuff) and it would be really nice to make a new state machine from an existing archetype of sorts. Merely cloning one in its initial state would be great!

ToGraph() is completely broken and returns random or malformed DOT file

From run to run I am getting random results:
Run without debugger:

digraph {
        compound=true;
        node [shape=Mrecord];
        rankdir="LR";

        Connected [label="Connected"];
        subgraph cluster_Connected {
                label="Substates of\nConnected";
                style="dashed";
                Paused [label="Paused"];
        }
        Connecting [label="Connecting"];
        Disconnected [label="Disconnected"];
        Reconnecting [label="Reconnecting"];
        Connected -> Disconnected [label="connection error"];
        Reconnecting -> Connected [label="vpn connected"];ror"]; // SYNTAX ERROR HERE
        init [label="", shape=point];
        init -> Disconnected
}

Run step by step with debugger seems to return right result:

digraph {
        compound=true;
        node [shape=Mrecord];
        rankdir="LR";

        Connected [label="Connected"];
        subgraph cluster_Connected {
                label="Substates of\nConnected";
                style="dashed";
                Paused [label="Paused"];
        }
        Connecting [label="Connecting"];
        Disconnected [label="Disconnected"];
        Reconnecting [label="Reconnecting"];
        Connected -> Disconnected [label="connection error"];
        Connected -> Disconnected [label="disconnect"];
        Connected -> Paused [label="pause"];
        Connected -> Reconnecting [label="reconnect"];
        Connecting -> Disconnected [label="connection error"];
        Connecting -> Connected [label="vpn connected"];
        Disconnected -> Connecting [label="connect"];
        Paused -> Reconnecting [label="resume"];
        Reconnecting -> Disconnected [label="connection error"];
        Reconnecting -> Connected [label="vpn connected"];
        init [label="", shape=point];
        init -> Disconnected
}

Run with debugger without breakpoints (a lot of transitions just went missing):

digraph {
        compound=true;
        node [shape=Mrecord];
        rankdir="LR";

        Connected [label="Connected"];
        subgraph cluster_Connected {
                label="Substates of\nConnected";
                style="dashed";
                Paused [label="Paused"];
        }
        Connecting [label="Connecting"];
        Disconnected [label="Disconnected"];
        Reconnecting [label="Reconnecting"];
        Reconnecting -> Disconnected [label="connection error"];
        Reconnecting -> Connected [label="vpn connected"];
        init [label="", shape=point];
        init -> Disconnected
}

Test code:

Details

package main

import (
	"context"
	"fmt"

	"github.com/qmuntal/stateless"
)

var (
	stateDisconnected = "Disconnected"
	stateConnecting   = "Connecting"
	stateConnected    = "Connected"
	stateReconnecting = "Reconnecting"
	statePaused       = "Paused"
)

var (
	trigConnect         = "connect"
	trigConnected       = "vpn connected"
	trigConnectionError = "connection error"
	trigDisconnect      = "disconnect"
	trigReconnect       = "reconnect"
	trigPause           = "pause"
	trigResume          = "resume"
)

func main() {
	sm := stateless.NewStateMachine(stateDisconnected)

	sm.Configure(stateDisconnected).Permit(trigConnect, stateConnecting)

	sm.Configure(stateConnecting).Permit(trigConnected, stateConnected)
	sm.Configure(stateConnecting).Permit(trigConnectionError, stateDisconnected)

	sm.Configure(stateConnected).Permit(trigDisconnect, stateDisconnected)
	sm.Configure(stateConnected).Permit(trigReconnect, stateReconnecting)
	sm.Configure(stateConnected).Permit(trigConnectionError, stateDisconnected)

	sm.Configure(stateReconnecting).Permit(trigConnected, stateConnected)
	sm.Configure(stateReconnecting).Permit(trigConnectionError, stateDisconnected)

	sm.Configure(stateConnected).Permit(trigPause, statePaused)
	sm.Configure(statePaused).
		SubstateOf(stateConnected).
		Permit(trigResume, stateReconnecting)

	if err := sm.Fire(trigConnect); err != nil {
		panic(err)
	}

	fmt.Println(sm.State(context.Background()))
	fmt.Println(sm.ToGraph())
}

How to get the super state of hierarchical state?

I have states called in_review,verifying and waiting_for_decisions that are all a substate of processing. I want the user to only know that it's in processing if it's in any of these substates. I know that I can do IsInState and verify if it's processing. But is there a way to directly get the super state for all of the substates?

Q: how could I use with `stateless` in my case?

As the title describes, I'm asking for advice that can help me. My state machine looks like this:
image

With my thought of stateless, the state could not contain any variable? just like this:

type state struct {
  s OnOrOffState // indicates which state is activate 
  seq int               // to mark the order of event
}

I must explain why the state machine looks maybe weird:

  1. The event(trigger) could not be ordered all the time, but I need to make sure the final state is correct.
  2. The seq is the key to help me keep the state correct. Maybe there is another way to design the state machine, but for now, I need this seq variable. A new state machine design could be grateful.
  3. Drop event means the action to change state, but I didn't find a clue to define this in stateless
  4. Trigger could be defined as the actual way to change the state, however stateless is not designed like this, right?

Finally, I wonder could stateless help me in this case? Is there any advice on my state machine? it must be better that this issue could help stateless to add new features.

What are internal transitions?

In the code there are mentions of "internal" transitions as well as of an "initial" transition which supposedly adds an internal transition to the machine. What is the purpose of this and how and when to use them? The README just tells how to define them in code without any examples on how to actually use them.

Panic on 32bit ARM processor

The following Unaligned panic occurs on a 32 bit ARM processor

runtime/internal/atomic.panicUnaligned()
	/usr/lib/go/src/runtime/internal/atomic/unaligned.go:8 +0x24
runtime/internal/atomic.Load64(0xf482b4)
	/usr/lib/go/src/runtime/internal/atomic/atomic_arm.s:286 +0x14
github.com/qmuntal/stateless.(*StateMachine).Firing(...)
	/home/andy/Documents/werk/software/products/xte/xte_dev/vendor/github.com/qmuntal/stateless/statemachine.go:268
	

A simple workaround is to re-arrange the StateMachine struct to place the 'ops' field first where it has the correct byte alignment.

type StateMachine struct {
	ops                    uint64
	stateConfig            map[State]*stateRepresentation
	triggerConfig          map[Trigger]triggerWithParameters
	stateAccessor          func(context.Context) (State, error)
	stateMutator           func(context.Context, State) error
	unhandledTriggerAction UnhandledTriggerActionFunc
	onTransitioningEvents  onTransitionEvents
	onTransitionedEvents   onTransitionEvents
	eventQueue             *list.List
	firingMode             FiringMode
	firingMutex            sync.Mutex
}

SetTriggerParameters enhancment: validate interfaces

Currently SetTriggerParameters only matches types exactly. It would be great if instead of requiring the exact type, you could pass SetTriggerParameters and validateParameters used reflect.Value#Implements to validate the parameter.

Does FiringQueued statemachine run to complete if any error occurs?

in internalFireQueued, trigger events will be added to queue and execute one by one.

If sm.internalFireOne() got error, and no trigger event come again, the event queue will not be exhausted for ever.

Do that work as design?

How to trigger the queued events in this case?

Panic when firing trigger

We're seeing an intermittent problem with a panic when a trigger is fired from a callback (in a separate goroutine)
The call stack is as follows:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6c78a1]

goroutine 711 [running]:
container/list.(*List).insert(...)
        /usr/local/go/src/container/list/list.go:96
container/list.(*List).insertValue(...)
        /usr/local/go/src/container/list/list.go:104
container/list.(*List).PushBack(...)
        /usr/local/go/src/container/list/list.go:155
github.com/qmuntal/stateless.(*StateMachine).internalFireQueued(0xc00004c4e0, 0xebfb40, 0xc00019e010, 0xc589e0, 0xe9cd30, 0x0, 0x0, 0x0, 0x0, 0x0)
        /go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:296 +0x4c1
github.com/qmuntal/stateless.(*StateMachine).internalFire(0xc00004c4e0, 0xebfb40, 0xc00019e010, 0xc589e0, 0xe9cd30, 0x0, 0x0, 0x0, 0x0, 0x0)
        /go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:289 +0x89
github.com/qmuntal/stateless.(*StateMachine).FireCtx(...)
        /go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:227
github.com/qmuntal/stateless.(*StateMachine).Fire(...)
        /go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:220

... our code omitted...

We've been seeing this on and off when we run our unit tests so I decided to investigate. I believe theres a concurrency issue in the internalFireQueued function.

Here's my analysis:
Our code is running some code in the OnEntry func as it has just transitioned to a new state. The callback mentioned above fires a new trigger, which is adding the next trigger to the sm.eventQueue in

sm.eventQueue.PushBack(queuedTrigger{Trigger: trigger, Args: args})

At the same time as the new trigger is being added, the OnEntry func returns, and the internalFireQueued resumes execution on

if err != nil {

The problematic lines of code at this point are:

for sm.eventQueue.Len() != 0 {

e := sm.eventQueue.Front()

sm.eventQueue.Remove(e)

All of these is accessing the sm.eventQueue which is being mutated by the callback on line

sm.eventQueue.PushBack(queuedTrigger{Trigger: trigger, Args: args})

Any access to the sm.eventQueue must be done within the scope of sm.firingMutex to ensure "thread" safety.

How to stop FSM execution

I'm creating workflow execution using the stateless FSM behind the scene. In the execution I need to keep track of number of transitions, and halt the execution if that count is ever exceeded, in order to prevent runaway situations.

I have a struct to keep track of number of transitions happened in the FSM:

type ResetOperation struct {
	fsm                     *stateless.StateMachine
	transitionCount int32
}

To increment transitionCount I'm using OnTransitioning() handler. However, there seems to be no way to return error from the handler function, and calling fsm.Deactive() will not stop the execution.

I could probably fire trigger "maxTransitionsReached" and have an error state in the FSM, but if I understand correctly, firing the trigger will cause it to be put on queue fireModeQueued, and queue might not be empty.

What is proper way to handle this scenario?

State machine will not change if OnEntry() took long time to execute

I have a state "A" which has OnEntry() function, in this function, it will sleep 5s. During the sleeping time, another goroutine tries to fire another trigger to change the state, but the state will not change and no error happened. Is this normal? Or can I do long-time operations in the OnEntry() function

How to handle State timers or counters?

I am trying to figure out how I might implement something along the lines of "stay in state for X duration" or "stay in state for X consecutive events". I do not have a lot of experience with state machines, so I'm wondering if:

  1. What I am trying to do is no longer a finite state machine (so this library is probably not what I want to use)
  2. It is a FSM, but this library isn't good for this particular task
  3. This library is good, you might do it like โ€ฆ?

Simplified Example:

Events: eGood and eBad. A scheduler is running, sending either a good/bad event at mostly regular intervals. These events are the input to the state machine.

States: sGood, sBad, sPending.

My confusion is around the sPending state. The idea of the pending state is that it is a hold for a time duration where consecutive sBad events have been received, before going into the sBad state and performing some action.

sPending Transitions:

  • Transition into the pending state sPending when eBad event is received and the current state is sGood.
  • (When in sPending, eGood event would set the state to sGood - basically a reset)

What I am not clear on how a transition from sPending to sBad could be done after either:

  • A certain number of consecutive eBad events have been received
  • A certain amount of time has passed

Things Considered:

A) If using the count method, I could create Pending1, Pending2, Pending3, etc state constants, but this feels cumbersome and wouldn't work as well with time durations I don't think.

B) I can imagine that on receiving a eBad while in the pending state I could start a timer, or create a counter. However, my concern is that I am creating a piece (the timer) of information that is detached from State and StateMachine objects, but does impact the transition behavior of the state machine - and this will get me into trouble.

A better way to pass transition arguments

Currently it is possible to pass arguments using variable number of arguments since Fire() function is variadic.
This works, however this sort of defeats the purpose of static type checking that Go provides while also bloating user code with type checks.

While I have no definite solution in mind, I would like to discuss a better solution for this.
Namely, what if we introduced an argument wrapper structure that handled most of typical use cases that users could need.

In my mind I have two specific use-cases:

  1. User always passes a single argument, typically a pointer to a complex struct (i.e. chat bot message struct)
  2. User passes multiple primitive arguments akin to program start arguments (strings, integers, floats)

We could introduce a stateless.TransitionArguments struct which would be a wrapper for the underlying []interface{}.
Then we could tackle both use cases using such wrapper.

First use case

For the first use case we could introduce First() and Empty() methods:

// Check to see if there are any arguments at all
func (ta *stateless.TransitionArguments) Empty() bool
// Return the first argument from the list
func (ta *stateless.TransitionArguments) First() interface{}

Then we could use it like so:

fsm.Configure("state_a").Permit("step", "state_b").
    OnEntry(func(ctx context.Context, args stateless.TransitionArguments) error {
        var arg = args.First() // returns interface{}
        if arg == nil {
            return errors.New("argument must not be nil")
        }

        var message = arg.(*Message) // Which you could later cast (risking segfault if not careful)

	fmt.Printf("Recieved: %s", message)

        return nil
    })

We could further simply it like this:

fsm.Configure("state_a").Permit("step", "state_b").
    OnEntry(func(ctx context.Context, args stateless.TransitionArguments) error {
        if args.Empty() {
            return errors.New("argument must not be nil")
        }

        var message = args.First().(*Message)

	fmt.Printf("Recieved: %s", message)

        return nil
    })

I am not sure, but maybe there is a way to use reflection to enforce static contract for a value to be either of specified type or nil? This could help avoid segfault during runtime.

Second use case

For the second use case we could define a fluent API similar to what genv uses.

fsm.Configure("state_a").Permit("step", "state_b").
    OnEntry(func(ctx context.Context, args stateless.TransitionArguments) error {
        if args.Has(0) != true {
            return errors.New("argument 0 is required")
        }

        var text string = args.At(0).String()
        var count int = args.At(1).Default(1).Integer()

	fmt.Printf("Recieved: %s, %d", text, count)

        return nil
    })

Of course, this will be a breaking change, but I am interested in what is your opinion on this.
There still persists a problem that there is no way to enforce contract for Fire() method, but I feel like input validation is much more error-prone and useful in the end.

External state storage breaking change

I've been using version 1.1.6 of stateless and i noticed that my tests fail after upgrading to 1.1.7. It seems that the callback for writing the state value is no longer being executed after OnEntry.

machine := stateless.NewStateMachineWithExternalStorage(func(_ context.Context) (stateless.State, error) {
  return myState.Value, nil
}, func(_ context.Context, state stateless.State) error {
  // this callback is not being executed anymore after onEntry so that i'm able to persist the state to db
  db.Save(myState)
  return nil
}, stateless.FiringQueued)


.Configure(entity.StateCompleted).OnEntry(func(c context.Context, args ...interface{}) error {
  myState.Value = "newValue"
})

Sequence of events with 1.1.6:

  • Fire custom event
  • read state value
  • write state value
  • OnEntry hook executed
  • write state value

Sequence of events with 1.1.7:

  • Fire custom event
  • read state value
  • write state value
  • OnEntry hook executed

I was wondering if this is an expected breaking change.

possible improvement auto configure final state

hello,

thanks for this lib it's really interresting.

This is not a bug but a possible usage improvement.

In the following code the final state is "B". Since it's the final state I omitted to configure it.
I have no problem to run the code but the graph function panics.

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x38 pc=0x4969f1]

goroutine 1 [running]:
github.com/qmuntal/stateless.(*graph).formatAllStateTransitions(0xc000090f07, 0xc00005e2a0, 0xc000094c00, 0xc00001a150, 0x2e)
	/home/pierrot/go/src/github.com/qmuntal/stateless/graph.go:101 +0x611
github.com/qmuntal/stateless.(*graph).FormatStateMachine(0xc000090f07, 0xc00005e2a0, 0xc00005e240, 0x4cfb40)
	/home/pierrot/go/src/github.com/qmuntal/stateless/graph.go:22 +0x49f
github.com/qmuntal/stateless.(*StateMachine).ToGraph(...)
	/home/pierrot/go/src/github.com/qmuntal/stateless/statemachine.go:109
main.showGraph()
	/home/pierrot/dev/test/main.go:106 +0x3e
main.main()
	/home/pierrot/dev/test/main.go:117 +0xe9
exit status 2

If I un-comment this line graph is fine.

//machine.Configure(majorB)

Therefore I guess it's possible to either :

  • auto "configure" final state
  • detect "not configured" state and raise an error
package main

import (
	"context"
	"fmt"

	"github.com/qmuntal/stateless"
)

const (
	majorA  = "A"
	subOn   = "On"
	subOn1  = "On1"
	subOff  = "Off"
	subOff1 = "Off1"
	majorB  = "B"

	trigger     = "trigger"
	triggerOn   = "triggerOn"
	triggerOn1  = "triggerOn1"
	triggerOff  = "triggerOff"
	triggerOff1 = "triggerOff1"
)

var button = false

func trButton(_ context.Context, _ ...interface{}) error {
	fmt.Print("trButton :")
	if button {
		fmt.Println("On")
	} else {
		fmt.Println("Off")
	}
	return nil
}
func trOn(_ context.Context, _ ...interface{}) error {
	fmt.Println("trOn")
	return nil
}
func trOn1(_ context.Context, _ ...interface{}) error {
	fmt.Println("trOn1")
	return nil
}
func trOff(_ context.Context, _ ...interface{}) error {
	fmt.Println("trOff")
	return nil
}
func trOff1(_ context.Context, _ ...interface{}) error {
	fmt.Println("trOff1")
	return nil
}

func guardOff(_ context.Context, _ ...interface{}) bool {
	fmt.Println("   << guardOff:", !button)
	return !button
}

func guardOn(_ context.Context, _ ...interface{}) bool {
	fmt.Println("   << guardOn:", button)
	return button
}

func newMachine() *stateless.StateMachine {
	machine := stateless.NewStateMachine(majorA)

	machine.Configure(majorA).
		Permit(trigger, subOn, guardOn).
		Permit(trigger, subOff, guardOff)

	machine.Configure(subOn).OnEntry(trOn).
		Permit(triggerOn, subOn1)
	machine.Configure(subOn1).OnEntry(trOn1).
		Permit(triggerOn1, majorB)

	machine.Configure(subOff).OnEntry(trOff).
		Permit(triggerOff, subOff1)
	machine.Configure(subOff1).OnEntry(trOff1).
		Permit(triggerOff1, majorB)

	//machine.Configure(majorB)

	return machine
}

func runMachine() {
	fmt.Println("Create Machine")
	machine := newMachine()
	fmt.Println("Execute Machine")
	fmt.Println(machine)
	list, err := machine.PermittedTriggers()
	if err != nil {
		panic("invalid case:" + err.Error())
	}
	for len(list) > 0 {
		machine.Fire(list[0])
		fmt.Println(machine)
		list, err = machine.PermittedTriggers()
		if err != nil {
			panic("invalid case:" + err.Error())
		}
	}
}

func showGraph() {
	machine := newMachine()
	fmt.Println(machine.ToGraph())
}

func main() {
	fmt.Println("\n#### Off ")
	runMachine()

	button = true
	fmt.Println("\n#### On ")
	runMachine()

	showGraph()
}

Note that I tried to reproduce with a simpler example but no panic here

package main

import (
	"fmt"

	"github.com/qmuntal/stateless"
)

const (
	stateA = "A"
	stateB = "B"

	triggerB = "triggerB"
)

func main() {
	machine := stateless.NewStateMachine(stateA)

	machine.Configure(stateA).
		Permit(triggerB, stateB)

	fmt.Println(machine)
	machine.Fire(triggerB)
	fmt.Println(machine)

	// no panic if I comment this line
	// machine.Configure(stateB)

	fmt.Println(machine.ToGraph())
}

Error control flow poorly documented

After Ctrl+F'ing the go reference page for error handling documentation I can't be sure what happens if an Entry handler returns an error... does the program panic? Does it prevent the state transition and return to previous state (behaviour I would infer)? Some documentation on OnEntryFrom and OnEntry, among others would be nice. Below is an example of what I mean by returning error

SM.Configure(state.TestSystems).OnEntryFrom(trigger.TestSystems, 
        func(c context.Context, i ...interface{}) error {
		act := drivers.FromContext(c)
		if act == nil || act[0] == nil {
			return errors.New("got nil actuator slice in state machine") // what happen?
		}
		setZero(act)
		return nil
	})

unaligned 64-bit atomic operation

I'm trying to control the state machine with gin (http server) on a raspberry pi 4, but after firing the first event i get a panic:

2023/01/31 23:18:24 [Recovery] 2023/01/31 - 23:18:24 panic recovered:
POST /api/measure/automatic HTTP/1.1
Host: raspberrypi:8080
Accept: */*
Content-Length: 20
Content-Type: application/json
User-Agent: curl/7.83.1


unaligned 64-bit atomic operation
/usr/local/go/src/runtime/internal/atomic/unaligned.go:8 (0x125f3)
        panicUnaligned: panic("unaligned 64-bit atomic operation")
/usr/local/go/src/runtime/internal/atomic/atomic_arm.s:280 (0x12947)
        Load64: CHECK_ALIGN
/home/kk/go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:279 (0x177bcb)
        (*StateMachine).Firing: return atomic.LoadUint64(&sm.ops) != 0
/home/kk/go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:338 (0x177bf4)
        (*StateMachine).internalFireQueued: if sm.Firing() {
/home/kk/go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:324 (0x177957)
        (*StateMachine).internalFire: return sm.internalFireQueued(ctx, trigger, args...)
/home/kk/go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:251 (0x17aeb7)
        (*StateMachine).FireCtx: return sm.internalFire(ctx, trigger, args...)
/home/kk/go/pkg/mod/github.com/qmuntal/[email protected]/statemachine.go:234 (0x17ae6c)
        (*StateMachine).Fire: return sm.FireCtx(context.Background(), trigger, args...)
/home/kk/repos/custom-app/internal/measure/measure.go:30 (0x17ae74)
        Start: measurement.stateMachine.Fire(triggerStart)
/home/kk/repos/custom-app/internal/restserver/measure.go:46 (0x418d27)
        MeasureEndpointPOST: measure.Start()
/home/kk/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:173 (0x40c3db)
        (*Context).Next: c.handlers[c.index](c)
/home/kk/go/pkg/mod/github.com/gin-gonic/[email protected]/recovery.go:101 (0x40c3b8)
        CustomRecoveryWithWriter.func1: c.Next()
/home/kk/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:173 (0x40b4bb)
        (*Context).Next: c.handlers[c.index](c)
/home/kk/go/pkg/mod/github.com/gin-gonic/[email protected]/logger.go:240 (0x40b498)
        LoggerWithConfig.func1: c.Next()
/home/kk/go/pkg/mod/github.com/gin-gonic/[email protected]/context.go:173 (0x40a433)
        (*Context).Next: c.handlers[c.index](c)
/home/kk/go/pkg/mod/github.com/gin-gonic/[email protected]/gin.go:616 (0x40a128)
        (*Engine).handleHTTPRequest: c.Next()
/home/kk/go/pkg/mod/github.com/gin-gonic/[email protected]/gin.go:572 (0x409e87)
        (*Engine).ServeHTTP: engine.handleHTTPRequest(c)
/usr/local/go/src/net/http/server.go:2947 (0x2c875f)
        serverHandler.ServeHTTP: handler.ServeHTTP(rw, req)
/usr/local/go/src/net/http/server.go:1991 (0x2c4efb)
        (*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req)
/usr/local/go/src/runtime/asm_arm.s:831 (0x8008b)
        goexit: MOVW    R0, R0  // NOP

System:
Linux raspberrypi 5.15.84-v7l+ #1613 SMP Thu Jan 5 12:01:26 GMT 2023 armv7l GNU/Linux

If i change the ops variable frm uint64 to uint32 and all corresponding atomic methods to uint32 too, it works fine without panic:

type StateMachine struct {
	// ops is accessed atomically so we put it at the beginning of the struct to achieve 64 bit alignment
-	ops                    uint64
+	ops                    uint32
	stateConfig            map[State]*stateRepresentation
	triggerConfig          map[Trigger]triggerWithParameters
	stateAccessor          func(context.Context) (State, error)
	stateMutator           func(context.Context, State) error
	unhandledTriggerAction UnhandledTriggerActionFunc
	onTransitioningEvents  []TransitionFunc
	onTransitionedEvents   []TransitionFunc
	eventQueue             list.List
	firingMode             FiringMode
	firingMutex            sync.Mutex
}

Concurrent issue on String() function

When calling String() concurrently, it goes to panic.
Is it possible to use sync.Map to avoid panic?

func (sm *StateMachine) stateRepresentation(state State) (sr *stateRepresentation) {
	var ok bool
	if sr, ok = sm.stateConfig[state]; !ok {
		sr = newstateRepresentation(state)
		sm.stateConfig[state] = sr // concurrent panic point
	}
	return
}

Provide API to return Is state machine has queued event

I write a wrapper for stateless.StateMachine.
for run some function after trigger succeed.

func (sm *State Machine) FireCtx(ctx context.Context, trigger stateless.Trigger, args ...interface{}) error {
	if v, ok := ctx.Value(deferRunnerKey{}).(*deferRunner); !ok || v == nil {
		ctx = context.WithValue(ctx, deferRunnerKey{}, &deferRunner{})
	}
	err := sm.StateMachine.FireCtx(ctx, trigger, args...)
	if err == nil { // one more condition if trigger queued, defer function shouldn't be call
		defers := ctx.Value(deferRunnerKey{}).(*deferRunner) // nolint:errcheck
		for _, task := range defers.tasks {
			task(ctx)
		}
	}
	return err
}

but there is a bug when call Fire in StateMachine

func TestDeferTwice(t *testing.T) {
	counter := 0
	sm := &StateMachine{stateless.NewStateMachine("A")}
	sm.Configure("A").Permit("toB", "B").
		OnExit(func(ctx context.Context, i ...interface{}) error {
			addDeferRunner(ctx, func(ctx context.Context) {
				counter++
			})
			return nil
		})
	sm.Configure("B").Permit("toC", "C").
		OnEntry(func(ctx context.Context, i ...interface{}) error {
			return sm.FireCtx(ctx, "toC")
		}).
		OnExit(func(ctx context.Context, i ...interface{}) error {
			addDeferRunner(ctx, func(ctx context.Context) {
				counter++
			})
			return nil
		})
	assert.Equal(t, 0, counter)
	assert.NoError(t, sm.Fire("toB"))
	assert.Equal(t, "C", sm.MustState())
	assert.Equal(t, 2, counter) // error here 3 vs 2
}

but there is no api to get that is there any event pendding.

Can we provide a Firing() bool for StateMachine to indicate is StateMachine firing now?

Invalid DOT graph generated when methods are used as actions

When methods are used as actions calling .ToGraph() generates an invalid DOT file with incorrect string quoting for the method name.

It's easy to reproduce the issue with the code in https://go.dev/play/p/SVJBbhx760k, the DOT output contains the following line which has too many double quotes:

initial [label="initial|entry / "method-fm""];

Trying to call dot with this output causes the following error:

$ dot -Tsvg ./a.dot
Error: ./a.dot: syntax error in line 6 near '-'

I can try following up with a PR latter but I'm afraid I'm not well-versed in DOT format and all the possible quoting rules.

document idiomatic in-state looping

It's fairly common to want to loop over some code when in a state and exit once exiting.

Say you are controlling a orbital launcher, for example. You'd want to loop over a set of instructions that measure position and acceleration and perform some actuation to control trajectory (state controlled_ascent). When your rocket fuel is empty you'd want to switch to a new piece of code that continuously reads sensor data and detects when below a certain altitude->deploy parachutes (state prepare_chute).

Given this problem, what would be an idiomatic way of implementing this state machine with stateless?

accessing current state inside stateMutator

Halo, thank you very much for creating this amazing project!

So, I've been experimenting on this for this past few days and I wonder if there's a possibility to access the current state inside the stateMutator?

I've tried doing it but I had an issue, coz when stateMutator gets executed (i.e. setting the next state), it acquires the lock from stateMutex, then when I try to get the current state (i.e. StateMachine.State) inside stateMutator, it calls the stateAccessor but the lock is already acquired by stateMutator, hence, deadlock.

This is specially useful coz I'm storing the state from an external storage (e.g. in-memory or db).

No valid leaving transitions are permitted from state ... for trigger ...

Hi,

So far, I followed the instructions on how to use this lib and cant get it running.

this is how i define the states and triggers.

    const (
	    initial         = "initial"
	    processing      = "processing"
	    done            = "done"
    )
    const (
	    TriggerProcessing        = "triggerProcessing"
	    TriggerDone              = "triggerDone"
    )

this is how i configure the state machine...

                        stateMachine := stateless.NewStateMachine(initial)

			stateMachine.Configure(initial).Permit(TriggerProcessing, processing)

			stateMachine.Configure(processing).
				OnEntryFrom(err, func(ctx context.Context, args ...any) error {
					// signal that the processing has started
					println("processing started after error")
					return nil
				}).
				OnEntryFrom(TriggerProcessing, func(ctx context.Context, args ...any) error {
					// signal that the processing has started
					println("processing started on uploading data")
					return nil
				}).
				OnExit(func(ctx context.Context, args ...any) error {
					// signal that the processing has finished
					println("processing finished")
					return nil
				}).
				Permit(TriggerDone, done)


                       stateMachine.Configure(done).
				OnEntry(func(ctx context.Context, args ...any) error {
					// signal done
					println("done")
					return nil
				}).
				OnExit(func(ctx context.Context, args ...any) error {
					// signal that it is leaving the done state
					println("leaving done state")
					return nil
				}).
				Permit(TriggerProcessing, processing)

when i then fire a trigger "TriggerProcessing" on the stateMachine, i get the following error:

unexpected error: stateless: No valid leaving transitions are permitted from state 'StateMachine {{ State = initial, PermittedTriggers = [triggerProcessing] }}' for trigger 'triggerProcessing', consider ignoring the trigger

Right now i really cant tell what goes wrong and i would appreciate your help very much. I simply followed the concept from the example..

I tried out multiple things to get rid of this issue, but it didnt help (Ignore, Guards, etc. )

Allow setting a state machine to some intermediate state

In scenarios where state machine entities are persisted and outlive a host's memory, and then are later retrieved to act upon, it would be helpful to be able to efficiently load the state machine logic for its current state, to know what it can do and fire off tasks and potentially transition to the next state.

So something like func (sm *StateMachine[S, T]) SetState(state S), to immediately put the StateMachine into the state the entity is in, and act on it from there.

Is there appetite for something like this, and are there any concerns or limitations preventing it from being added?

No way to get error returned from PermitDynamic

It appears that currently there seems no way to get the error returned from PermitDynamic.

In my code I return an error using the following code:

fsm.Configure("prompt").
PermitDynamic("step", func(ctx context.Context, args ...interface{}) (stateless.State, error) {
    code := args[0].(string)
    if code != "bar" {
        return "prompt", errors.New("invalid code")
    }

    return "apply", nil
})

in my test I fire a transition:

err := fsm.Fire("step", "foo")
assert.ErrorContains(t, err, "invalid code")

and get:

Error "stateless: Dynamic handler for trigger step in state prompt has failed" does not contain "invalid code"

I suppose this is a bug and when firing a transition, an error should be overwritten?

How to handle triggers that are allowed in all states

Hi, thanks for this great state machine library and I meent a problem when using it.
I have a trigger named "shutdown machine", and it can be fired at any state, which will result in back to the very original state. How to implement this? Adding the trigger to every state?

Model vs Machine

This is a question, not an issue. I am trying to gather the intended method of usage for this package.

I am wondering if there is a way with this package to define the valid triggers/states one time (maybe in func main(). Then afterwards, create a single StateMachine instance per incoming HTTP request using the previously created configuration (triggers/states)?

I am hoping to use this package in a way where I would define the triggers/states on application start up then I would use a new StateMachine instance for each new incoming HTTP request because my app is stateless. So for each incoming request I would do the following:

  1. retrieve current state from an external durable storage
  2. create a new StateMachine with the current state and the pre-defined valid triggers/states
  3. fire triggers according to the particular HTTP endpoint
  4. store end state into external durable storage and return data to HTTP endpoint caller

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.