[statemachine] initial steps for declarative transitions

# Conflicts:
#	codex/utils/asyncstatemachine.nim
This commit is contained in:
Eric Mastro 2023-02-15 14:35:44 +11:00
parent 2035568b88
commit 127adc0b8a
No known key found for this signature in database
GPG Key ID: AD065ECE27A873B9
2 changed files with 63 additions and 17 deletions

View File

@ -2,36 +2,55 @@ import pkg/questionable
import pkg/chronos import pkg/chronos
import pkg/upraises import pkg/upraises
push: {.upraises:[].}
type type
Machine* = ref object of RootObj Machine* = ref object of RootObj
# TODO: context?
state: State state: State
running: Future[void] running: Future[void]
scheduled: AsyncQueue[Event] scheduled: AsyncQueue[Event]
scheduling: Future[void] scheduling: Future[void]
transitions: seq[Transition]
State* = ref object of RootObj State* = ref object of RootObj
Event* = proc(state: State): ?State {.gcsafe, upraises:[].} Event = proc(state: State): ?State {.gcsafe, upraises:[].}
TransitionCondition* = proc(machine: Machine, state: State): bool {.gcsafe, upraises:[].}
Transition* = object of RootObj
prevState: State
nextState: State
condition: TransitionCondition
TransitionProperty*[T] = ref object of RootObj
machine: Machine
value*: T
proc new*(T: type Transition,
prev, next: State,
condition: TransitionCondition): T =
Transition(prevState: prev, nextState: next, condition: condition)
proc newTransitionProperty*[T](self: Machine,
initialValue: T): TransitionProperty[T] =
TransitionProperty[T](machine: self, value: initialValue)
proc transition(_: type Event, previous, next: State): Event = proc transition(_: type Event, previous, next: State): Event =
return proc (state: State): ?State = return proc (state: State): ?State =
if state == previous: if state == previous:
return some next return some next
proc schedule*(machine: Machine, event: Event) = proc setValue*[T](prop: TransitionProperty[T], value: T) =
try: prop.value = value
machine.scheduled.putNoWait(event) let machine = prop.machine
except AsyncQueueFullError: for transition in machine.transitions:
raiseAssert "unlimited queue is full?!" if transition.condition(machine, machine.state) and
machine.state == transition.prevState:
machine.schedule(Event.transition(machine.state, transition.nextState))
# TODO: provide context instead of machine? proc schedule*(machine: Machine, event: Event) =
method run*(state: State, machine: Machine): Future[?State] {.base, upraises:[].} = machine.scheduled.putNoWait(event)
method run*(state: State): Future[?State] {.base, upraises:[].} =
discard discard
proc run(machine: Machine, state: State) {.async.} = proc run(machine: Machine, state: State) {.async.} =
try: try:
if next =? await state.run(machine): if next =? await state.run():
machine.schedule(Event.transition(state, next)) machine.schedule(Event.transition(state, next))
except CancelledError: except CancelledError:
discard discard
@ -50,11 +69,12 @@ proc scheduler(machine: Machine) {.async.} =
discard discard
proc start*(machine: Machine, initialState: State) = proc start*(machine: Machine, initialState: State) =
if machine.scheduled.isNil:
machine.scheduled = newAsyncQueue[Event]()
machine.scheduling = machine.scheduler() machine.scheduling = machine.scheduler()
machine.schedule(Event.transition(machine.state, initialState)) machine.schedule(Event.transition(machine.state, initialState))
proc stop*(machine: Machine) = proc stop*(machine: Machine) =
machine.scheduling.cancel() machine.scheduling.cancel()
machine.running.cancel() machine.running.cancel()
proc new*(T: type Machine, transitions: seq[Transition]): T =
T(scheduled: newAsyncQueue[Event](), transitions: transitions)

View File

@ -6,6 +6,8 @@ import codex/utils/asyncstatemachine
import ../helpers/eventually import ../helpers/eventually
type type
MyMachine = ref object of Machine
slotFilled: TransitionProperty[bool]
State1 = ref object of State State1 = ref object of State
State2 = ref object of State State2 = ref object of State
State3 = ref object of State State3 = ref object of State
@ -37,8 +39,8 @@ method onMoveToNextStateEvent(state: State3): ?State =
some State(State1.new()) some State(State1.new())
suite "async state machines": suite "async state machines":
var machine: Machine var machine: MyMachine
var state1, state2: State var state1, state2, state3: State
proc moveToNextStateEvent(state: State): ?State = proc moveToNextStateEvent(state: State): ?State =
state.onMoveToNextStateEvent() state.onMoveToNextStateEvent()
@ -46,9 +48,24 @@ suite "async state machines":
setup: setup:
runs = [0, 0, 0] runs = [0, 0, 0]
cancellations = [0, 0, 0] cancellations = [0, 0, 0]
machine = Machine.new()
state1 = State1.new() state1 = State1.new()
state2 = State2.new() state2 = State2.new()
state3 = State3.new()
machine = MyMachine.new(@[
Transition.new(
state3,
state1,
proc(m: Machine, s: State): bool =
MyMachine(m).slotFilled.value
)
])
machine.slotFilled = machine.newTransitionProperty(false)
# EXAMPLE USAGE ONLY -- can be removed from tests
# should represent a typical external event callback, ie event called via
# subscription
proc externalEventCallback() =# would take params (rid: RequestId, slotIdx: UInt256) =
machine.slotFilled.setValue(true)
test "should call run on start state": test "should call run on start state":
machine.start(state1) machine.start(state1)
@ -82,3 +99,12 @@ suite "async state machines":
await sleepAsync(1.millis) await sleepAsync(1.millis)
check runs == [0, 1, 0] check runs == [0, 1, 0]
check cancellations == [0, 1, 0] check cancellations == [0, 1, 0]
test "can transition to state without next state":
machine.start(state3)
check eventually runs == [0, 0, 1]
test "moves states based on declared transitions and conditions":
machine.start(state3)
machine.slotFilled.setValue(true)
check eventually runs == [1, 0, 1]