Cleanup purchasing state machine (#422)
* [state machine] Allow querying of state properties * [purchasing] use new state machine * [state machine] remove old state machine implementation * [purchasing] remove duplication in error handling
This commit is contained in:
parent
3e7ce137a4
commit
3181361658
|
@ -44,10 +44,10 @@ func new*(_: type Purchase,
|
||||||
return purchase
|
return purchase
|
||||||
|
|
||||||
proc start*(purchase: Purchase) =
|
proc start*(purchase: Purchase) =
|
||||||
purchase.switch(PurchasePending())
|
purchase.start(PurchasePending())
|
||||||
|
|
||||||
proc load*(purchase: Purchase) =
|
proc load*(purchase: Purchase) =
|
||||||
purchase.switch(PurchaseUnknown())
|
purchase.start(PurchaseUnknown())
|
||||||
|
|
||||||
proc wait*(purchase: Purchase) {.async.} =
|
proc wait*(purchase: Purchase) {.async.} =
|
||||||
await purchase.future
|
await purchase.future
|
||||||
|
@ -63,3 +63,8 @@ func error*(purchase: Purchase): ?(ref CatchableError) =
|
||||||
some purchase.future.error
|
some purchase.future.error
|
||||||
else:
|
else:
|
||||||
none (ref CatchableError)
|
none (ref CatchableError)
|
||||||
|
|
||||||
|
func state*(purchase: Purchase): ?string =
|
||||||
|
proc description(state: State): string =
|
||||||
|
$state
|
||||||
|
purchase.query(description)
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
import ../utils/statemachine
|
import ../utils/asyncstatemachine
|
||||||
import ../market
|
import ../market
|
||||||
import ../clock
|
import ../clock
|
||||||
import ../errors
|
import ../errors
|
||||||
|
|
||||||
export market
|
export market
|
||||||
export clock
|
export clock
|
||||||
export statemachine
|
export asyncstatemachine
|
||||||
|
|
||||||
type
|
type
|
||||||
Purchase* = ref object of StateMachine
|
Purchase* = ref object of Machine
|
||||||
future*: Future[void]
|
future*: Future[void]
|
||||||
market*: Market
|
market*: Market
|
||||||
clock*: Clock
|
clock*: Clock
|
||||||
requestId*: RequestId
|
requestId*: RequestId
|
||||||
request*: ?StorageRequest
|
request*: ?StorageRequest
|
||||||
PurchaseState* = ref object of AsyncState
|
PurchaseState* = ref object of State
|
||||||
PurchaseError* = object of CodexError
|
PurchaseError* = object of CodexError
|
||||||
|
|
||||||
method description*(state: PurchaseState): string {.base.} =
|
|
||||||
raiseAssert "description not implemented for state"
|
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
import ../statemachine
|
import ../statemachine
|
||||||
|
import ./errorhandling
|
||||||
import ./error
|
import ./error
|
||||||
|
|
||||||
type PurchaseCancelled* = ref object of PurchaseState
|
type PurchaseCancelled* = ref object of ErrorHandlingState
|
||||||
|
|
||||||
method enterAsync*(state: PurchaseCancelled) {.async.} =
|
method `$`*(state: PurchaseCancelled): string =
|
||||||
without purchase =? (state.context as Purchase):
|
|
||||||
raiseAssert "invalid state"
|
|
||||||
|
|
||||||
try:
|
|
||||||
await purchase.market.withdrawFunds(purchase.requestId)
|
|
||||||
except CatchableError as error:
|
|
||||||
state.switch(PurchaseErrored(error: error))
|
|
||||||
return
|
|
||||||
|
|
||||||
let error = newException(Timeout, "Purchase cancelled due to timeout")
|
|
||||||
state.switch(PurchaseErrored(error: error))
|
|
||||||
|
|
||||||
method description*(state: PurchaseCancelled): string =
|
|
||||||
"cancelled"
|
"cancelled"
|
||||||
|
|
||||||
|
method run*(state: PurchaseCancelled, machine: Machine): Future[?State] {.async.} =
|
||||||
|
let purchase = Purchase(machine)
|
||||||
|
await purchase.market.withdrawFunds(purchase.requestId)
|
||||||
|
let error = newException(Timeout, "Purchase cancelled due to timeout")
|
||||||
|
return some State(PurchaseErrored(error: error))
|
||||||
|
|
|
@ -3,11 +3,9 @@ import ../statemachine
|
||||||
type PurchaseErrored* = ref object of PurchaseState
|
type PurchaseErrored* = ref object of PurchaseState
|
||||||
error*: ref CatchableError
|
error*: ref CatchableError
|
||||||
|
|
||||||
method enter*(state: PurchaseErrored) =
|
method `$`*(state: PurchaseErrored): string =
|
||||||
without purchase =? (state.context as Purchase):
|
|
||||||
raiseAssert "invalid state"
|
|
||||||
|
|
||||||
purchase.future.fail(state.error)
|
|
||||||
|
|
||||||
method description*(state: PurchaseErrored): string =
|
|
||||||
"errored"
|
"errored"
|
||||||
|
|
||||||
|
method run*(state: PurchaseErrored, machine: Machine): Future[?State] {.async.} =
|
||||||
|
let purchase = Purchase(machine)
|
||||||
|
purchase.future.fail(state.error)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import pkg/questionable
|
||||||
|
import ../statemachine
|
||||||
|
import ./error
|
||||||
|
|
||||||
|
type
|
||||||
|
ErrorHandlingState* = ref object of PurchaseState
|
||||||
|
|
||||||
|
method onError*(state: ErrorHandlingState, error: ref CatchableError): ?State =
|
||||||
|
some State(PurchaseErrored(error: error))
|
|
@ -4,9 +4,9 @@ import ./error
|
||||||
type
|
type
|
||||||
PurchaseFailed* = ref object of PurchaseState
|
PurchaseFailed* = ref object of PurchaseState
|
||||||
|
|
||||||
method enter*(state: PurchaseFailed) =
|
method `$`*(state: PurchaseFailed): string =
|
||||||
let error = newException(PurchaseError, "Purchase failed")
|
|
||||||
state.switch(PurchaseErrored(error: error))
|
|
||||||
|
|
||||||
method description*(state: PurchaseFailed): string =
|
|
||||||
"failed"
|
"failed"
|
||||||
|
|
||||||
|
method run*(state: PurchaseFailed, machine: Machine): Future[?State] {.async.} =
|
||||||
|
let error = newException(PurchaseError, "Purchase failed")
|
||||||
|
return some State(PurchaseErrored(error: error))
|
||||||
|
|
|
@ -2,11 +2,9 @@ import ../statemachine
|
||||||
|
|
||||||
type PurchaseFinished* = ref object of PurchaseState
|
type PurchaseFinished* = ref object of PurchaseState
|
||||||
|
|
||||||
method enter*(state: PurchaseFinished) =
|
method `$`*(state: PurchaseFinished): string =
|
||||||
without purchase =? (state.context as Purchase):
|
|
||||||
raiseAssert "invalid state"
|
|
||||||
|
|
||||||
purchase.future.complete()
|
|
||||||
|
|
||||||
method description*(state: PurchaseFinished): string =
|
|
||||||
"finished"
|
"finished"
|
||||||
|
|
||||||
|
method run*(state: PurchaseFinished, machine: Machine): Future[?State] {.async.} =
|
||||||
|
let purchase = Purchase(machine)
|
||||||
|
purchase.future.complete()
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
import ../statemachine
|
import ../statemachine
|
||||||
|
import ./errorhandling
|
||||||
import ./submitted
|
import ./submitted
|
||||||
import ./error
|
import ./error
|
||||||
|
|
||||||
type PurchasePending* = ref object of PurchaseState
|
type PurchasePending* = ref object of ErrorHandlingState
|
||||||
|
|
||||||
method enterAsync(state: PurchasePending) {.async.} =
|
method `$`*(state: PurchasePending): string =
|
||||||
without purchase =? (state.context as Purchase) and
|
|
||||||
request =? purchase.request:
|
|
||||||
raiseAssert "invalid state"
|
|
||||||
|
|
||||||
try:
|
|
||||||
await purchase.market.requestStorage(request)
|
|
||||||
except CatchableError as error:
|
|
||||||
state.switch(PurchaseErrored(error: error))
|
|
||||||
return
|
|
||||||
|
|
||||||
state.switch(PurchaseSubmitted())
|
|
||||||
|
|
||||||
method description*(state: PurchasePending): string =
|
|
||||||
"pending"
|
"pending"
|
||||||
|
|
||||||
|
method run*(state: PurchasePending, machine: Machine): Future[?State] {.async.} =
|
||||||
|
let purchase = Purchase(machine)
|
||||||
|
let request = !purchase.request
|
||||||
|
await purchase.market.requestStorage(request)
|
||||||
|
return some State(PurchaseSubmitted())
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import ../statemachine
|
import ../statemachine
|
||||||
|
import ./errorhandling
|
||||||
import ./error
|
import ./error
|
||||||
import ./finished
|
import ./finished
|
||||||
import ./failed
|
import ./failed
|
||||||
|
|
||||||
type PurchaseStarted* = ref object of PurchaseState
|
type PurchaseStarted* = ref object of ErrorHandlingState
|
||||||
|
|
||||||
method enterAsync*(state: PurchaseStarted) {.async.} =
|
method `$`*(state: PurchaseStarted): string =
|
||||||
without purchase =? (state.context as Purchase):
|
"started"
|
||||||
raiseAssert "invalid state"
|
|
||||||
|
method run*(state: PurchaseStarted, machine: Machine): Future[?State] {.async.} =
|
||||||
|
let purchase = Purchase(machine)
|
||||||
|
|
||||||
let clock = purchase.clock
|
let clock = purchase.clock
|
||||||
let market = purchase.market
|
let market = purchase.market
|
||||||
|
@ -18,17 +21,11 @@ method enterAsync*(state: PurchaseStarted) {.async.} =
|
||||||
let subscription = await market.subscribeRequestFailed(purchase.requestId, callback)
|
let subscription = await market.subscribeRequestFailed(purchase.requestId, callback)
|
||||||
|
|
||||||
let ended = clock.waitUntil(await market.getRequestEnd(purchase.requestId))
|
let ended = clock.waitUntil(await market.getRequestEnd(purchase.requestId))
|
||||||
try:
|
let fut = await one(ended, failed)
|
||||||
let fut = await one(ended, failed)
|
await subscription.unsubscribe()
|
||||||
if fut.id == failed.id:
|
if fut.id == failed.id:
|
||||||
ended.cancel()
|
ended.cancel()
|
||||||
state.switch(PurchaseFailed())
|
return some State(PurchaseFailed())
|
||||||
else:
|
else:
|
||||||
failed.cancel()
|
failed.cancel()
|
||||||
state.switch(PurchaseFinished())
|
return some State(PurchaseFinished())
|
||||||
await subscription.unsubscribe()
|
|
||||||
except CatchableError as error:
|
|
||||||
state.switch(PurchaseErrored(error: error))
|
|
||||||
|
|
||||||
method description*(state: PurchaseStarted): string =
|
|
||||||
"started"
|
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import ../statemachine
|
import ../statemachine
|
||||||
|
import ./errorhandling
|
||||||
import ./error
|
import ./error
|
||||||
import ./started
|
import ./started
|
||||||
import ./cancelled
|
import ./cancelled
|
||||||
|
|
||||||
type PurchaseSubmitted* = ref object of PurchaseState
|
type PurchaseSubmitted* = ref object of ErrorHandlingState
|
||||||
|
|
||||||
method enterAsync(state: PurchaseSubmitted) {.async.} =
|
method `$`*(state: PurchaseSubmitted): string =
|
||||||
without purchase =? (state.context as Purchase) and
|
"submitted"
|
||||||
request =? purchase.request:
|
|
||||||
raiseAssert "invalid state"
|
|
||||||
|
|
||||||
|
method run*(state: PurchaseSubmitted, machine: Machine): Future[?State] {.async.} =
|
||||||
|
let purchase = Purchase(machine)
|
||||||
|
let request = !purchase.request
|
||||||
let market = purchase.market
|
let market = purchase.market
|
||||||
let clock = purchase.clock
|
let clock = purchase.clock
|
||||||
|
|
||||||
|
@ -28,13 +30,6 @@ method enterAsync(state: PurchaseSubmitted) {.async.} =
|
||||||
try:
|
try:
|
||||||
await wait().withTimeout()
|
await wait().withTimeout()
|
||||||
except Timeout:
|
except Timeout:
|
||||||
state.switch(PurchaseCancelled())
|
return some State(PurchaseCancelled())
|
||||||
return
|
|
||||||
except CatchableError as error:
|
|
||||||
state.switch(PurchaseErrored(error: error))
|
|
||||||
return
|
|
||||||
|
|
||||||
state.switch(PurchaseStarted())
|
return some State(PurchaseStarted())
|
||||||
|
|
||||||
method description*(state: PurchaseSubmitted): string =
|
|
||||||
"submitted"
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import ../statemachine
|
import ../statemachine
|
||||||
|
import ./errorhandling
|
||||||
import ./submitted
|
import ./submitted
|
||||||
import ./started
|
import ./started
|
||||||
import ./cancelled
|
import ./cancelled
|
||||||
|
@ -6,32 +7,26 @@ import ./finished
|
||||||
import ./failed
|
import ./failed
|
||||||
import ./error
|
import ./error
|
||||||
|
|
||||||
type PurchaseUnknown* = ref object of PurchaseState
|
type PurchaseUnknown* = ref object of ErrorHandlingState
|
||||||
|
|
||||||
method enterAsync(state: PurchaseUnknown) {.async.} =
|
method `$`*(state: PurchaseUnknown): string =
|
||||||
without purchase =? (state.context as Purchase):
|
|
||||||
raiseAssert "invalid state"
|
|
||||||
|
|
||||||
try:
|
|
||||||
if (request =? await purchase.market.getRequest(purchase.requestId)) and
|
|
||||||
(requestState =? await purchase.market.requestState(purchase.requestId)):
|
|
||||||
|
|
||||||
purchase.request = some request
|
|
||||||
|
|
||||||
case requestState
|
|
||||||
of RequestState.New:
|
|
||||||
state.switch(PurchaseSubmitted())
|
|
||||||
of RequestState.Started:
|
|
||||||
state.switch(PurchaseStarted())
|
|
||||||
of RequestState.Cancelled:
|
|
||||||
state.switch(PurchaseCancelled())
|
|
||||||
of RequestState.Finished:
|
|
||||||
state.switch(PurchaseFinished())
|
|
||||||
of RequestState.Failed:
|
|
||||||
state.switch(PurchaseFailed())
|
|
||||||
|
|
||||||
except CatchableError as error:
|
|
||||||
state.switch(PurchaseErrored(error: error))
|
|
||||||
|
|
||||||
method description*(state: PurchaseUnknown): string =
|
|
||||||
"unknown"
|
"unknown"
|
||||||
|
|
||||||
|
method run*(state: PurchaseUnknown, machine: Machine): Future[?State] {.async.} =
|
||||||
|
let purchase = Purchase(machine)
|
||||||
|
if (request =? await purchase.market.getRequest(purchase.requestId)) and
|
||||||
|
(requestState =? await purchase.market.requestState(purchase.requestId)):
|
||||||
|
|
||||||
|
purchase.request = some request
|
||||||
|
|
||||||
|
case requestState
|
||||||
|
of RequestState.New:
|
||||||
|
return some State(PurchaseSubmitted())
|
||||||
|
of RequestState.Started:
|
||||||
|
return some State(PurchaseStarted())
|
||||||
|
of RequestState.Cancelled:
|
||||||
|
return some State(PurchaseCancelled())
|
||||||
|
of RequestState.Finished:
|
||||||
|
return some State(PurchaseFinished())
|
||||||
|
of RequestState.Failed:
|
||||||
|
return some State(PurchaseFailed())
|
||||||
|
|
|
@ -57,7 +57,7 @@ func `%`*(id: RequestId | SlotId | Nonce | AvailabilityId): JsonNode =
|
||||||
|
|
||||||
func `%`*(purchase: Purchase): JsonNode =
|
func `%`*(purchase: Purchase): JsonNode =
|
||||||
%*{
|
%*{
|
||||||
"state": (purchase.state as PurchaseState).?description |? "none",
|
"state": purchase.state |? "none",
|
||||||
"error": purchase.error.?msg,
|
"error": purchase.error.?msg,
|
||||||
"request": purchase.request,
|
"request": purchase.request,
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ type
|
||||||
scheduling: Future[void]
|
scheduling: Future[void]
|
||||||
started: bool
|
started: bool
|
||||||
State* = ref object of RootObj
|
State* = ref object of RootObj
|
||||||
|
Query*[T] = proc(state: State): T
|
||||||
Event* = proc(state: State): ?State {.gcsafe, upraises:[].}
|
Event* = proc(state: State): ?State {.gcsafe, upraises:[].}
|
||||||
|
|
||||||
logScope:
|
logScope:
|
||||||
|
@ -26,6 +27,12 @@ proc transition(_: type Event, previous, next: State): Event =
|
||||||
if state == previous:
|
if state == previous:
|
||||||
return some next
|
return some next
|
||||||
|
|
||||||
|
proc query*[T](machine: Machine, query: Query[T]): ?T =
|
||||||
|
if machine.state == nil:
|
||||||
|
none T
|
||||||
|
else:
|
||||||
|
some query(machine.state)
|
||||||
|
|
||||||
proc schedule*(machine: Machine, event: Event) =
|
proc schedule*(machine: Machine, event: Event) =
|
||||||
if not machine.started:
|
if not machine.started:
|
||||||
return
|
return
|
||||||
|
@ -90,4 +97,5 @@ proc stop*(machine: Machine) =
|
||||||
if not machine.running.isNil:
|
if not machine.running.isNil:
|
||||||
machine.running.cancel()
|
machine.running.cancel()
|
||||||
|
|
||||||
|
machine.state = nil
|
||||||
machine.started = false
|
machine.started = false
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
import std/typetraits
|
|
||||||
import pkg/chronicles
|
|
||||||
import pkg/questionable
|
|
||||||
import pkg/chronos
|
|
||||||
import ./optionalcast
|
|
||||||
|
|
||||||
## Implementation of the the state pattern:
|
|
||||||
## https://en.wikipedia.org/wiki/State_pattern
|
|
||||||
##
|
|
||||||
## Define your own state machine and state types:
|
|
||||||
##
|
|
||||||
## type
|
|
||||||
## Light = ref object of StateMachine
|
|
||||||
## color: string
|
|
||||||
## LightState = ref object of State
|
|
||||||
##
|
|
||||||
## let light = Light(color: "yellow")
|
|
||||||
##
|
|
||||||
## Define the states:
|
|
||||||
##
|
|
||||||
## type
|
|
||||||
## On = ref object of LightState
|
|
||||||
## Off = ref object of LightState
|
|
||||||
##
|
|
||||||
## Perform actions on state entry and exit:
|
|
||||||
##
|
|
||||||
## method enter(state: On) =
|
|
||||||
## echo light.color, " light switched on"
|
|
||||||
##
|
|
||||||
## method exit(state: On) =
|
|
||||||
## echo light.color, " light no longer switched on"
|
|
||||||
##
|
|
||||||
## light.switch(On()) # prints: 'light switched on'
|
|
||||||
## light.switch(Off()) # prints: 'light no longer switched on'
|
|
||||||
##
|
|
||||||
## Allow behaviour to change based on the current state:
|
|
||||||
##
|
|
||||||
## method description*(state: LightState): string {.base.} =
|
|
||||||
## return "a light"
|
|
||||||
##
|
|
||||||
## method description*(state: On): string =
|
|
||||||
## if light =? (state.context as Light):
|
|
||||||
## return "a " & light.color & " light"
|
|
||||||
##
|
|
||||||
## method description*(state: Off): string =
|
|
||||||
## return "a dark light"
|
|
||||||
##
|
|
||||||
## proc description*(light: Light): string =
|
|
||||||
## if state =? (light.state as LightState):
|
|
||||||
## return state.description
|
|
||||||
##
|
|
||||||
## light.switch(On())
|
|
||||||
## echo light.description # prints: 'a yellow light'
|
|
||||||
## light.switch(Off())
|
|
||||||
## echo light.description # prints 'a dark light'
|
|
||||||
|
|
||||||
|
|
||||||
export questionable
|
|
||||||
export optionalcast
|
|
||||||
|
|
||||||
type
|
|
||||||
StateMachine* = ref object of RootObj
|
|
||||||
state: ?State
|
|
||||||
State* = ref object of RootObj
|
|
||||||
context: ?StateMachine
|
|
||||||
|
|
||||||
method `$`*(state: State): string {.base.} =
|
|
||||||
(typeof state).name
|
|
||||||
|
|
||||||
method enter(state: State) {.base.} =
|
|
||||||
discard
|
|
||||||
|
|
||||||
method exit(state: State) {.base.} =
|
|
||||||
discard
|
|
||||||
|
|
||||||
func state*(machine: StateMachine): ?State =
|
|
||||||
machine.state
|
|
||||||
|
|
||||||
func context*(state: State): ?StateMachine =
|
|
||||||
state.context
|
|
||||||
|
|
||||||
proc switch*(machine: StateMachine, newState: State) =
|
|
||||||
if state =? machine.state:
|
|
||||||
state.exit()
|
|
||||||
state.context = StateMachine.none
|
|
||||||
machine.state = newState.some
|
|
||||||
newState.context = machine.some
|
|
||||||
newState.enter()
|
|
||||||
|
|
||||||
proc switch*(oldState, newState: State) =
|
|
||||||
if context =? oldState.context:
|
|
||||||
context.switch(newState)
|
|
||||||
|
|
||||||
type
|
|
||||||
AsyncState* = ref object of State
|
|
||||||
activeTransition: ?Future[void]
|
|
||||||
StateMachineAsync* = ref object of StateMachine
|
|
||||||
|
|
||||||
method enterAsync(state: AsyncState) {.base, async.} =
|
|
||||||
discard
|
|
||||||
|
|
||||||
method exitAsync(state: AsyncState) {.base, async.} =
|
|
||||||
discard
|
|
||||||
|
|
||||||
method enter(state: AsyncState) =
|
|
||||||
asyncSpawn state.enterAsync()
|
|
||||||
|
|
||||||
method exit(state: AsyncState) =
|
|
||||||
asyncSpawn state.exitAsync()
|
|
||||||
|
|
||||||
proc switchAsync*(machine: StateMachineAsync, newState: AsyncState) {.async.} =
|
|
||||||
if state =? (machine.state as AsyncState):
|
|
||||||
trace "Switching sales state", `from` = $state, to = $newState
|
|
||||||
if activeTransition =? state.activeTransition and
|
|
||||||
not activeTransition.completed:
|
|
||||||
await activeTransition.cancelAndWait()
|
|
||||||
# should wait for exit before switch. could add a transition option during
|
|
||||||
# switch if we don't need to wait
|
|
||||||
await state.exitAsync()
|
|
||||||
state.context = none StateMachine
|
|
||||||
else:
|
|
||||||
trace "Switching state", `from` = "no state", to = $newState
|
|
||||||
|
|
||||||
machine.state = some State(newState)
|
|
||||||
newState.context = some StateMachine(machine)
|
|
||||||
newState.activeTransition = some newState.enterAsync()
|
|
||||||
|
|
||||||
proc switchAsync*(oldState, newState: AsyncState) {.async.} =
|
|
||||||
if context =? oldState.context:
|
|
||||||
await StateMachineAsync(context).switchAsync(newState)
|
|
|
@ -3,7 +3,12 @@ import pkg/asynctest
|
||||||
import pkg/chronos
|
import pkg/chronos
|
||||||
import pkg/stint
|
import pkg/stint
|
||||||
import pkg/codex/purchasing
|
import pkg/codex/purchasing
|
||||||
import pkg/codex/purchasing/states/[finished, error, started, submitted, unknown]
|
import pkg/codex/purchasing/states/finished
|
||||||
|
import pkg/codex/purchasing/states/started
|
||||||
|
import pkg/codex/purchasing/states/submitted
|
||||||
|
import pkg/codex/purchasing/states/unknown
|
||||||
|
import pkg/codex/purchasing/states/cancelled
|
||||||
|
import pkg/codex/purchasing/states/failed
|
||||||
import ./helpers/mockmarket
|
import ./helpers/mockmarket
|
||||||
import ./helpers/mockclock
|
import ./helpers/mockclock
|
||||||
import ./helpers/eventually
|
import ./helpers/eventually
|
||||||
|
@ -31,11 +36,11 @@ suite "Purchasing":
|
||||||
|
|
||||||
test "submits a storage request when asked":
|
test "submits a storage request when asked":
|
||||||
discard await purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
let submitted = market.requested[0]
|
check eventually market.requested.len > 0
|
||||||
check submitted.ask.slots == request.ask.slots
|
check market.requested[0].ask.slots == request.ask.slots
|
||||||
check submitted.ask.slotSize == request.ask.slotSize
|
check market.requested[0].ask.slotSize == request.ask.slotSize
|
||||||
check submitted.ask.duration == request.ask.duration
|
check market.requested[0].ask.duration == request.ask.duration
|
||||||
check submitted.ask.reward == request.ask.reward
|
check market.requested[0].ask.reward == request.ask.reward
|
||||||
|
|
||||||
test "remembers purchases":
|
test "remembers purchases":
|
||||||
let purchase1 = await purchasing.purchase(request)
|
let purchase1 = await purchasing.purchase(request)
|
||||||
|
@ -49,11 +54,13 @@ suite "Purchasing":
|
||||||
test "can change default value for proof probability":
|
test "can change default value for proof probability":
|
||||||
purchasing.proofProbability = 42.u256
|
purchasing.proofProbability = 42.u256
|
||||||
discard await purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
check market.requested[0].ask.proofProbability == 42.u256
|
check market.requested[0].ask.proofProbability == 42.u256
|
||||||
|
|
||||||
test "can override proof probability per request":
|
test "can override proof probability per request":
|
||||||
request.ask.proofProbability = 42.u256
|
request.ask.proofProbability = 42.u256
|
||||||
discard await purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
check market.requested[0].ask.proofProbability == 42.u256
|
check market.requested[0].ask.proofProbability == 42.u256
|
||||||
|
|
||||||
test "has a default value for request expiration interval":
|
test "has a default value for request expiration interval":
|
||||||
|
@ -63,25 +70,30 @@ suite "Purchasing":
|
||||||
purchasing.requestExpiryInterval = 42.u256
|
purchasing.requestExpiryInterval = 42.u256
|
||||||
let start = getTime().toUnix()
|
let start = getTime().toUnix()
|
||||||
discard await purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
check market.requested[0].expiry == (start + 42).u256
|
check market.requested[0].expiry == (start + 42).u256
|
||||||
|
|
||||||
test "can override expiry time per request":
|
test "can override expiry time per request":
|
||||||
let expiry = (getTime().toUnix() + 42).u256
|
let expiry = (getTime().toUnix() + 42).u256
|
||||||
request.expiry = expiry
|
request.expiry = expiry
|
||||||
discard await purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
check market.requested[0].expiry == expiry
|
check market.requested[0].expiry == expiry
|
||||||
|
|
||||||
test "includes a random nonce in every storage request":
|
test "includes a random nonce in every storage request":
|
||||||
discard await purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
discard await purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
check market.requested[0].nonce != market.requested[1].nonce
|
check market.requested[0].nonce != market.requested[1].nonce
|
||||||
|
|
||||||
test "sets client address in request":
|
test "sets client address in request":
|
||||||
discard await purchasing.purchase(request)
|
discard await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
check market.requested[0].client == await market.getSigner()
|
check market.requested[0].client == await market.getSigner()
|
||||||
|
|
||||||
test "succeeds when request is finished":
|
test "succeeds when request is finished":
|
||||||
let purchase = await purchasing.purchase(request)
|
let purchase = await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
let request = market.requested[0]
|
let request = market.requested[0]
|
||||||
let requestEnd = getTime().toUnix() + 42
|
let requestEnd = getTime().toUnix() + 42
|
||||||
market.requestEnds[request.id] = requestEnd
|
market.requestEnds[request.id] = requestEnd
|
||||||
|
@ -92,6 +104,7 @@ suite "Purchasing":
|
||||||
|
|
||||||
test "fails when request times out":
|
test "fails when request times out":
|
||||||
let purchase = await purchasing.purchase(request)
|
let purchase = await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
let request = market.requested[0]
|
let request = market.requested[0]
|
||||||
clock.set(request.expiry.truncate(int64))
|
clock.set(request.expiry.truncate(int64))
|
||||||
expect PurchaseTimeout:
|
expect PurchaseTimeout:
|
||||||
|
@ -99,6 +112,7 @@ suite "Purchasing":
|
||||||
|
|
||||||
test "checks that funds were withdrawn when purchase times out":
|
test "checks that funds were withdrawn when purchase times out":
|
||||||
let purchase = await purchasing.purchase(request)
|
let purchase = await purchasing.purchase(request)
|
||||||
|
check eventually market.requested.len > 0
|
||||||
let request = market.requested[0]
|
let request = market.requested[0]
|
||||||
clock.set(request.expiry.truncate(int64))
|
clock.set(request.expiry.truncate(int64))
|
||||||
expect PurchaseTimeout:
|
expect PurchaseTimeout:
|
||||||
|
@ -150,20 +164,20 @@ suite "Purchasing state machine":
|
||||||
market.requestEnds[request2.id] = clock.now() - 1
|
market.requestEnds[request2.id] = clock.now() - 1
|
||||||
|
|
||||||
await purchasing.load()
|
await purchasing.load()
|
||||||
check purchasing.getPurchase(PurchaseId(request1.id)).?finished == false.some
|
check eventually purchasing.getPurchase(PurchaseId(request1.id)).?finished == false.some
|
||||||
check purchasing.getPurchase(PurchaseId(request2.id)).?finished == true.some
|
check eventually purchasing.getPurchase(PurchaseId(request2.id)).?finished == true.some
|
||||||
check purchasing.getPurchase(PurchaseId(request3.id)).?finished == true.some
|
check eventually purchasing.getPurchase(PurchaseId(request3.id)).?finished == true.some
|
||||||
check purchasing.getPurchase(PurchaseId(request4.id)).?finished == true.some
|
check eventually purchasing.getPurchase(PurchaseId(request4.id)).?finished == true.some
|
||||||
check purchasing.getPurchase(PurchaseId(request5.id)).?finished == true.some
|
check eventually purchasing.getPurchase(PurchaseId(request5.id)).?finished == true.some
|
||||||
check purchasing.getPurchase(PurchaseId(request5.id)).?error.isSome
|
check eventually purchasing.getPurchase(PurchaseId(request5.id)).?error.isSome
|
||||||
|
|
||||||
test "moves to PurchaseSubmitted when request state is New":
|
test "moves to PurchaseSubmitted when request state is New":
|
||||||
let request = StorageRequest.example
|
let request = StorageRequest.example
|
||||||
let purchase = Purchase.new(request, market, clock)
|
let purchase = Purchase.new(request, market, clock)
|
||||||
market.requested = @[request]
|
market.requested = @[request]
|
||||||
market.requestState[request.id] = RequestState.New
|
market.requestState[request.id] = RequestState.New
|
||||||
purchase.switch(PurchaseUnknown())
|
let next = await PurchaseUnknown().run(purchase)
|
||||||
check (purchase.state as PurchaseSubmitted).isSome
|
check !next of PurchaseSubmitted
|
||||||
|
|
||||||
test "moves to PurchaseStarted when request state is Started":
|
test "moves to PurchaseStarted when request state is Started":
|
||||||
let request = StorageRequest.example
|
let request = StorageRequest.example
|
||||||
|
@ -171,69 +185,51 @@ suite "Purchasing state machine":
|
||||||
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
||||||
market.requested = @[request]
|
market.requested = @[request]
|
||||||
market.requestState[request.id] = RequestState.Started
|
market.requestState[request.id] = RequestState.Started
|
||||||
purchase.switch(PurchaseUnknown())
|
let next = await PurchaseUnknown().run(purchase)
|
||||||
check (purchase.state as PurchaseStarted).isSome
|
check !next of PurchaseStarted
|
||||||
|
|
||||||
test "moves to PurchaseErrored when request state is Cancelled":
|
test "moves to PurchaseCancelled when request state is Cancelled":
|
||||||
let request = StorageRequest.example
|
let request = StorageRequest.example
|
||||||
let purchase = Purchase.new(request, market, clock)
|
let purchase = Purchase.new(request, market, clock)
|
||||||
market.requested = @[request]
|
market.requested = @[request]
|
||||||
market.requestState[request.id] = RequestState.Cancelled
|
market.requestState[request.id] = RequestState.Cancelled
|
||||||
purchase.switch(PurchaseUnknown())
|
let next = await PurchaseUnknown().run(purchase)
|
||||||
check (purchase.state as PurchaseErrored).isSome
|
check !next of PurchaseCancelled
|
||||||
check purchase.error.?msg == "Purchase cancelled due to timeout".some
|
|
||||||
|
|
||||||
test "moves to PurchaseFinished when request state is Finished":
|
test "moves to PurchaseFinished when request state is Finished":
|
||||||
let request = StorageRequest.example
|
let request = StorageRequest.example
|
||||||
let purchase = Purchase.new(request, market, clock)
|
let purchase = Purchase.new(request, market, clock)
|
||||||
market.requested = @[request]
|
market.requested = @[request]
|
||||||
market.requestState[request.id] = RequestState.Finished
|
market.requestState[request.id] = RequestState.Finished
|
||||||
purchase.switch(PurchaseUnknown())
|
let next = await PurchaseUnknown().run(purchase)
|
||||||
check (purchase.state as PurchaseFinished).isSome
|
check !next of PurchaseFinished
|
||||||
|
|
||||||
test "moves to PurchaseErrored when request state is Failed":
|
test "moves to PurchaseFailed when request state is Failed":
|
||||||
let request = StorageRequest.example
|
let request = StorageRequest.example
|
||||||
let purchase = Purchase.new(request, market, clock)
|
let purchase = Purchase.new(request, market, clock)
|
||||||
market.requested = @[request]
|
market.requested = @[request]
|
||||||
market.requestState[request.id] = RequestState.Failed
|
market.requestState[request.id] = RequestState.Failed
|
||||||
purchase.switch(PurchaseUnknown())
|
let next = await PurchaseUnknown().run(purchase)
|
||||||
check (purchase.state as PurchaseErrored).isSome
|
check !next of PurchaseFailed
|
||||||
check purchase.error.?msg == "Purchase failed".some
|
|
||||||
|
|
||||||
test "moves to PurchaseErrored state once RequestFailed emitted":
|
test "moves to PurchaseFailed state once RequestFailed emitted":
|
||||||
let me = await market.getSigner()
|
|
||||||
let request = StorageRequest.example
|
let request = StorageRequest.example
|
||||||
market.requested = @[request]
|
let purchase = Purchase.new(request, market, clock)
|
||||||
market.activeRequests[me] = @[request.id]
|
|
||||||
market.requestState[request.id] = RequestState.Started
|
|
||||||
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
||||||
await purchasing.load()
|
let future = PurchaseStarted().run(purchase)
|
||||||
|
|
||||||
# emit mock contract failure event
|
|
||||||
market.emitRequestFailed(request.id)
|
market.emitRequestFailed(request.id)
|
||||||
# must allow time for the callback to trigger the completion of the future
|
|
||||||
await sleepAsync(chronos.milliseconds(10))
|
|
||||||
|
|
||||||
# now check the result
|
let next = await future
|
||||||
let purchase = purchasing.getPurchase(PurchaseId(request.id))
|
check !next of PurchaseFailed
|
||||||
let state = purchase.?state
|
|
||||||
check (state as PurchaseErrored).isSome
|
|
||||||
check (!purchase).error.?msg == "Purchase failed".some
|
|
||||||
|
|
||||||
test "moves to PurchaseFinished state once request finishes":
|
test "moves to PurchaseFinished state once request finishes":
|
||||||
let me = await market.getSigner()
|
|
||||||
let request = StorageRequest.example
|
let request = StorageRequest.example
|
||||||
market.requested = @[request]
|
let purchase = Purchase.new(request, market, clock)
|
||||||
market.activeRequests[me] = @[request.id]
|
|
||||||
market.requestState[request.id] = RequestState.Started
|
|
||||||
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
|
||||||
await purchasing.load()
|
let future = PurchaseStarted().run(purchase)
|
||||||
|
|
||||||
# advance the clock to the end of the request
|
|
||||||
clock.advance(request.ask.duration.truncate(int64))
|
clock.advance(request.ask.duration.truncate(int64))
|
||||||
|
|
||||||
# now check the result
|
let next = await future
|
||||||
proc requestState: ?PurchaseState =
|
check !next of PurchaseFinished
|
||||||
purchasing.getPurchase(PurchaseId(request.id)).?state as PurchaseState
|
|
||||||
|
|
||||||
check eventually (requestState() as PurchaseFinished).isSome
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import ./utils/teststatemachine
|
|
||||||
import ./utils/teststatemachineasync
|
|
||||||
import ./utils/testoptionalcast
|
import ./utils/testoptionalcast
|
||||||
import ./utils/testkeyutils
|
import ./utils/testkeyutils
|
||||||
import ./utils/testasyncstatemachine
|
import ./utils/testasyncstatemachine
|
||||||
|
|
|
@ -113,3 +113,21 @@ suite "async state machines":
|
||||||
machine.schedule(moveToNextStateEvent)
|
machine.schedule(moveToNextStateEvent)
|
||||||
check eventually cancellations == [0, 1, 0, 0]
|
check eventually cancellations == [0, 1, 0, 0]
|
||||||
check errors == [0, 0, 0, 0]
|
check errors == [0, 0, 0, 0]
|
||||||
|
|
||||||
|
test "queries properties of the current state":
|
||||||
|
proc description(state: State): string =
|
||||||
|
$state
|
||||||
|
|
||||||
|
machine.start(State2.new())
|
||||||
|
check eventually machine.query(description) == some "State2"
|
||||||
|
machine.schedule(moveToNextStateEvent)
|
||||||
|
check eventually machine.query(description) == some "State3"
|
||||||
|
|
||||||
|
test "stops handling queries when stopped":
|
||||||
|
proc description(state: State): string =
|
||||||
|
$state
|
||||||
|
|
||||||
|
machine.start(State2.new())
|
||||||
|
check eventually machine.query(description).isSome
|
||||||
|
machine.stop()
|
||||||
|
check machine.query(description).isNone
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import std/unittest
|
|
||||||
import pkg/questionable
|
|
||||||
import codex/utils/statemachine
|
|
||||||
|
|
||||||
type
|
|
||||||
Light = ref object of StateMachine
|
|
||||||
On = ref object of State
|
|
||||||
Off = ref object of State
|
|
||||||
|
|
||||||
var enteredOn: bool
|
|
||||||
var exitedOn: bool
|
|
||||||
|
|
||||||
method enter(state: On) =
|
|
||||||
enteredOn = true
|
|
||||||
|
|
||||||
method exit(state: On) =
|
|
||||||
exitedOn = true
|
|
||||||
|
|
||||||
suite "state machines":
|
|
||||||
|
|
||||||
setup:
|
|
||||||
enteredOn = false
|
|
||||||
exitedOn = false
|
|
||||||
|
|
||||||
test "calls `enter` when entering state":
|
|
||||||
Light().switch(On())
|
|
||||||
check enteredOn
|
|
||||||
|
|
||||||
test "calls `exit` when exiting state":
|
|
||||||
let light = Light()
|
|
||||||
light.switch(On())
|
|
||||||
check not exitedOn
|
|
||||||
light.switch(Off())
|
|
||||||
check exitedOn
|
|
||||||
|
|
||||||
test "allows access to state machine from state":
|
|
||||||
let light = Light()
|
|
||||||
let on = On()
|
|
||||||
check not isSome on.context
|
|
||||||
light.switch(on)
|
|
||||||
check on.context == some StateMachine(light)
|
|
||||||
|
|
||||||
test "removes access to state machine when state exited":
|
|
||||||
let light = Light()
|
|
||||||
let on = On()
|
|
||||||
light.switch(on)
|
|
||||||
light.switch(Off())
|
|
||||||
check not isSome on.context
|
|
|
@ -1,30 +0,0 @@
|
||||||
import pkg/asynctest
|
|
||||||
import pkg/chronos
|
|
||||||
import pkg/questionable
|
|
||||||
import codex/utils/statemachine
|
|
||||||
|
|
||||||
type
|
|
||||||
AsyncMachine = ref object of StateMachineAsync
|
|
||||||
LongRunningStart = ref object of AsyncState
|
|
||||||
LongRunningFinish = ref object of AsyncState
|
|
||||||
LongRunningError = ref object of AsyncState
|
|
||||||
Callback = proc(): Future[void] {.gcsafe.}
|
|
||||||
|
|
||||||
proc triggerIn(time: Duration, cb: Callback) {.async.} =
|
|
||||||
await sleepAsync(time)
|
|
||||||
await cb()
|
|
||||||
|
|
||||||
method enterAsync(state: LongRunningStart) {.async.} =
|
|
||||||
proc cb() {.async.} =
|
|
||||||
await state.switchAsync(LongRunningFinish())
|
|
||||||
asyncSpawn triggerIn(500.milliseconds, cb)
|
|
||||||
await sleepAsync(1.seconds)
|
|
||||||
await state.switchAsync(LongRunningError())
|
|
||||||
|
|
||||||
suite "async state machines":
|
|
||||||
|
|
||||||
test "can cancel a state":
|
|
||||||
let am = AsyncMachine()
|
|
||||||
await am.switchAsync(LongRunningStart())
|
|
||||||
await sleepAsync(2.seconds)
|
|
||||||
check (am.state as LongRunningFinish).isSome
|
|
Loading…
Reference in New Issue