Giter Site home page Giter Site logo

fsm's People

Contributors

abramlab avatar aman-sai avatar annismckenzie avatar apertoire avatar atlas-comstock avatar baw-benji avatar bcho avatar eminden avatar itsarbit avatar jamesbibby avatar jan-xyz avatar kristoiv avatar kuzmig avatar majst01 avatar mavogel avatar maxekman avatar nbaztec avatar noneback avatar passos avatar peteut avatar prashant-agarwala avatar tony 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  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

fsm's Issues

Preconditions, PostConditions, transitions, onEntry, onExit

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?

Panic appearing in callback causes weird error

example test code:

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", )
}

output:

=== RUN   TestCallbackPanic
fatal error: sync: RUnlock of unlocked RWMutex

suggest

recover the panic in the callback func and return error in function fsm.Event()

Is it one redundant code?

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.

Separate module path for the v1 releases

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

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?

Proper way to call Events

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

Scheduling internal transitions within callbacks

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.

Why fsm code not update to the latest version

Hi, I see you example :

fsm

When run the demo , it errors:

image

So I look the code , found the callback :

type Callback func(*Event)

But the latest version, the callback :

type Callback func(context.Context, *Event)

So I try to update the code to the latest version:

image

But it not work

Using enum instead strings

The current approach of fsm is to use string to represent stages:

fsm/examples/simple.go

Lines 14 to 19 in e668a85

fsm := fsm.NewFSM(
"closed",
fsm.Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},

which is kind of error prone. These strings should better be replace with enum values instead, to

  • save space, and most importantly
  • allow compiler to do spellcheck for us

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.

Permissions system and available states from current transition

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.

fsm.Event(ctx, ...) not work properly with context.WithTimeout()

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.

Double transitions possible as "active transition" is only activated post "before_"-callbacks

Also a problem for "after_"-callbacks in transitions where source and destination is the same state.

Test case reproducing the error:

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()
}

Suggested solution:

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()
{...}

Provides a v1 release

Users of this library would need a semver v1 release of this library to ensure API stability.

Available Transitions/Events

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?

Allow "No Transition"

It is sometimes needed to allow an event to be processed and no state transition, such as timeout in receive message, and then resend.

Thread safety of calls that result in transitions

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("...")
}()

fix typo

fix some typo while reading the source code
ref: #74

Release

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

could the dst state to be set in the event handler?

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

cant go back?

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

Deadlock when event is fired in callback

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

Not possible to return errors in callback function

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?

when I use SetState in a callback in a `before_<event>` => deadlock

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!

New Release ?

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?

Testing FSM objects: reflect.DeepEqual fails when FSM attribute is set on an object

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?

SetCurrent(state string)

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.

Multiple events with same name

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?

Context in event handlers

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).

FSM persistance

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 ?

Multiple callback coverage bugs for the same event

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

Can FSM.Transaction() accept a parameter to cancel the asynchronous transaction?

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.

Should cancelFunc be public?

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:

  1. We may need to skip the cancelFunc if it is nil to give decouple when testing the callback function here.
  2. We may need to public the cancelFunc to CancelFunc or a set function to assign the cancelFunc.

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.