-
Notifications
You must be signed in to change notification settings - Fork 314
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow transitions in callbacks (#88)
* Allow state transitions in callbacks This adds the possibility of "starting" a state machine and have it execute multiple state transitions in succession, given that no errors occur. The equivalent code without this change: ```go var errTransition error for errTransition == nil { transitions := request.FSM.AvailableTransitions() if len(transitions) == 0 { break } if len(transitions) > 1 { errTransition = errors.New("only 1 transition should be available") } errTransition = request.FSM.Event(transitions[0]) } if errTransition != nil { fmt.Println(errTransition) } ``` Arguably, that’s bad because of several reasons: 1. The state machine is used like a puppet. 2. The state transitions that make up the "happy path" are encoded outside the state machine. 3. The code really isn’t good. 4. There’s no way to intervene or make different decisions on which state to transition to next (reinforces bullet point 2). 5. There’s no way to add proper error handling. It is possible to fix a certain number of those problems but not all of them, especially 2 and 4 but also 1. The added test is green and uses both an enter state and an after event callback. No other test case was touched in any way (besides enhancing the context one that was added in the previous commit). * Allow async state transition to be canceled This adds a context and cancelation facility to the type `AsyncError`. Async state transitions can now be canceled by calling `CancelTransition` on the AsyncError returned by `fsm.Event`. The context on that error can also be handed off as described in #77 (comment). * Add example for triggering transitions in callbacks * Add example for canceling an async transition
- Loading branch information
1 parent
54bbb61
commit 3637340
Showing
7 changed files
with
362 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
//go:build ignore | ||
// +build ignore | ||
|
||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/looplab/fsm" | ||
) | ||
|
||
func main() { | ||
f := fsm.NewFSM( | ||
"start", | ||
fsm.Events{ | ||
{Name: "run", Src: []string{"start"}, Dst: "end"}, | ||
}, | ||
fsm.Callbacks{ | ||
"leave_start": func(_ context.Context, e *fsm.Event) { | ||
e.Async() | ||
}, | ||
}, | ||
) | ||
|
||
err := f.Event(context.Background(), "run") | ||
asyncError, ok := err.(fsm.AsyncError) | ||
if !ok { | ||
panic(fmt.Sprintf("expected error to be 'AsyncError', got %v", err)) | ||
} | ||
var asyncStateTransitionWasCanceled bool | ||
go func() { | ||
<-asyncError.Ctx.Done() | ||
asyncStateTransitionWasCanceled = true | ||
if asyncError.Ctx.Err() != context.Canceled { | ||
panic(fmt.Sprintf("Expected error to be '%v' but was '%v'", context.Canceled, asyncError.Ctx.Err())) | ||
} | ||
}() | ||
asyncError.CancelTransition() | ||
time.Sleep(20 * time.Millisecond) | ||
|
||
if err = f.Transition(); err != nil { | ||
panic(fmt.Sprintf("Error encountered when transitioning: %v", err)) | ||
} | ||
if !asyncStateTransitionWasCanceled { | ||
panic("expected async state transition cancelation to have propagated") | ||
} | ||
if f.Current() != "start" { | ||
panic("expected state to be 'start'") | ||
} | ||
|
||
fmt.Println("Successfully ran state machine.") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
//go:build ignore | ||
// +build ignore | ||
|
||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/looplab/fsm" | ||
) | ||
|
||
func main() { | ||
var afterFinishCalled bool | ||
fsm := fsm.NewFSM( | ||
"start", | ||
fsm.Events{ | ||
{Name: "run", Src: []string{"start"}, Dst: "end"}, | ||
{Name: "finish", Src: []string{"end"}, Dst: "finished"}, | ||
{Name: "reset", Src: []string{"end", "finished"}, Dst: "start"}, | ||
}, | ||
fsm.Callbacks{ | ||
"enter_end": func(ctx context.Context, e *fsm.Event) { | ||
if err := e.FSM.Event(ctx, "finish"); err != nil { | ||
fmt.Println(err) | ||
} | ||
}, | ||
"after_finish": func(ctx context.Context, e *fsm.Event) { | ||
afterFinishCalled = true | ||
if e.Src != "end" { | ||
panic(fmt.Sprintf("source should have been 'end' but was '%s'", e.Src)) | ||
} | ||
if err := e.FSM.Event(ctx, "reset"); err != nil { | ||
fmt.Println(err) | ||
} | ||
}, | ||
}, | ||
) | ||
|
||
if err := fsm.Event(context.Background(), "run"); err != nil { | ||
panic(fmt.Sprintf("Error encountered when triggering the run event: %v", err)) | ||
} | ||
|
||
if !afterFinishCalled { | ||
panic(fmt.Sprintf("After finish callback should have run, current state: '%s'", fsm.Current())) | ||
} | ||
|
||
currentState := fsm.Current() | ||
if currentState != "start" { | ||
panic(fmt.Sprintf("expected state to be 'start', was '%s'", currentState)) | ||
} | ||
|
||
fmt.Println("Successfully ran state machine.") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package fsm | ||
|
||
import ( | ||
"context" | ||
"time" | ||
) | ||
|
||
type uncancel struct { | ||
context.Context | ||
} | ||
|
||
func (*uncancel) Deadline() (deadline time.Time, ok bool) { return } | ||
func (*uncancel) Done() <-chan struct{} { return nil } | ||
func (*uncancel) Err() error { return nil } | ||
|
||
// uncancelContext returns a context which ignores the cancellation of the parent and only keeps the values. | ||
// Also returns a new cancel function. | ||
// This is useful to keep a background task running while the initial request is finished. | ||
func uncancelContext(ctx context.Context) (context.Context, context.CancelFunc) { | ||
return context.WithCancel(&uncancel{ctx}) | ||
} |
Oops, something went wrong.