Skip to content

Commit

Permalink
Merge pull request #178 from unistack-org/fsm
Browse files Browse the repository at this point in the history
fsm: improve and convert to interface
  • Loading branch information
vtolstov authored Jan 29, 2023
2 parents f6b7f1b + cb743ce commit 7462b0b
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 431 deletions.
126 changes: 126 additions & 0 deletions fsm/default.go
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 163 deletions fsm/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,179 +3,27 @@ package fsm // import "go.unistack.org/micro/v3/fsm"
import (
"context"
"errors"
"fmt"
"sync"
)

var (
ErrInvalidState = errors.New("does not exists")
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)

// 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()
type StateFunc func(ctx context.Context, state State, opts ...StateOption) (State, error)

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

0 comments on commit 7462b0b

Please sign in to comment.