How to handle State timers or counters?
kylebrandt opened this issue · 4 comments
I am trying to figure out how I might implement something along the lines of "stay in state for X duration" or "stay in state for X consecutive events". I do not have a lot of experience with state machines, so I'm wondering if:
- What I am trying to do is no longer a finite state machine (so this library is probably not what I want to use)
- It is a FSM, but this library isn't good for this particular task
- This library is good, you might do it like …?
Simplified Example:
Events: eGood
and eBad
. A scheduler is running, sending either a good/bad event at mostly regular intervals. These events are the input to the state machine.
States: sGood
, sBad
, sPending
.
My confusion is around the sPending
state. The idea of the pending state is that it is a hold for a time duration where consecutive sBad events have been received, before going into the sBad state and performing some action.
sPending
Transitions:
- Transition into the pending state
sPending
wheneBad
event is received and the current state issGood
. - (When in
sPending
,eGood
event would set the state tosGood
- basically a reset)
What I am not clear on how a transition from sPending
to sBad
could be done after either:
- A certain number of consecutive
eBad
events have been received - A certain amount of time has passed
Things Considered:
A) If using the count method, I could create Pending1, Pending2, Pending3, etc state constants, but this feels cumbersome and wouldn't work as well with time durations I don't think.
B) I can imagine that on receiving a eBad
while in the pending state I could start a timer, or create a counter. However, my concern is that I am creating a piece (the timer) of information that is detached from State and StateMachine objects, but does impact the transition behavior of the state machine - and this will get me into trouble.
@kylebrandt your use case can be modelled by a FSM as the states are finite and the transitions are well defined. It can also be easily implemented using stateless
in multiple ways.
What you are describing, talking in terms of a [https://en.wikipedia.org/wiki/UML_state_machine](UML state machine), is an state machine with:
- Three events: [eGood, eBad, eTimeout]
- Three states: [sGood, sBad, sPending]
- Two extended states: [badCounter, timer]
- Two guard conditions: [fewBads, tooManyBads]
- sPending entry action that starts timer countdown
- sPending exit action that ends timer countdown
- Run-to-completion execution model (concurrent events will be FIFO queued)
And the transition diagram could look like:
Going to the implementation, it is always recommended to wrap the state machine in an object that just expose the business logic API, in your case Good()
and Bad()
so all the finite state machine nuance is hidden to the caller. This wrapper will be also responsible of owning the extended states. It could look like:
const (
maxBads = 3
countdownDuration = 3 * time.Second
)
type SM struct {
fsm *stateless.StateMachine
badCount int
cancelTimer chan struct{}
}
func (sm *SM) Bad() {
sm.badCount++
sm.fsm.Fire(eBad)
}
func (sm *SM) Good() {
sm.badCount = 0
sm.fsm.Fire(eGood)
}
func (sm *SM) initCountdown(_ context.Context, _ ...interface{}) error {
go func() {
select {
case <-time.After(countdownDuration):
sm.fsm.Fire(eTimeOut)
case <-sm.cancelTimer:
}
}()
return nil
}
func (sm *SM) stopCountdown(_ context.Context, _ ...interface{}) error {
select {
case sm.cancelTimer <- struct{}{}:
default:
}
return nil
}
func (sm *SM) fewBads(_ context.Context, _ ...interface{}) bool {
return sm.badCount <= maxBads
}
func (sm *SM) tooManyBads(_ context.Context, arg_s ...interface{}) bool {
return !sm.fewBads(nil, nil)
}
Finally the configuration of the state machine would happen inside a the NewStateMachine
function:
const (
eGood = "GoodEvent"
eBad = "BadEvent"
eTimeOut = "TimeOutEvent"
sGood = "Good"
sBad = "Bad"
sPending = "Pending"
)
func NewStateMachine() *SM {
fsm := stateless.NewStateMachine(sGood)
sm := &SM{
fsm: fsm,
cancelTimer: make(chan struct{}),
}
fsm.Configure(sGood).
Ignore(eGood).
Permit(eBad, sPending)
fsm.Configure(sBad).
Ignore(eBad).
Permit(eGood, sGood)
fsm.Configure(sPending).
OnEntry(sm.initCountdown).
OnExit(sm.stopCountdown).
Permit(eGood, sGood).
Permit(eTimeOut, sBad).
Permit(eBad, sBad, sm.tooManyBads).
Ignore(eBad, sm.fewBads)
return sm
}
Always glad to help!
One more comment: If you need to trigger some action when the next state is the current state you can change the Ignore
configuration with InternalTransition
, which allows for custom actions without calling entry/exit actions, or even PermitReentry
to trigger entry/exit actions on every reentry.
Closing since answered <3