Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

🚦 Declarative Finite-State Machines in Go

License

NotificationsYou must be signed in to change notification settings

Gurpartap/statemachine-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

StateMachine

StateMachine supports creating productive State Machines In Go

GoDoc

Introduction

TLDR Turnstile Example

Turnstile StateMachine Example Diagram

State machines provide an alternative way of thinking about how we code anyjob/process/workflow.

Using a state machine for an object, that reacts to events differently based onits current state, reduces the amount of boilerplate and duct-taping you haveto introduce to your code.

StateMachine package provides a feature complete implementation offinite-state machines in Go.

What Is A Finite-State Machine Even?

A finite-state machine (FSM) is an abstract machine that can be in exactly oneof a finite number of states at any given time. The FSM can change from onestate to another in response to some external inputs; the change from onestate to another is called a transition. An FSM is defined by a list of itsstates, its initial state, and the conditions for each transition.

Wikipedia

Further Reading

Installation

Run this in your project directory:

go get -u https://github.com/Gurpartap/statemachine-go

Import StateMachine with this line in your Go code:

import"github.com/Gurpartap/statemachine-go"

Usage

Project Goals

A complex system that works is invariably found to have evolved from a simplesystem that worked. A complex system designed from scratch never works andcannot be patched up to make it work. You have to start over with a workingsimple system.

John Gall (1975)

StateMachine is simple in its specification, DSL, and internal implementation.And it works. There are no plans to introduce advanced FSM features such asregions, submachines, history based transitions, join, fork, etc., unlessthere's a simple way to do so without affecting the rest of the implementation.Well, submachines have already been implemented (partially and is in flux).

Performance is generally a significant factor when considering the use of athird party package. However, an API that I can actually code and design in mymind, ahead of using it, is just as important to me.

StateMachine's API design and developer productivity take precedence overits benchmark numbers (especially when compared to a bare metal switchstatement based state machine implementation, which may not take you as far).

For this, StateMachine provides a DSL using its builder objects. Thesebuilders compute and validate the state definitions, and then inject theresult (states, events, transitions, callbacks, etc.) into the state machineduring its initialization. Subsequently, these builders are free to begarbage collected.

Moreover, the statemachinery is not dependent on these DSL builders. Statemachines may also be initialized from directly allocating definition structs,or even parsing them from JSON, along with pre-registered callback references.

StateMachine definitions comprise of the following basic components:

These, and some additional components are covered below, along with theirexample usage code.

Adding a state machine is as simple as embedding statemachine.Machine inyour struct, defining states and events, along with their transitions.

typeProcessstruct {    statemachine.Machine// orMachine statemachine.Machine}funcNewProcess()*Process {process:=&Process{}process.Machine=statemachine.NewMachine()process.Machine.Build(func(m statemachine.MachineBuilder) {// ...    })// orprocess.Machine=statemachine.BuildNewMachine(func(m statemachine.MachineBuilder) {// ...    })returnprocess}

States, events, and transitions are defined using a DSL composed of "builders",includingstatemachine.MachineBuilder andstatemachine.EventBuilder. These builders provide a clean and type-safe DSLfor writing the specification of how the state machine functions.

The subsequent examples are a close port ofmy experiencewith using thestate_machinesRuby gem, from which StateMachine Go package's DSL is highly inspired.

System Process StateMachine Example Diagram

The example represented in the diagram above is implemented inexamples/cognizant/process.go.

If, instead of the builders DSL, you would rather want to specify theStateMachine directly using definition structs, take a look at theExampleMachineDeftest function. The same may also be imported from JSON orHCL.

States and Initial State

Possible states in the state machine may be manually defined, along with theinitial state. However, states are also inferred from event transitiondefinitions.

Initial state is set during the initialization of the state machine, and isrequired to be defined in the builder.

process.Machine.Build(func(m statemachine.MachineBuilder) {m.States("unmonitored","running","stopped")m.States("starting","stopping","restarting")// Initial state must be defined.m.InitialState("unmonitored")})

Events

Events act as a virtual function which when fired, trigger a state transition.

process.Machine.Build(func(m statemachine.MachineBuilder) {    m.Event("monitor",... )    m.Event("start",... )    m.Event("stop",... )    m.Event("restart",... )    m.Event("unmonitor",... )    m.Event("tick",... )})

Timed Events

Currently there one one timed event available:

TimedEvery(duration time.Duration)

Makes the event fire automatically at every specified duration.

process.Machine.Build(func(m statemachine.MachineBuilder) {m.Event("tick",func(e statemachine.EventBuilder) {e.TimedEvery(1*time.Second)// e.Transition().From(...).To(...)}// orm.Event("tick").TimedEvery(1*time.Second).// e.Transition().From(...).To(...)}

Choice

Choice assists in choosing event transition(s) based on a boolean condition.

Note that Choice is not executed if the event also specifies transitions of itsown.

The example below runs thetick event every second, and decides the state totransition to based on based on whether the process is currently running on thesystem or not, as long as we're also not set toSkipTicks (forstart, stop, and restart grace times).

process.Machine.Build(func(m statemachine.MachineBuilder) {// the nested way:m.Event("tick",func(e statemachine.EventBuilder) {e.Choice(&process.IsProcessRunning,func(c statemachine.ChoiceBuilder) {c.Unless(process.SkipTick)c.OnTrue(func(e statemachine.EventBuilder) {// e.Transition().From(...).To(...)})c.OnFalse(func(e statemachine.EventBuilder) {// e.Transition().From(...).To(...)})})})// preferred alternative syntax:m.Event("tick").TimedEvery(1*time.Second).Choice(&process.IsProcessRunning).Label("isRunning").// Label helps with diagrams and debuggingUnless(process.SkipTick).// TODO: move this to SkipUntilOnTrue(func(e statemachine.EventBuilder) {e.Transition().From("starting").To("running")e.Transition().From("restarting").To("running")e.Transition().From("stopping").To("running")e.Transition().From("stopped").To("running")}).OnFalse(func(e statemachine.EventBuilder) {e.Transition().From("starting").To("stopped")e.Transition().From("restarting").To("stopped")e.Transition().From("running").To("stopped")e.Transition().From("stopping").To("stopped")e.Transition().From("stopped").To("starting").If(&process.ShouldAutoStart).Label("shouldAutoStart")})}

Transitions

Transitions represent the change in state when an event is fired.

Note that.From(states ...string) accepts variadic args.

process.Machine.Build(func(m statemachine.MachineBuilder) {m.Event("monitor",func(e statemachine.EventBuilder) {e.Transition().From("unmonitored").To("stopped")    })m.Event("start",func(e statemachine.EventBuilder) {// from either of the defined statese.Transition().From("unmonitored","stopped").To("starting")    })m.Event("stop",func(e statemachine.EventBuilder) {e.Transition().From("running").To("stopping")    })m.Event("restart",func(e statemachine.EventBuilder) {e.Transition().From("running","stopped").To("restarting")    })m.Event("unmonitor",func(e statemachine.EventBuilder) {e.Transition().FromAny().To("unmonitored")    })m.Event("tick",func(e statemachine.EventBuilder) {// ...    })})

Transition Guards (Conditions)

Transition Guards are conditional callbacks which expect a boolean returnvalue, implying whether or not the transition in context should occur.

typeTransitionGuardFnBuilderinterface {If(guardFunc...TransitionGuardFunc)Unless(guardFunc...TransitionGuardFunc)}

Valid TransitionGuardFunc signatures:

*boolfunc()boolfunc(transition statemachine.Transition)bool
// Assuming process.IsProcessRunning is a bool variable, and// process.GetIsProcessRunning is a func returning a bool value.m.Event("tick",func(e statemachine.EventBuilder) {// If guarde.Transition().From("starting").To("running").If(&process.IsProcessRunning)// Unless guarde.Transition().From("starting").To("stopped").Unless(process.GetIsProcessRunning)// ...e.Transition().From("stopped").To("starting").If(func(t statemachine.Transition)bool {returnprocess.ShouldAutoStart&&!process.GetIsProcessRunning()    })// ore.Transition().From("stopped").To("starting").If(&process.ShouldAutoStart).AndUnless(&process.IsProcessRunning)// ...})

Transition Callbacks

Transition Callback methods are called before, around, or after a transition.

Before Transition

Before Transition callbacks do not act as a conditional, and a bool returnvalue will not impact the transition.

Valid TransitionCallbackFunc signatures:

func()func(m statemachine.Machine)func(t statemachine.Transition)func(m statemachine.Machine,t statemachine.Transition)
process.Machine.Build(func(m statemachine.MachineBuilder) {// ...m.BeforeTransition().FromAny().To("stopping").Do(func() {process.ShouldAutoStart=false    })// ...}

Around Transition

Around Transition's callback provides a next func as input, which must becalled inside the callback. (TODO: Missing to call the method will trigger a runtimefailure with an appropriately described error.)

Valid TransitionCallbackFunc signatures:

func(nextfunc())func(m statemachine.Machine,nextfunc())func(t statemachine.Transition,nextfunc())func(m statemachine.Machine,t statemachine.Transition,nextfunc())
process.Machine.Build(func(m statemachine.MachineBuilder) {// ...m.AroundTransition().From("starting","restarting").To("running").Do(func(nextfunc()) {start:=time.Now()// it'll trigger a failure if next is not callednext()elapsed=time.Since(start)log.Printf("it took %s to [re]start the process.\n",elapsed)        })// ...})

After Transition

After Transition callback is called when the state has successfullytransitioned.

Valid TransitionCallbackFunc signatures:

func()func(m statemachine.Machine)func(t statemachine.Transition)func(m statemachine.Machine,t statemachine.Transition)
process.Machine.Build(func(m statemachine.MachineBuilder) {// ...// notify system adminm.AfterTransition().From("running").ToAny().Do(process.DialHome)// log all transitionsm.AfterTransition().Any().Do(func(t statemachine.Transition) {log.Printf("State changed from '%s' to '%s'.\n",t.From(),t.To())        })// ...})

Event Callbacks

There is only one Event Callback method, which is called after an event failsto transition the state.

After Failure

After Failure callback is called when there's an error with event firing.

Valid TransitionCallbackFunc signatures:

func()func(errerror)func(m statemachine.Machine,errerror)func(t statemachine.Event,errerror)func(m statemachine.Machine,t statemachine.Event,errerror)
process.Machine.Build(func(m statemachine.MachineBuilder) {// ...m.AfterFailure().OnAnyEvent().Do(func(e statemachine.Event,errerror) {log.Printf("could not transition with event='%s' err=%+v\n",e.Event(),err            )        })// ...})

Matchers

Event Transition Matchers

These may map from one or morefrom states to exactly oneto state.

typeTransitionBuilderinterface {From(states...string)TransitionFromBuilderFromAny()TransitionFromBuilderFromAnyExcept(states...string)TransitionFromBuilder}typeTransitionFromBuilderinterface {ExceptFrom(states...string)TransitionExceptFromBuilderTo(statestring)TransitionToBuilder}typeTransitionExceptFromBuilderinterface {To(statestring)TransitionToBuilder}

Examples:

e.Transition().From("first_gear").To("second_gear")e.Transition().From("first_gear","second_gear","third_gear").To("stalled")allGears:=vehicle.GetAllGearStates()e.Transition().From(allGears...).ExceptFrom("neutral_gear").To("stalled")e.Transition().FromAny().To("stalled")e.Transition().FromAnyExcept("neutral_gear").To("stalled")

Transition Callback Matchers

These may map from one or morefrom states to one or moreto states.

typeTransitionCallbackBuilderinterface {From(states...string)TransitionCallbackFromBuilderFromAny()TransitionCallbackFromBuilderFromAnyExcept(states...string)TransitionCallbackFromBuilder}typeTransitionCallbackFromBuilderinterface {ExceptFrom(states...string)TransitionCallbackExceptFromBuilderTo(states...string)TransitionCallbackToBuilderToSame()TransitionCallbackToBuilderToAny()TransitionCallbackToBuilderToAnyExcept(states...string)TransitionCallbackToBuilder}typeTransitionCallbackExceptFromBuilderinterface {To(states...string)TransitionCallbackToBuilderToSame()TransitionCallbackToBuilderToAny()TransitionCallbackToBuilderToAnyExcept(states...string)TransitionCallbackToBuilder}

Examples:

m.BeforeTransition().From("idle").ToAny().Do(someFunc)m.AroundTransition().From("state_x").ToAnyExcept("state_y").Do(someFunc)m.AfterTransition().Any().Do(someFunc)// ...is same as:m.AfterTransition().FromAny().ToAny().Do(someFunc)

Event Callback Matchers

These may match on one or moreevents.

typeEventCallbackBuilderinterface {On(events...string)EventCallbackOnBuilderOnAnyEvent()EventCallbackOnBuilderOnAnyEventExcept(events...string)EventCallbackOnBuilder}typeEventCallbackOnBuilderinterface {Do(callbackFuncEventCallbackFunc)EventCallbackOnBuilder}

Examples:

m.AfterFailure().OnAnyEventExcept("event_z").Do(someFunc)

Callback Functions

Any callback function's arguments (and return types) are dynamically set basedon what types are defined (dependency injection). Setting any unavailable argor return type will cause a panic during initialization.

For example, if your BeforeTransition() callback does not need access to thestatemachine.Transition variable, you may just define the callback with ablank function signature:func(), instead offunc(t statemachine.Transition). Similarly, for an AfterFailure()callback you can usefunc(err error), orfunc(e statemachine.Event, err error), or even justfunc() .

About

Copyright 2017 Gurpartap SinghLicensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at    http://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.

Releases

No releases published

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp