Closed kylebrandt closed 3 years ago
@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:
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
}
Thank you very much! This is very helpful. I had made this transition table in a doc for myself yesterday:
So this really helps me understand how to map that (and I do have some more features in mind to explore that only add complexity, so this is very much appreciated!
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
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:
Simplified Example:
Events:
eGood
andeBad
. 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:sPending
wheneBad
event is received and the current state issGood
.sPending
,eGood
event would set the state tosGood
- basically a reset)What I am not clear on how a transition from
sPending
tosBad
could be done after either:eBad
events have been receivedThings 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.