fsm: improve and convert to interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
126
fsm/default.go
Normal file
126
fsm/default.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package fsm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
body interface{}
|
||||
name string
|
||||
}
|
||||
|
||||
var _ State = &state{}
|
||||
|
||||
func (s *state) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *state) Body() interface{} {
|
||||
return s.body
|
||||
}
|
||||
|
||||
// fsm is a finite state machine
|
||||
type fsm struct {
|
||||
statesMap map[string]StateFunc
|
||||
current string
|
||||
statesOrder []string
|
||||
opts Options
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewFSM creates a new finite state machine having the specified initial state
|
||||
// with specified options
|
||||
func NewFSM(opts ...Option) *fsm {
|
||||
return &fsm{
|
||||
statesMap: map[string]StateFunc{},
|
||||
opts: NewOptions(opts...),
|
||||
}
|
||||
}
|
||||
|
||||
// Current returns the current state
|
||||
func (f *fsm) Current() string {
|
||||
f.mu.Lock()
|
||||
s := f.current
|
||||
f.mu.Unlock()
|
||||
return s
|
||||
}
|
||||
|
||||
// Current returns the current state
|
||||
func (f *fsm) Reset() {
|
||||
f.mu.Lock()
|
||||
f.current = f.opts.Initial
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// State adds state to fsm
|
||||
func (f *fsm) State(state string, fn StateFunc) {
|
||||
f.mu.Lock()
|
||||
f.statesMap[state] = fn
|
||||
f.statesOrder = append(f.statesOrder, state)
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// Start runs state machine with provided data
|
||||
func (f *fsm) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) {
|
||||
var err error
|
||||
|
||||
f.mu.Lock()
|
||||
options := f.opts
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
sopts := []StateOption{StateDryRun(options.DryRun)}
|
||||
|
||||
cstate := options.Initial
|
||||
states := make(map[string]StateFunc, len(f.statesMap))
|
||||
for k, v := range f.statesMap {
|
||||
states[k] = v
|
||||
}
|
||||
f.current = cstate
|
||||
f.mu.Unlock()
|
||||
|
||||
var s State
|
||||
s = &state{name: cstate, body: args}
|
||||
nstate := s.Name()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
fn, ok := states[nstate]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`state "%s" %w`, nstate, ErrInvalidState)
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.current = nstate
|
||||
f.mu.Unlock()
|
||||
|
||||
// wrap the handler func
|
||||
for i := len(options.Wrappers); i > 0; i-- {
|
||||
fn = options.Wrappers[i-1](fn)
|
||||
}
|
||||
|
||||
s, err = fn(ctx, s, sopts...)
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
return s.Body(), err
|
||||
case s.Name() == StateEnd:
|
||||
return s.Body(), nil
|
||||
case s.Name() == "":
|
||||
for idx := range f.statesOrder {
|
||||
if f.statesOrder[idx] == nstate && len(f.statesOrder) > idx+1 {
|
||||
nstate = f.statesOrder[idx+1]
|
||||
}
|
||||
}
|
||||
default:
|
||||
nstate = s.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
174
fsm/fsm.go
174
fsm/fsm.go
@@ -3,8 +3,6 @@ package fsm // import "go.unistack.org/micro/v3/fsm"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -12,170 +10,20 @@ var (
|
||||
StateEnd = "end"
|
||||
)
|
||||
|
||||
// Options struct holding fsm options
|
||||
type Options struct {
|
||||
// DryRun mode
|
||||
DryRun bool
|
||||
// Initial state
|
||||
Initial string
|
||||
// HooksBefore func slice runs in order before state
|
||||
HooksBefore []HookBeforeFunc
|
||||
// HooksAfter func slice runs in order after state
|
||||
HooksAfter []HookAfterFunc
|
||||
type State interface {
|
||||
Name() string
|
||||
Body() interface{}
|
||||
}
|
||||
|
||||
// HookBeforeFunc func signature
|
||||
type HookBeforeFunc func(ctx context.Context, state string, args interface{})
|
||||
|
||||
// HookAfterFunc func signature
|
||||
type HookAfterFunc func(ctx context.Context, state string, args interface{})
|
||||
|
||||
// Option func signature
|
||||
type Option func(*Options)
|
||||
|
||||
// StateOptions holds state options
|
||||
type StateOptions struct {
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// StateDryRun says that state executes in dry run mode
|
||||
func StateDryRun(b bool) StateOption {
|
||||
return func(o *StateOptions) {
|
||||
o.DryRun = b
|
||||
}
|
||||
}
|
||||
|
||||
// StateOption func signature
|
||||
type StateOption func(*StateOptions)
|
||||
|
||||
// InitialState sets init state for state machine
|
||||
func InitialState(initial string) Option {
|
||||
return func(o *Options) {
|
||||
o.Initial = initial
|
||||
}
|
||||
}
|
||||
|
||||
// HookBefore provides hook func slice
|
||||
func HookBefore(fns ...HookBeforeFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.HooksBefore = fns
|
||||
}
|
||||
}
|
||||
|
||||
// HookAfter provides hook func slice
|
||||
func HookAfter(fns ...HookAfterFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.HooksAfter = fns
|
||||
}
|
||||
}
|
||||
// StateWrapper wraps the StateFunc and returns the equivalent
|
||||
type StateWrapper func(StateFunc) StateFunc
|
||||
|
||||
// StateFunc called on state transition and return next step and error
|
||||
type StateFunc func(ctx context.Context, args interface{}, opts ...StateOption) (string, interface{}, error)
|
||||
type StateFunc func(ctx context.Context, state State, opts ...StateOption) (State, error)
|
||||
|
||||
// FSM is a finite state machine
|
||||
type FSM struct {
|
||||
mu sync.Mutex
|
||||
statesMap map[string]StateFunc
|
||||
statesOrder []string
|
||||
opts *Options
|
||||
current string
|
||||
}
|
||||
|
||||
// New creates a new finite state machine having the specified initial state
|
||||
// with specified options
|
||||
func New(opts ...Option) *FSM {
|
||||
options := &Options{}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
return &FSM{
|
||||
statesMap: map[string]StateFunc{},
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
// Current returns the current state
|
||||
func (f *FSM) Current() string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.current
|
||||
}
|
||||
|
||||
// Current returns the current state
|
||||
func (f *FSM) Reset() {
|
||||
f.mu.Lock()
|
||||
f.current = f.opts.Initial
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// State adds state to fsm
|
||||
func (f *FSM) State(state string, fn StateFunc) {
|
||||
f.mu.Lock()
|
||||
f.statesMap[state] = fn
|
||||
f.statesOrder = append(f.statesOrder, state)
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// Init initialize fsm and check states
|
||||
|
||||
// Start runs state machine with provided data
|
||||
func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) {
|
||||
var err error
|
||||
var ok bool
|
||||
var fn StateFunc
|
||||
var nstate string
|
||||
|
||||
f.mu.Lock()
|
||||
options := f.opts
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
sopts := []StateOption{StateDryRun(options.DryRun)}
|
||||
|
||||
cstate := options.Initial
|
||||
states := make(map[string]StateFunc, len(f.statesMap))
|
||||
for k, v := range f.statesMap {
|
||||
states[k] = v
|
||||
}
|
||||
f.current = cstate
|
||||
f.mu.Unlock()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
fn, ok = states[cstate]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`state "%s" %w`, cstate, ErrInvalidState)
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.current = cstate
|
||||
f.mu.Unlock()
|
||||
for _, fn := range options.HooksBefore {
|
||||
fn(ctx, cstate, args)
|
||||
}
|
||||
nstate, args, err = fn(ctx, args, sopts...)
|
||||
for _, fn := range options.HooksAfter {
|
||||
fn(ctx, cstate, args)
|
||||
}
|
||||
switch {
|
||||
case err != nil:
|
||||
return args, err
|
||||
case nstate == StateEnd:
|
||||
return args, nil
|
||||
case nstate == "":
|
||||
for idx := range f.statesOrder {
|
||||
if f.statesOrder[idx] == cstate && len(f.statesOrder) > idx+1 {
|
||||
nstate = f.statesOrder[idx+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
cstate = nstate
|
||||
}
|
||||
}
|
||||
type FSM interface {
|
||||
Start(context.Context, interface{}, ...Option) (interface{}, error)
|
||||
Current() string
|
||||
Reset()
|
||||
State(string, StateFunc)
|
||||
}
|
||||
|
@@ -1,63 +1,72 @@
|
||||
package fsm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.unistack.org/micro/v3/logger"
|
||||
)
|
||||
|
||||
func TestFSMStart(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
buf := bytes.NewBuffer(nil)
|
||||
pfb := func(_ context.Context, state string, _ interface{}) {
|
||||
fmt.Fprintf(buf, "before state %s\n", state)
|
||||
|
||||
if err := logger.DefaultLogger.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pfa := func(_ context.Context, state string, _ interface{}) {
|
||||
fmt.Fprintf(buf, "after state %s\n", state)
|
||||
|
||||
wrapper := func(next StateFunc) StateFunc {
|
||||
return func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||
sctx = logger.NewContext(sctx, logger.Fields("state", s.Name()))
|
||||
return next(sctx, s, opts...)
|
||||
}
|
||||
}
|
||||
f := New(InitialState("1"), HookBefore(pfb), HookAfter(pfa))
|
||||
f1 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
|
||||
args := req.(map[string]interface{})
|
||||
|
||||
f := NewFSM(InitialState("1"), WrapState(wrapper))
|
||||
f1 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||
_, ok := logger.FromContext(sctx)
|
||||
if !ok {
|
||||
t.Fatal("f1 context does not have logger")
|
||||
}
|
||||
args := s.Body().(map[string]interface{})
|
||||
if v, ok := args["request"].(string); !ok || v == "" {
|
||||
return "", nil, fmt.Errorf("empty request")
|
||||
return nil, fmt.Errorf("empty request")
|
||||
}
|
||||
return "2", map[string]interface{}{"response": "test2"}, nil
|
||||
return &state{name: "", body: map[string]interface{}{"response": "state1"}}, nil
|
||||
}
|
||||
f2 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
|
||||
args := req.(map[string]interface{})
|
||||
if v, ok := args["response"].(string); !ok || v == "" {
|
||||
return "", nil, fmt.Errorf("empty response")
|
||||
f2 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||
_, ok := logger.FromContext(sctx)
|
||||
if !ok {
|
||||
t.Fatal("f2 context does not have logger")
|
||||
}
|
||||
return "", map[string]interface{}{"response": "test"}, nil
|
||||
args := s.Body().(map[string]interface{})
|
||||
if v, ok := args["response"].(string); !ok || v == "" {
|
||||
return nil, fmt.Errorf("empty response")
|
||||
}
|
||||
return &state{name: "", body: map[string]interface{}{"response": "state2"}}, nil
|
||||
}
|
||||
f3 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) {
|
||||
args := req.(map[string]interface{})
|
||||
if v, ok := args["response"].(string); !ok || v == "" {
|
||||
return "", nil, fmt.Errorf("empty response")
|
||||
f3 := func(sctx context.Context, s State, opts ...StateOption) (State, error) {
|
||||
_, ok := logger.FromContext(sctx)
|
||||
if !ok {
|
||||
t.Fatal("f3 context does not have logger")
|
||||
}
|
||||
return StateEnd, map[string]interface{}{"response": "test_last"}, nil
|
||||
args := s.Body().(map[string]interface{})
|
||||
if v, ok := args["response"].(string); !ok || v == "" {
|
||||
return nil, fmt.Errorf("empty response")
|
||||
}
|
||||
return &state{name: StateEnd, body: map[string]interface{}{"response": "state3"}}, nil
|
||||
}
|
||||
f.State("1", f1)
|
||||
f.State("2", f2)
|
||||
f.State("3", f3)
|
||||
rsp, err := f.Start(ctx, map[string]interface{}{"request": "test1"})
|
||||
rsp, err := f.Start(ctx, map[string]interface{}{"request": "state"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := rsp.(map[string]interface{})
|
||||
if v, ok := args["response"].(string); !ok || v == "" {
|
||||
t.Fatalf("nil rsp: %#+v", args)
|
||||
} else if v != "test_last" {
|
||||
} else if v != "state3" {
|
||||
t.Fatalf("invalid rsp %#+v", args)
|
||||
}
|
||||
|
||||
if !bytes.Contains(buf.Bytes(), []byte(`before state 1`)) ||
|
||||
!bytes.Contains(buf.Bytes(), []byte(`before state 2`)) ||
|
||||
!bytes.Contains(buf.Bytes(), []byte(`after state 1`)) ||
|
||||
!bytes.Contains(buf.Bytes(), []byte(`after state 2`)) ||
|
||||
!bytes.Contains(buf.Bytes(), []byte(`after state 3`)) ||
|
||||
!bytes.Contains(buf.Bytes(), []byte(`after state 3`)) {
|
||||
t.Fatalf("fsm not works properly or hooks error, buf: %s", buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
52
fsm/options.go
Normal file
52
fsm/options.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package fsm
|
||||
|
||||
// Options struct holding fsm options
|
||||
type Options struct {
|
||||
// Initial state
|
||||
Initial string
|
||||
// Wrappers runs before state
|
||||
Wrappers []StateWrapper
|
||||
// DryRun mode
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// Option func signature
|
||||
type Option func(*Options)
|
||||
|
||||
// StateOptions holds state options
|
||||
type StateOptions struct {
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// StateDryRun says that state executes in dry run mode
|
||||
func StateDryRun(b bool) StateOption {
|
||||
return func(o *StateOptions) {
|
||||
o.DryRun = b
|
||||
}
|
||||
}
|
||||
|
||||
// StateOption func signature
|
||||
type StateOption func(*StateOptions)
|
||||
|
||||
// InitialState sets init state for state machine
|
||||
func InitialState(initial string) Option {
|
||||
return func(o *Options) {
|
||||
o.Initial = initial
|
||||
}
|
||||
}
|
||||
|
||||
// WrapState adds a state Wrapper to a list of options passed into the fsm
|
||||
func WrapState(w StateWrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.Wrappers = append(o.Wrappers, w)
|
||||
}
|
||||
}
|
||||
|
||||
// NewOptions returns new Options struct filled by passed Option
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return options
|
||||
}
|
Reference in New Issue
Block a user