looplab / fsm Goto Github PK
View Code? Open in Web Editor NEWFinite State Machine for Go
License: Apache License 2.0
Finite State Machine for Go
License: Apache License 2.0
eg, an event that can transition from any state to Done State. Thanks
Preconditions are handled using "beforeEvent"
Postconditions are handled using "afterEvent"
State support enterState and leaveState where appropriate state logic occurs
I think there needs to support for "doTansition" where the transition logic would occur.
Thoughts?
func TestCallbackPanic(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
panic("test")
},
},
)
fsm.Event("run", )
}
=== RUN TestCallbackPanic
fatal error: sync: RUnlock of unlocked RWMutex
recover the panic in the callback func and return error in function fsm.Event()
Hi,
Firstly, thanks for you work, it's a great job.
I have one question in the following code:
*In fsm.go line:324 function: func (f FSM) Event(event string, args ...interface{}) error
// Perform the rest of the transition, if not asynchronous.
f.stateMu.RUnlock()
err = f.doTransition()
f.stateMu.RLock()
if err != nil {
return InternalError{}
}
Per the comment in line:319 "// Perform the rest of the transition, if not asynchronous.".
I think that means in sync mode would run the following code.
So, InternalError{} should come from f.doTransition()
But, In function:
// doTransition wraps transitioner.transition.
func (f *FSM) doTransition() error {
return f.transitionerObj.transition(f)
}
"f.transitionerObj" is "transitionerStruct"
Then transitionerStruct's transition() should be called.
But In this function, the only way return error is "f.transition == nil"
func (t transitionerStruct) transition(f *FSM) error {
if f.transition == nil {
return NotInTransitionError{}
}
f.transition()
f.transition = nil
return nil
}
But, per the previous code, if "f.doTransition()" can be invoked, "f.transition" have no way to get nil.
Is it one redundant code? I'm confuse, I hope I could get your help to confirm did I miss something or not.
Currently the module path in go.mod file is same for v0 and v1 releases. This makes it impossible to use latest the fsm package if there are any indirect dependencies on v0 versions of fsm package. And it would be according to a standard way, if the module path of the v1 releases are as follows,
module github.com/looplab/fsm/v1
What is your opinion on this ?
Self transitions don't seem to be supported. The following code snippet from fsm.go seems to consider self transitions as errors.
if f.current == dst {
f.afterEventCallbacks(e)
return &NoTransitionError{e.Err}
}
Perhaps I'm not using the fsm appropriately. any thoughts?
I don't understand this part of code: https://github.com/looplab/fsm/blob/master/fsm.go#L322
We're unlocking mutex for a read, doing some work and locking for read.
What is so? Thank you.
This includes using the main repo version of cover.
Sorry if this is a stupid question.
Related To #37
Why can't we fire events in Callback?
where are we supposed to call Event Transition?
I'm trying to write a protocol lib, that reacts based on the server response.
So from the callback I'm sending messages to the server, and based on the response I fire a specific event.
Thanks for your help
currently, if the destination state is the same as the source state (https://github.com/looplab/fsm/blob/master/fsm.go#L325), no after/before event is fired.
i find it reasonable that no after/before state should be fired (since state is unchanged), but i was expecting the after/before events to be triggered.
any thoughts on this matter ?
In the Javascript implementation, the EventDesc Dst (To in the JS implementation) can accept a function to implement conditional transitions (https://github.com/jakesgordon/javascript-state-machine/blob/master/docs/states-and-transitions.md#conditional-transitions).
FSM should allow conditional transitions with the target state determined via a function call at runtime.
Hi,
I have a particular need and want to get your views and the best way to deal with this. Here are my transitions -
[1]initial=disconnected
[2]disconnected: on "connect"->connected
[3]connected: on "disconnect"->disconnected
[4]connected: on "echo" -> "connected"
[5]connected: on "echolimit_met"->"up"
Please note that on line [4], the transition is from connected->connected which results in NoTransitionError and that is ok.
Now, any number of echo events might happen in connected state. But, after 3rd echo I need a transition to "up"
I intend to do this transition (using a echolimit_met event) after the current event
has been processed. There is currently no way to do this within the callbacks and I'm doing this right now by starting a new goroutine within the callback
go func() {
fsm.Event(e)
}()
Do this approach seem right to you?
Perhaps we could have a fsm.ScheduleLater(e) which the fsm executes towards the end of current event processing.
when looplab/fsm comes new version?
The current approach of fsm is to use string to represent stages:
Lines 14 to 19 in e668a85
which is kind of error prone. These strings should better be replace with enum values instead, to
Like this:
type myFSM int
const (
Init myFSM = iota
Open
Progress
Close = Stop
)
For further details, check out https://pkg.go.dev/golang.org/x/tools/cmd/stringer
Please consider.
May I define the FSMs class dynamically through config file or database, not from code? Like when start service, create FSM class and callbacks functions by reading file or database, is this possible?
Is this abandoned?
beforeEventCallback
is a candidate for this, but checking for permissions also should encompass easy a trivial way to determine which states are accessible from the fsm
's current state.
I figured beforeEventCallback
could be ran iterated for events through a helper function, and that could be used to determine if an event could run without running Event
. But that has two downsides: we have Can
's default usage won't check the callback, making it a little confusing to others who read the code (the method could infer to some all is good to proceed with the event), and in my case I want to use beforeEventCallback
for other purposes.
The user could do this, perform slice operations and get to it themselves. I'm going to experiment with how adding a preliminary callback hook / a helper method or two outside of Event
to weigh if adding it is justified.
Out the door, I'm more inclined in favor of incorporating one in some form. It's possible the additional hook won't incur any penalty for those who decide not to add permission callbacks.
Generation of FSM at runtime from SCXML file/reader? Would be pretty cool! What are your thoughts?
When I use context.WithTimeout(ctx, time.Duration) with fsm.Event(ctx, ...) like:
Callbacks{
"enter_end": func(ctx context.Context, e *Event) {
go func() {
<-ctx.Done()
enterEndAsyncWorkDone = true
}()
},
},
the internal block exec immediately, not after timeout.
Generation of a dot file for the FSM would be rad.
Also a problem for "after_"-callbacks in transitions where source and destination is the same state.
func TestDoubleTransition(t *testing.T) {
var fsm *FSM
var wg sync.WaitGroup
wg.Add(2)
fsm = NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_run": func(e *Event) {
wg.Done()
// Imagine a concurrent event coming in of the same type while
// the data access mutex is unlocked because the current transition
// is running its event callbacks, getting around the "active"
// transition checks
if len(e.Args) == 0 {
// Must be concurrent so the test may pass when we add a mutex that synchronizes
// calls to Event(...). It will then fail as an inappropriate transition as we
// have changed state.
go func() {
if err := fsm.Event("run", "second run"); err != nil {
fmt.Println(err)
wg.Done() // It should fail, and then we unfreeze the test.
}
}()
time.Sleep(20*time.Millisecond)
}else{
panic("Was able to reissue an event mid-transition")
}
},
},
)
if err := fsm.Event("run"); err != nil {
fmt.Println(err)
}
wg.Wait()
}
What if we remove the "in transition" check and instead add another mutex (sync.Mutex this time) that locks "fsm.Event()"-calls from start to finish, making events synchronous. Thus calls to the Event method don't fail if they are valid transitions, but happen in sequence. That would also fix my test-case above.
{...}
func (f *FSM) Event(event string, args ...interface{}) error {
f.eventMutex.Lock()
defer f.eventMutex.Unlock()
f.mutex.Lock()
defer f.mutex.Unlock()
{...}
Users of this library would need a semver v1 release of this library to ensure API stability.
Would be cool if it was possible to query the FSM for the available transitions from the current state. Maybe an additional method that returns the valid events that would trigger the valid transitions from the current state. Thoughts?
It is sometimes needed to allow an event to be processed and no state transition, such as timeout in receive message, and then resend.
Possible problem case: eg. reading the current state name while transitioning. There is no locking going on as far as I can see. Am I missing something?
Eg.:
fsm := NewFSM(....)
go func() {
fmt.Println(fsm.Current())
}()
go func() {
fsm.Event("...")
}()
How can i get the error from callback function?
fix some typo while reading the source code
ref: #74
Hi,
we are using commit 9c1f252 in our software. Our renovate bot is quite unhappy with the fact that this is not a tagged release version.
Would it be possible to build a new release with the latest chanages?
Thanks in advanced!
Patrick
currently the dst state is set in the event defining, could it be set in the call backs, which would be much flexible?
as we just need to define a set of state, and the trigger, then we can add the change the state according to the handler result
thanks
Trailing "a" character in the documentation comment for the Callbacks type.
Can the state return?
example:
f:= fsm.NewFSM(
"5",
fsm.Events{
//Src--状态源;Dst--触发后转换的状态;Name--事件名称
{Name: "0", Src: []string{"1"}, Dst: "2"},
{Name: "1", Src: []string{"2"}, Dst: "3"},
{Name: "2", Src: []string{"3"}, Dst: "4"},
{Name: "3", Src: []string{"4"}, Dst: "5"},
{Name: "4", Src: []string{"5"}, Dst: "6"},
},
fsm.Callbacks{
"enter_state": func(e *fsm.Event) { d.enterState(e) },
//添加触发函数
"0": func(e *fsm.Event) {
fmt.Printf("触发状态:%s \n", e.Src)
},
},
)
f.Current()// state=5
f.Back()// my fun---the current state=4
For example https://github.com/smothiki/fsm/tree/master/examples/genesis
I have the following state machine defined and want to handle an error event which tries to achieve a single state. Here while handling error event we are getting no transition error as an error from the event. A better way might be to log it as debug info rather than throwing it as an event error.
I guess you shouldn't normally do that but imagine you would like to implement the most useless machine ever:
var fsm *FSM
fsm = NewFSM(
"off",
Events{
{Name: "turn-on", Src: []string{"off"}, Dst: "on"},
{Name: "turn-off", Src: []string{"on"}, Dst: "off"},
},
Callbacks{
"leave_off": func(e *Event) {
fmt.Println("Not today")
if err := fsm.Event("turn-off"); err != nil {
fmt.Println("Not able to turn off")
}
},
},
)
fsm.Event("turn-on")
The result is deadlock.
Expected behavior - Not able to turn off
message printed
Scenario:
Status A - > status B, I want to check if a field is valid before transition.
I know there is a before_EVENT, but I don't know how to control don't let the status from A to B in this function.
Thanks
Again more of a question perhaps, but neither e.Cancel() nor the CallBack type allow for an error.
I have a "before_purchase" callback, which will fire off an API call and update the database. These things can fail and it would be useful to have them bubble up to the fsm.FSM.Event() call.
Perhaps this is not the right use?
This will allow us to use errors.Is to detect sub errors instead of string comparisons.
I am writing an fsm that needs the follow:
an Event named creating
can goes to 3 different states (ok
, unreachable
, creating
) depending on some checks. In order to realize this, I wrote a callback function like this one:
func (p *ClusterState) beforeCreating(e *fsm.Event) {
p.Logger.Infof("Triggered event %v, trying to reach status %s", e.Event, e.Dst)
if p.Unreachable {
timeLimit := p.CreatedAt.Add(time.Minute * 30)
if timeLimit.Before(p.Clock.Now()) {
e.FSM.SetState("CREATING") // it hangs here
}
}
}
Probably there is a better way to do that, or i am missing something.
Can anyone help me with that?
Thanks in advance!
A commit was introduced 01565a8 to address a very important issue, "storing metadata" that can be consumed as one transitions between states.
However, this commit has not been published and the lib is still in v0.2.0
. When will it be possible to have a release that ships with this feature?
We use testify mocks for mocking objects while testing (uses reflect.DeepEqual
to compare).
We are using FSM on a model (has FSM
property) and when this object is passed around to repo layer for saving, we mock the Save
method in our tests. The problem is that, the tests fail because ob.FSM
property doesn't match with the expected object's property.
To work around this we have to strip the FSM property (set ob.FSM = nil
) and pass it to Save
so that the object matching is successful. Is there a correct way to handle this?
This is probably more of a question, don't even know whether this is the right place to ask, but ... There doesn't seem to be a way to set the current state?
In the JavaScript version this is simply possible by assignment.
This would be particularly useful if I persist a struct (containing a state field) and later-on retrieve it and would want to check whether certain transitions were possible and/or fire certain callbacks.
Would it make sense to add SetCurrent(state string) to FSM? I'd be happy to send a pull-request. Probably all that's needed is to check whether "state" is a valid state in the FSM.
And another one I'm afraid ... really liking FSM though!
Following the example #2, I want to have do following:
{Name: "purchase", Src: []string{"quoted"}, Dst: "open"},
{Name: "purchase", Src: []string{"quoted"}, Dst: "onhold"},
I issue an purchase event, if it succeed the new state should be open. If it fails, or is cancelled, I would like FSM to try the next. Fysom seems to do something like that? https://github.com/oxplot/fysom/blob/master/fysom.py#L65
But this https://github.com/looplab/fsm/blob/master/fsm.go#L179 pretty much prevents me from doing that.
Any suggestions, am I doing it wrong?
It would be great to add a context to event trigger function and event handler function signature because it is required in many operations.
Probably it will require to double the methods (i.e. fsm.EventContext(ctx, event, args...)
and type CallbackContext func(ctx, *Event)
.
Cancel should take an error object and just set the error.
Since this means changing the interface we could maybe do something like:
Cancel(errs ...error)
Firstly, thank you for your work, this is really useful.
I currently work on a project that needs a FSM (in fact many FSM).
This FSMs are created and fed an event log read.
For a production use, we need to schedule some persistence of the FSMs in order to avoid reading the event log from start each time we restart the program.
My initial approach is to serialize data to persist (via encoding/gob for ex.) including FSMs.
But the type FSM has no exported field that can be accessed by gob encoders.
So my question is : have you planned to add functions like (f *FSM) Save() []byte and (f *FSM) Load(backup []byte?
Did you think this can be useful ?
bug code:
package main
import (
"fmt"
"github.com/looplab/fsm"
)
func main() {
fsm := fsm.NewFSM(
"idle",
fsm.Events{
{Name: "scan", Src: []string{"idle"}, Dst: "scanning"},
},
fsm.Callbacks{
"after_scan": func(e *fsm.Event) {
fmt.Println("after_scan: " + e.FSM.Current())
},
"scan": func(e *fsm.Event) {
fmt.Println("scan: " + e.FSM.Current())
},
},
)
fmt.Println(fsm.Current())
if err := fsm.Event("scan"); err != nil {
fmt.Println(err)
}
}
two callback all store in map as key: cKey{"scan", callbackAfterEvent}
so bug is coming
As far as I know, after I triggered an asynchronous event, the only thing I can do is calling FSM.Transaction()
to finish its transaction.
sm := fsm.NewFSM(
"start",
fsm.Events{
{Name: "start", Src: []string{"start"}, Dst: "end"},
},
fsm.Callbacks{
"leave_start": func(e *fsm.Event) {
e.Async()
},
},
)
_ = sm.Event("start") // Return immediately
// Do my own business
// It's impossible to cancel the event at this time
_ = sm.Transaction() // Only can finish transaction
Currently there is no way to interrupt an asynchronous transaction on demand, right?
So I suppose maybe Transaction()
method can accept a parameter to cancel the execution.
From the v1.0.0 release, the cancelFunc
is introduced during FSM init with context. However, we had decoupled the callback function. When the unit test of callback may panic since it may cause the cancelFunc
is not assignable.
For example:
create a callback function and do the unit test with a callback function. The input event may not have cancelFunc and caused panic.
P.S: Here is the event caused the panic.
Suggestion:
cancelFunc
if it is nil to give decouple when testing the callback function here.cancelFunc
to CancelFunc or a set function to assign the cancelFunc
.A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.