Adds tests and fix for race condition in sales preparing state.
This commit is contained in:
parent
a4f4104cf0
commit
a321cb2dd4
|
@ -15,6 +15,8 @@ import ./errored
|
||||||
|
|
||||||
type
|
type
|
||||||
SalePreparing* = ref object of ErrorHandlingState
|
SalePreparing* = ref object of ErrorHandlingState
|
||||||
|
# I really don't want to keep this. Someone, please help.
|
||||||
|
doThing*: proc(): Future[void] {.async, gcsafe.}
|
||||||
|
|
||||||
logScope:
|
logScope:
|
||||||
topics = "marketplace sales preparing"
|
topics = "marketplace sales preparing"
|
||||||
|
@ -31,7 +33,7 @@ method onSlotFilled*(state: SalePreparing, requestId: RequestId,
|
||||||
slotIndex: UInt256): ?State =
|
slotIndex: UInt256): ?State =
|
||||||
return some State(SaleFilled())
|
return some State(SaleFilled())
|
||||||
|
|
||||||
method run*(state: SalePreparing, machine: Machine): Future[?State] {.async.} =
|
method run*(prepareState: SalePreparing, machine: Machine): Future[?State] {.async.} =
|
||||||
let agent = SalesAgent(machine)
|
let agent = SalesAgent(machine)
|
||||||
let data = agent.data
|
let data = agent.data
|
||||||
let context = agent.context
|
let context = agent.context
|
||||||
|
@ -67,17 +69,25 @@ method run*(state: SalePreparing, machine: Machine): Future[?State] {.async.} =
|
||||||
request.ask.pricePerSlot,
|
request.ask.pricePerSlot,
|
||||||
request.ask.collateral):
|
request.ask.collateral):
|
||||||
debug "no availability found for request, ignoring"
|
debug "no availability found for request, ignoring"
|
||||||
|
|
||||||
return some State(SaleIgnored())
|
return some State(SaleIgnored())
|
||||||
|
|
||||||
info "availability found for request, creating reservation"
|
info "availability found for request, creating reservation"
|
||||||
|
|
||||||
|
# I need a hook to mess with the reservations AFTER the availability is found.
|
||||||
|
if not isNil(prepareState.doThing):
|
||||||
|
await prepareState.doThing()
|
||||||
|
|
||||||
without reservation =? await reservations.createReservation(
|
without reservation =? await reservations.createReservation(
|
||||||
availability.id,
|
availability.id,
|
||||||
request.ask.slotSize,
|
request.ask.slotSize,
|
||||||
request.id,
|
request.id,
|
||||||
data.slotIndex
|
data.slotIndex
|
||||||
), error:
|
), error:
|
||||||
|
# Race condition:
|
||||||
|
# reservations.findAvailability (line 65) is no guarantee. You can never know for certain that the reservation can be created until after you have it.
|
||||||
|
# Should createReservation fail because there's no space, we proceed to SaleIgnored.
|
||||||
|
if error of BytesOutOfBoundsError:
|
||||||
|
return some State(SaleIgnored())
|
||||||
return some State(SaleErrored(error: error))
|
return some State(SaleErrored(error: error))
|
||||||
|
|
||||||
data.reservation = some reservation
|
data.reservation = some reservation
|
||||||
|
|
|
@ -1,21 +1,62 @@
|
||||||
import std/unittest
|
import pkg/chronos
|
||||||
import pkg/questionable
|
import pkg/questionable
|
||||||
|
import pkg/datastore
|
||||||
|
import pkg/stew/byteutils
|
||||||
import pkg/codex/contracts/requests
|
import pkg/codex/contracts/requests
|
||||||
import pkg/codex/sales/states/preparing
|
import pkg/codex/sales/states/preparing
|
||||||
import pkg/codex/sales/states/downloading
|
import pkg/codex/sales/states/downloading
|
||||||
import pkg/codex/sales/states/cancelled
|
import pkg/codex/sales/states/cancelled
|
||||||
import pkg/codex/sales/states/failed
|
import pkg/codex/sales/states/failed
|
||||||
import pkg/codex/sales/states/filled
|
import pkg/codex/sales/states/filled
|
||||||
|
import pkg/codex/sales/states/ignored
|
||||||
|
import pkg/codex/sales/states/errored
|
||||||
|
import pkg/codex/sales/salesagent
|
||||||
|
import pkg/codex/sales/salescontext
|
||||||
|
import pkg/codex/sales/reservations
|
||||||
|
import pkg/codex/stores/repostore
|
||||||
|
import ../../../asynctest
|
||||||
|
import ../../helpers
|
||||||
import ../../examples
|
import ../../examples
|
||||||
|
import ../../helpers/mockmarket
|
||||||
|
import ../../helpers/mockclock
|
||||||
|
|
||||||
suite "sales state 'preparing'":
|
asyncchecksuite "sales state 'preparing'":
|
||||||
|
|
||||||
let request = StorageRequest.example
|
let request = StorageRequest.example
|
||||||
let slotIndex = (request.ask.slots div 2).u256
|
let slotIndex = (request.ask.slots div 2).u256
|
||||||
|
let market = MockMarket.new()
|
||||||
|
let clock = MockClock.new()
|
||||||
|
var agent: SalesAgent
|
||||||
var state: SalePreparing
|
var state: SalePreparing
|
||||||
|
var repo: RepoStore
|
||||||
|
var availability: Availability
|
||||||
|
var context: SalesContext
|
||||||
|
|
||||||
|
setup:
|
||||||
|
availability = Availability(
|
||||||
|
totalSize: request.ask.slotSize + 100.u256,
|
||||||
|
freeSize: request.ask.slotSize + 100.u256,
|
||||||
|
duration: request.ask.duration + 60.u256,
|
||||||
|
minPrice: request.ask.pricePerSlot - 10.u256,
|
||||||
|
maxCollateral: request.ask.collateral + 400.u256
|
||||||
|
)
|
||||||
|
let repoDs = SQLiteDatastore.new(Memory).tryGet()
|
||||||
|
let metaDs = SQLiteDatastore.new(Memory).tryGet()
|
||||||
|
repo = RepoStore.new(repoDs, metaDs)
|
||||||
|
await repo.start()
|
||||||
|
|
||||||
setup:
|
|
||||||
state = SalePreparing.new()
|
state = SalePreparing.new()
|
||||||
|
context = SalesContext(
|
||||||
|
market: market,
|
||||||
|
clock: clock
|
||||||
|
)
|
||||||
|
context.reservations = Reservations.new(repo)
|
||||||
|
agent = newSalesAgent(context,
|
||||||
|
request.id,
|
||||||
|
slotIndex,
|
||||||
|
request.some)
|
||||||
|
|
||||||
|
teardown:
|
||||||
|
await repo.stop()
|
||||||
|
|
||||||
test "switches to cancelled state when request expires":
|
test "switches to cancelled state when request expires":
|
||||||
let next = state.onCancelled(request)
|
let next = state.onCancelled(request)
|
||||||
|
@ -28,3 +69,40 @@ suite "sales state 'preparing'":
|
||||||
test "switches to filled state when slot is filled":
|
test "switches to filled state when slot is filled":
|
||||||
let next = state.onSlotFilled(request.id, slotIndex)
|
let next = state.onSlotFilled(request.id, slotIndex)
|
||||||
check !next of SaleFilled
|
check !next of SaleFilled
|
||||||
|
|
||||||
|
proc createAvailability() =
|
||||||
|
let a = waitFor context.reservations.createAvailability(
|
||||||
|
availability.totalSize,
|
||||||
|
availability.duration,
|
||||||
|
availability.minPrice,
|
||||||
|
availability.maxCollateral
|
||||||
|
)
|
||||||
|
availability = a.get
|
||||||
|
|
||||||
|
test "run switches to ignored when no availability":
|
||||||
|
let next = await state.run(agent)
|
||||||
|
check !next of SaleIgnored
|
||||||
|
|
||||||
|
test "run switches to downloading when reserved":
|
||||||
|
createAvailability()
|
||||||
|
let next = await state.run(agent)
|
||||||
|
check !next of SaleDownloading
|
||||||
|
|
||||||
|
test "run switches to errored when reserve failed":
|
||||||
|
createAvailability()
|
||||||
|
state.doThing = proc(): Future[void] {.async, gcsafe.} =
|
||||||
|
# Mess up availability, causes createReservation to fail:
|
||||||
|
(await repo.metaDs.put(availability.id.key.tryGet(), "A!".toBytes())).tryGet()
|
||||||
|
|
||||||
|
let next = await state.run(agent)
|
||||||
|
check !next of SaleErrored
|
||||||
|
|
||||||
|
test "run switches to ignored when reserve fails with BytesOutOfBounds":
|
||||||
|
createAvailability()
|
||||||
|
state.doThing = proc(): Future[void] {.async, gcsafe.} =
|
||||||
|
# Oops we don't have any free bytes after all.
|
||||||
|
availability.freeSize = 0.u256
|
||||||
|
(await repo.metaDs.put(availability.id.key.tryGet(), availability.toJson.toBytes)).tryGet()
|
||||||
|
|
||||||
|
let next = await state.run(agent)
|
||||||
|
check !next of SaleIgnored
|
Loading…
Reference in New Issue