- Introduction
- Installation
- Components of a State Machine
- Initializing the State Machine
- Initialization
- Getting an instance of the state machine
- ProMachine
- Creating Action, Invocation, Guard and Context Handlers
- Examples
StatePro is a Golang library for handling Finite State Machines, designed to optimize state management in microservices. Inspired by XState but focused on backend development, the JSON representation of the State Machine is compatible with XState's visual creator, facilitating its design and visualization.
Despite similarities with XState, StatePro has its own set of features, primarily to ensure efficiency and adaptability in state management in microservices. This is because some XState features, although useful for frontend development, do not always translate effectively to the microservices environment, which can lead to unnecessary complexity.
To install StatePro, use the following command:
go get github.com/rendis/statepro
-
State Machine: Defines the overall behavior of the system and is composed of states, transitions, events, actions, guards, and invocations.
-
State: A state represents a particular stage in the lifecycle of a state machine.
-
Action: Actions are behaviors that are performed when an event or transition occurs. Actions are synchronous and are executed in the order in which they are defined.
-
Guard: Guards are conditions that must be met for a transition to occur.
-
Invocation: Invocations are asynchronous tasks that are performed upon entering a state. Unlike other implementations, in StatePro, the result of an invocation (success or failure) does not affect the behavior of the state machine.
-
Transition: A transition is a change from one state to another in response to an event.
-
Event: Events are inputs that trigger transitions between states.
-
Context: The context is an object associated with the state machine that can be modified within actions and queried by the other components.
To initialize a state machine in StatePro, three components are needed: the JSON definition of the state machine, the context, and the definition registry.
The JSON will contain the definition of the state machine. This can be created in Stately and imported into your project. It is important that each JSON definition you load into your system has a unique ID. This ID will be used later to associate the JSON with the definition registry.
The location of the state machine definition JSONs is configurable. By default, StatePro will look in the statepro.yml
file at the root of the project.
You can change the location and filename of the configuration file using the SetDefinitionPath
method of the statepro
package:
import "github.com/rendis/statepro"
statepro.SetDefinitionPath("path/to/your/prop.yml")
Within the configuration file, you must define the location of the state machine definition JSON files. For example:
statepro:
file-prefix: '<prefix>'
paths:
- '<rute1>'
- '<rute2>'
...
file-prefix
: specifies the prefix that file names must have to be considered as definition files. Only files whose name begins with this prefix will be processed.paths
: is a list of paths that specify the directories and/or files that should be searched to find the state machine definitions.
The context is a struct that will be linked to the behavior of the state machine.
The definition registry is a struct that will contain the action, invocation, guard methods, and context handling methods that will be used in the state machine.
This struct should have the type of the context
as a generic.
This struct should implement the MachineRegistryDefinitions
interface, which is used to get the ID of the state machine.
This ID should be the same as the ID of the definition JSON that you want to associate it with.
For example:
type Context struct {
name string
state ContextState
...
}
type ContextMachineDefinitions[ContextType Context] struct {}
// Implementation of the GetMachineTemplateId method of the MachineRegistryDefinitions interface
func (cmd *ContextMachineDefinitions[ContextType]) GetMachineTemplateId() string {
return "MACHINE_ID"
}
// Implementation of action, invocation, guard methods, and context handling methods
Once you have these three components (JSON definition, context, and definition registry), you can initialize your state machine and start using it to manage the flow of your application.
Before initializing statepro
, you must have registered all the state machine definitions using the AddMachine
method of the statepro
package.
To initialize a state machine, you should use the InitMachines
method of the statepro
package.
var definition1 = ContextMachineDefinitions[Context]{}
var definition2 = ContextMachineDefinitions2[Context2]{}
var definition3 = ContextMachineDefinitions3[Context3]{}
...
// register definitions
var machineId1 = statepro.AddMachine[Context](definition1)
var machineId2 = statepro.AddMachine[Context2](definition2)
var machineId3 = statepro.AddMachine[Context3](definition3)
...
// InitMachines should be called after registering all definitions
statepro.InitMachines()
The return value of AddMachine
is the ID associated with the state machine created from the definition registry ContextMachineDefinitions[Context]
.
This ID will be unique and will be used to get an instance of the state machine.
Note: The ID returned by
AddMachine
is not the same as the ID of the JSON definition. It is a unique ID generated for each state machine.
To get a state machine, you should use the GetMachine
method of the statepro
package, which returns an object of type ProMachine
.
This is an interface that defines the methods that allow you to interact with the state machine.
var context = &Context{}
var contextMachine, err = statepro.GetMachine[Context](machineId1, context)
To obtain a state machine, in addition to its ID (machineId1
), an instance of the context must be provided.
If it is nil
, an instance of the context will be attempted to be obtained through the ContextFromSource
method of the associated definition registry.
If an instance of the context does not exist in the ContextFromSource
method, an error will be returned.
StatePro defines a ProMachine
interface with several methods that allow you to interact with the state machine:
type ProMachine[ContextType any] interface {
PlaceOn(stateName string) error
StartOn(stateName string) TransitionResponse
StartOnWithEvent(stateName string, event Event) TransitionResponse
SendEvent(event Event) TransitionResponse
GetNextEvents() []string
GetState() string
IsFinalState() bool
GetContext() ContextType
CallContextToSource() error
}
-
PlaceOn(stateName string) error
: places the state machine in a specific state, without executing entry actions. -
StartOn(stateName string) TransitionResponse
: places the state machine in a specific state and executes the entry actions. -
StartOnWithEvent(stateName string, event Event) TransitionResponse
: is similar toStartOn
, but also sends an event to the state machine. -
SendEvent(event Event) TransitionResponse
: allows sending an event to the state machine, which can trigger transitions and actions. -
GetNextEvents() []string
: returns a list of the names of events that can be sent from the current state. -
GetState() string
: returns the name of the current state. -
IsFinalState() bool
: checks if the current state is a final state. -
GetContext() ContextType
: allows getting the value of the context associated with the state machine. -
CallContextToSource() error
: allows calling the 'ContextToSource' method, if it exists, in the definition registry of the state machine.
The response structure TransitionResponse
contains information about the transition(s) that occurred.
type TransitionResponse interface {
GetLastEvent() Event
Error() error
}
GetLastEvent() Event
: returns the last event that was sent to the state machine.Error() error
: returns an error if one occurred during the transition.
Regarding the features related to states and transitions, StatePro allows defining specific behaviors that are defined in the design of the state machine:
-
Execute Entry Actions: When entering a state, specific actions can be executed.
-
Execute Exit Actions: When leaving a state, specific actions can be executed.
-
Execute Invocations: When entering a state, asynchronous tasks can be executed. Although invocations are asynchronous, in StatePro, their result (success or failure) does not affect the decision-making of the state machine. For example, when entering a state, you might want to asynchronously send an event to a message queue. If the invocation fails, the state machine will not be affected and will continue with its normal execution.
-
Execute Transition Actions: During a transition, specific actions can be executed.
-
Execute Guards: Before performing a transition, conditions can be evaluated that determine whether the transition should be performed or not.
-
Execute Guard Actions: While evaluating a guard, specific actions can be executed.
To work with StatePro, it is important to understand how to create and use actions, invocations, and guards. These
methods are essential to defining the behavior of the state machine. Before delving into how to define
these methods, we'll explain two essential elements: Event
and ActionTool
.
The Event
object is the basic unit of communication between the states of the machine. An Event
can contain a
Data
value, which can be used to pass information from one state to another. Here's its definition:
type Event interface {
GetName() string
GetFrom() string
HasData() bool
GetData() any
GetDataAsMap() (map[string] any, error)
GetErr() error
GetEvtType() EventType
ToBuilder() EventBuilder
}
To build an event, you use EventBuilder
:
type EventBuilder interface {
WithData(data any) EventBuilder
WithErr(err error) EventBuilder
WithType(eventType EventType) EventBuilder
Build() Event
}
For example, building an event would look like this:
import "github.com/rendis/statepro/piece"
var evt := piece.BuildEvent("EVENT_NAME").Build()
ActionTool
is an object used to interact with the state machine from within an action. It has the following definition:
type ActionTool[ContextType any] interface {
Send(event Event)
Propagate(event Event)
}
-
Send(event Event)
: This method allows sending an event to the state machine. The event will be processed by the current state of the machine. -
Propagate(event Event)
: This method allows propagating an event with new data and errors throughout the operations following the current action. It is useful for transmitting additional information or errors that occur during the execution of an action.
The action, guard, and invoke methods must have the same name as the component they are intended to associate with in the state machine's JSON.
It's important to note that the names in the JSON are case-insensitive. This means that if we have doSomething
, DoSomething
, and dosomething
in the JSON,
for the state machine engine, these three variants will be considered the same. Consequently, their counterpart in the code should be named exactly as DoSomething
.
This naming standard is crucial for maintaining consistency between the state machine's JSON and its implementation in the code.
Theses methods must be defined in the MachineRegistryDefinitions
as follows:
// Action
func(cmd *ContextMachineDefinitions[ContextType]) ActionName(contextValue *Context, evt Event, actTool ActionTool) error {...}
// Guard
func(cmd *ContextMachineDefinitions[ContextType]) GuardName(contextValue *Context, evt Event) (bool, error) {...}
// Invocation
func (cmd *ContextMachineDefinitions[ContextType]) InvocationName(contextValue Context, evt Event) {...}
In addition, within the definition of state machine records, two methods can be defined for getting and saving the context. These methods are useful for centralizing the logic in one place.
To define the context retrieval method, the ContextFromSource
function must be implemented in the StateMachineRegistry as follows:
// Context Retrieval
func (cmd *ContextMachineDefinitions[ContextType]) ContextFromSource(params ... any) (Context, error) {
// obtain parameters from params (params[0], params[1], etc)
// context retrieval logic
return context, nil
}
And to define the context saving method, the ContextToSource
function must be implemented in the StateMachineRegistry as follows:
// Context Saving
func (cmd *ContextMachineDefinitions[ContextType]) ContextToSource(context Context) error {
// context saving logic
return nil
}
In order to help you get a better understanding of how to use StatePro, we've created several examples that demonstrate its various features. These examples are organized into different branches, each focusing on a specific aspect of StatePro.
Here's a brief description of what each branch covers:
-
01-example-basic: This branch contains a basic example of how to use StatePro. It's the perfect starting point if you're new to the library.
-
02-read-basic-example: In this branch, you'll find examples of how to read the state of a state machine and how to react to changes.
-
03-write-basic-example: This branch focuses on writing to the state machine. You'll learn how to trigger events and cause state transitions.
-
04-events-example: This branch goes deeper into event handling with StatePro. It demonstrates how to define and use custom events.
-
05-invocations-services: In this branch, we cover the topic of invocations. You'll learn how to define and use asynchronous tasks that get executed when entering a state.
-
06-context-handlers: This branch focuses on context handlers. You'll learn how to define and use methods for getting and saving the context of a state machine.
To view the examples, simply switch to the respective branch. Remember, these examples are intended to be a learning resource. Feel free to modify and experiment with them as you become more comfortable with StatePro.