1
1
package act
2
2
3
3
import (
4
+ "context"
4
5
"fmt"
5
6
"reflect"
6
7
"runtime"
@@ -75,40 +76,44 @@ type StateMachine[D any] struct {
75
76
// Callback that is invoked immediately after every state change. If no
76
77
// callback is registered stateEnterCallback is nil.
77
78
stateEnterCallback StateEnterCallback [D ]
79
+
80
+ // Pointer to the most recently configured state timeout.
81
+ activeStateTimeout * ActiveStateTimeout
78
82
}
79
83
80
84
type Action interface {
81
85
isAction ()
82
86
}
83
87
84
- type StateTimeout [ M any ] struct {
88
+ type StateTimeout struct {
85
89
Duration time.Duration
86
- message M
90
+ Message any
87
91
}
88
92
89
- func (StateTimeout [ M ]) IsAction () {}
93
+ func (StateTimeout ) isAction () {}
90
94
91
- // state_timeout
92
- // timeout
95
+ type ActiveStateTimeout struct {
96
+ state gen.Atom
97
+ timeout StateTimeout
98
+ ctx context.Context
99
+ cancel context.CancelFunc
100
+ }
93
101
94
102
// Type alias for MessageHandler callbacks.
95
103
// D is the type of the data associated with the StateMachine.
96
104
// M is the type of the message this handler accepts.
97
- type StateMessageHandler [D any , M any ] func (gen.Atom , D , M , gen.Process ) (gen.Atom , D , error )
98
-
99
- // new version with actions
100
- //type StateMessageHandler[D any, M any] func(gen.Atom, D, M, gen.Process) (gen.Atom, D, []Action, error)
105
+ type StateMessageHandler [D any , M any ] func (gen.Atom , D , M , gen.Process ) (gen.Atom , D , []Action , error )
101
106
102
107
// Type alias for CallHandler callbacks.
103
108
// D is the type of the data associated with the StateMachine.
104
109
// M is the type of the message this handler accepts.
105
110
// R is the type of the result value.
106
- type StateCallHandler [D any , M any , R any ] func (gen.Atom , D , M , gen.Process ) (gen.Atom , D , R , error )
111
+ type StateCallHandler [D any , M any , R any ] func (gen.Atom , D , M , gen.Process ) (gen.Atom , D , R , [] Action , error )
107
112
108
113
// Type alias for event handler callbacks.
109
114
// D is the type of the data associated with the StateMachine.
110
115
// E is the type of the event.
111
- type EventHandler [D any , E any ] func (gen.Atom , D , E , gen.Process ) (gen.Atom , D , error )
116
+ type EventHandler [D any , E any ] func (gen.Atom , D , E , gen.Process ) (gen.Atom , D , [] Action , error )
112
117
113
118
// Type alias for StateEnter callback.
114
119
// D is the type of the data associated with the StateMachine.
@@ -137,7 +142,6 @@ func NewStateMachineSpec[D any](initialState gen.Atom, options ...Option[D]) Sta
137
142
}
138
143
return spec
139
144
}
140
-
141
145
func WithData [D any ](data D ) Option [D ] {
142
146
return func (s * StateMachineSpec [D ]) {
143
147
s .data = data
@@ -182,10 +186,18 @@ func (s *StateMachine[D]) CurrentState() gen.Atom {
182
186
183
187
func (s * StateMachine [D ]) SetCurrentState (state gen.Atom ) {
184
188
if state != s .currentState {
185
- s .Log ().Info ("setting current state to %v " , state )
189
+ s .Log ().Info ("StateMachine: switching to state %s " , state )
186
190
oldState := s .currentState
187
191
s .currentState = state
188
192
193
+ // If there is a state timeout set up for the new state then we have
194
+ // just registered this timeout in `ProcessActions` and we should not
195
+ // touch it. Otherwise we should cancel the active state timeout if there
196
+ // is one.
197
+ if s .hasActiveStateTimeout () && s .activeStateTimeout .state != state {
198
+ s .Log ().Info ("StateMachine: canceling state timeout for state %s" , state )
199
+ s .activeStateTimeout .cancel ()
200
+ }
189
201
// Execute state enter callback until no new transition is triggered.
190
202
if s .stateEnterCallback != nil {
191
203
newState , newData , err := s .stateEnterCallback (oldState , state , s .data , s )
@@ -206,6 +218,10 @@ func (s *StateMachine[D]) SetData(data D) {
206
218
s .data = data
207
219
}
208
220
221
+ func (s * StateMachine [D ]) hasActiveStateTimeout () bool {
222
+ return s .activeStateTimeout != nil && s .activeStateTimeout .ctx .Err () == nil
223
+ }
224
+
209
225
type startMonitoringEvents struct {}
210
226
211
227
//
@@ -246,10 +262,12 @@ func (sm *StateMachine[D]) ProcessInit(process gen.Process, args ...any) (rr err
246
262
sm .eventHandlers = spec .eventHandlers
247
263
sm .stateEnterCallback = spec .stateEnterCallback
248
264
249
- // if we have event handlers we need to start listening for events
265
+ // Send a message to ourselves to start monitoring events if there are
266
+ // event handlers registerd.
250
267
if len (sm .eventHandlers ) > 0 {
251
268
sm .Send (sm .PID (), startMonitoringEvents {})
252
269
}
270
+ sm .Log ().Info ("StateMachine: started in state %s" , sm .currentState )
253
271
254
272
return nil
255
273
}
@@ -320,7 +338,7 @@ func (sm *StateMachine[D]) ProcessRun() (rr error) {
320
338
panic (fmt .Sprintf ("Error monitoring event: %v." , err ))
321
339
}
322
340
}
323
- sm .Log ().Info ("StateMachine %s is now monitoring events" , sm . PID () )
341
+ sm .Log ().Info ("StateMachine: monitoring events" )
324
342
return nil
325
343
326
344
default :
@@ -423,6 +441,43 @@ func (s *StateMachine[D]) Terminate(reason error) {}
423
441
// Internals
424
442
//
425
443
444
+ func (sm * StateMachine [D ]) ProcessActions (actions []Action , state gen.Atom ) {
445
+ for _ , action := range actions {
446
+ switch action := action .(type ) {
447
+ case StateTimeout :
448
+ if sm .hasActiveStateTimeout () {
449
+ sm .activeStateTimeout .cancel ()
450
+ }
451
+ ctx , cancel := context .WithTimeout (context .Background (), action .Duration )
452
+ sm .activeStateTimeout = & ActiveStateTimeout {
453
+ state : state ,
454
+ timeout : action ,
455
+ ctx : ctx ,
456
+ cancel : cancel ,
457
+ }
458
+ go startStateTimeout (ctx , state , action .Message , sm )
459
+ return
460
+ default :
461
+ panic ("unsupported action" )
462
+ }
463
+ }
464
+ }
465
+
466
+ func startStateTimeout (ctx context.Context , state gen.Atom , message any , proc gen.Process ) {
467
+ select {
468
+ case <- ctx .Done ():
469
+ switch ctx .Err () {
470
+ case context .DeadlineExceeded :
471
+ proc .Log ().Info ("StateMachine: state timeout for state %s timed out" , state )
472
+ proc .Send (proc .PID (), message )
473
+ return
474
+ case context .Canceled :
475
+ proc .Log ().Info ("StateMachine: state timeout for state %s canceled" , state )
476
+ return
477
+ }
478
+ }
479
+ }
480
+
426
481
func (sm * StateMachine [D ]) lookupMessageHandler (messageType string ) (any , bool ) {
427
482
if stateMessageHandlers , exists := sm .stateMessageHandlers [sm .currentState ]; exists == true {
428
483
if callback , exists := stateMessageHandlers [messageType ]; exists == true {
@@ -433,29 +488,20 @@ func (sm *StateMachine[D]) lookupMessageHandler(messageType string) (any, bool)
433
488
}
434
489
435
490
func (sm * StateMachine [D ]) invokeMessageHandler (handler any , message * gen.MailboxMessage ) error {
491
+ stateMachineValue := reflect .ValueOf (sm )
436
492
callbackValue := reflect .ValueOf (handler )
437
493
stateValue := reflect .ValueOf (sm .currentState )
438
494
dataValue := reflect .ValueOf (sm .Data ())
439
495
msgValue := reflect .ValueOf (message .Message )
440
- procValue := reflect .ValueOf ( sm )
496
+ messageType := reflect .TypeOf ( message ). String ( )
441
497
442
- results := callbackValue .Call ([]reflect.Value {stateValue , dataValue , msgValue , procValue })
498
+ results := callbackValue .Call ([]reflect.Value {stateValue , dataValue , msgValue , stateMachineValue })
443
499
444
- if len (results ) != 3 {
445
- sm .Log ().Panic ("StateMachine terminated. Panic reason: unexpected " +
446
- "error when invoking call handler for %s" , reflect .TypeOf (message .Message ))
447
- return gen .TerminateReasonPanic
448
- }
449
- if ! results [2 ].IsNil () {
450
- return results [2 ].Interface ().(error )
500
+ validateResultSize (results , 4 , messageType )
501
+ if isError , err := resultIsError (results ); isError == true {
502
+ return err
451
503
}
452
-
453
- setDataMethod := reflect .ValueOf (sm ).MethodByName ("SetData" )
454
- setDataMethod .Call ([]reflect.Value {results [1 ]})
455
- // It is important that we set the state last as this can potentially trigger
456
- // a state enter callback
457
- setCurrentStateMethod := reflect .ValueOf (sm ).MethodByName ("SetCurrentState" )
458
- setCurrentStateMethod .Call ([]reflect.Value {results [0 ]})
504
+ updateStateMachineWithResults (stateMachineValue , results )
459
505
460
506
return nil
461
507
}
@@ -470,63 +516,100 @@ func (sm *StateMachine[D]) lookupCallHandler(messageType string) (any, bool) {
470
516
}
471
517
472
518
func (sm * StateMachine [D ]) invokeCallHandler (handler any , message * gen.MailboxMessage ) (any , error ) {
519
+ stateMachineValue := reflect .ValueOf (sm )
473
520
callbackValue := reflect .ValueOf (handler )
474
521
stateValue := reflect .ValueOf (sm .currentState )
475
522
dataValue := reflect .ValueOf (sm .Data ())
476
523
msgValue := reflect .ValueOf (message .Message )
477
- procValue := reflect .ValueOf (sm )
478
-
479
- results := callbackValue .Call ([]reflect.Value {stateValue , dataValue , msgValue , procValue })
524
+ messageType := reflect .TypeOf (message ).String ()
480
525
481
- if len (results ) != 4 {
482
- sm .Log ().Panic ("StateMachine terminated. Panic reason: unexpected " +
483
- "error when invoking call handler for %s" , reflect .TypeOf (message .Message ))
484
- return nil , gen .TerminateReasonPanic
485
- }
526
+ results := callbackValue .Call ([]reflect.Value {stateValue , dataValue , msgValue , stateMachineValue })
486
527
487
- if ! results [ 3 ]. IsNil () {
488
- err := results [ 3 ]. Interface ().( error )
528
+ validateResultSize ( results , 5 , messageType )
529
+ if isError , err := resultIsError ( results ); isError == true {
489
530
return nil , err
490
531
}
491
-
492
- setDataMethod := reflect .ValueOf (sm ).MethodByName ("SetData" )
493
- setDataMethod .Call ([]reflect.Value {results [1 ]})
494
- // It is important that we set the state last as this can potentially trigger
495
- // a state enter callback
496
- setCurrentStateMethod := reflect .ValueOf (sm ).MethodByName ("SetCurrentState" )
497
- setCurrentStateMethod .Call ([]reflect.Value {results [0 ]})
498
-
532
+ updateStateMachineWithResults (stateMachineValue , results )
499
533
result := results [2 ].Interface ()
500
534
501
535
return result , nil
502
536
}
503
537
504
538
func (sm * StateMachine [D ]) invokeEventHandler (handler any , message * gen.MessageEvent ) error {
539
+ stateMachineValue := reflect .ValueOf (sm )
505
540
callbackValue := reflect .ValueOf (handler )
506
541
stateValue := reflect .ValueOf (sm .currentState )
507
542
dataValue := reflect .ValueOf (sm .Data ())
508
543
msgValue := reflect .ValueOf (message .Message )
509
- procValue := reflect .ValueOf ( sm )
544
+ messageType := reflect .TypeOf ( message ). String ( )
510
545
511
- results := callbackValue .Call ([]reflect.Value {stateValue , dataValue , msgValue , procValue })
546
+ results := callbackValue .Call ([]reflect.Value {stateValue , dataValue , msgValue , stateMachineValue })
512
547
513
- if len (results ) != 3 {
514
- sm .Log ().Panic ("StateMachine terminated. Panic reason: unexpected " +
515
- "error when invoking call handler for %s" , reflect .TypeOf (message .Message ))
516
- return gen .TerminateReasonPanic
548
+ validateResultSize (results , 4 , messageType )
549
+ if isError , err := resultIsError (results ); isError == true {
550
+ return err
517
551
}
552
+ updateStateMachineWithResults (stateMachineValue , results )
518
553
519
- if ! results [2 ].IsNil () {
520
- err := results [2 ].Interface ().(error )
521
- return err
554
+ return nil
555
+ }
556
+
557
+ func validateResultSize (results []reflect.Value , expectedSize int , messageType string ) {
558
+ if len (results ) != expectedSize {
559
+ panic (fmt .Sprintf ("StateMachine terminated. Panic reason: unexpected " +
560
+ "error when invoking call handler for %s" , messageType ))
522
561
}
562
+ }
523
563
524
- setDataMethod := reflect .ValueOf (sm ).MethodByName ("SetData" )
564
+ func resultIsError (results []reflect.Value ) (bool , error ) {
565
+ errIndex := len (results ) - 1
566
+ if ! results [errIndex ].IsNil () {
567
+ err := results [errIndex ].Interface ().(error )
568
+ return true , err
569
+ }
570
+ return false , nil
571
+ }
572
+
573
+ func updateStateMachineWithResults (sm reflect.Value , results []reflect.Value ) {
574
+ // Check if any actions were returned. MessageHandler and EventHandler have
575
+ // the result tuple (gen.Atom, D, []Action, error) with the actions at index
576
+ // 2. CallHandler has the result typle (gen.Atom, D, R, []Action, error)
577
+ // with the actions at index 3.
578
+ var actionsIndex int
579
+ hasResult := len (results ) == 5
580
+ if hasResult {
581
+ actionsIndex = 3
582
+ } else {
583
+ actionsIndex = 2
584
+ }
585
+ if ! isSliceNilOrEmpty (results [actionsIndex ]) {
586
+ processActionsMethod := sm .MethodByName ("ProcessActions" )
587
+ if processActionsMethod .IsNil () {
588
+ }
589
+ processActionsMethod .Call ([]reflect.Value {results [actionsIndex ], results [0 ]})
590
+ }
591
+
592
+ // Update the data
593
+ setDataMethod := sm .MethodByName ("SetData" )
525
594
setDataMethod .Call ([]reflect.Value {results [1 ]})
595
+
526
596
// It is important that we set the state last as this can potentially trigger
527
- // a state enter callback
528
- setCurrentStateMethod := reflect .ValueOf (sm ).MethodByName ("SetCurrentState" )
597
+ // a state enter callback. By design state enter callbacks are triggered
598
+ // after setting up state timeouts as state timeouts are tied to te state
599
+ // they are defined for. A state enter callback could transition to another
600
+ // state which then will cancel the state timeout.
601
+ setCurrentStateMethod := sm .MethodByName ("SetCurrentState" )
529
602
setCurrentStateMethod .Call ([]reflect.Value {results [0 ]})
603
+ }
530
604
531
- return nil
605
+ func isSliceNilOrEmpty (resultValue reflect.Value ) bool {
606
+ if resultValue .IsNil () {
607
+ return true
608
+ }
609
+
610
+ if resultValue .Len () == 0 {
611
+ return true
612
+ }
613
+
614
+ return false
532
615
}
0 commit comments