mirror of
https://github.com/codex-storage/nim-codex.git
synced 2025-02-18 15:58:08 +00:00
[statemachine] initial steps for declarative transitions
# Conflicts: # codex/utils/asyncstatemachine.nim
This commit is contained in:
parent
2035568b88
commit
127adc0b8a
@ -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)
|
||||||
|
@ -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]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user