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
|
||||
SalePreparing* = ref object of ErrorHandlingState
|
||||
# I really don't want to keep this. Someone, please help.
|
||||
doThing*: proc(): Future[void] {.async, gcsafe.}
|
||||
|
||||
logScope:
|
||||
topics = "marketplace sales preparing"
|
||||
|
@ -31,7 +33,7 @@ method onSlotFilled*(state: SalePreparing, requestId: RequestId,
|
|||
slotIndex: UInt256): ?State =
|
||||
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 data = agent.data
|
||||
let context = agent.context
|
||||
|
@ -67,17 +69,25 @@ method run*(state: SalePreparing, machine: Machine): Future[?State] {.async.} =
|
|||
request.ask.pricePerSlot,
|
||||
request.ask.collateral):
|
||||
debug "no availability found for request, ignoring"
|
||||
|
||||
return some State(SaleIgnored())
|
||||
|
||||
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(
|
||||
availability.id,
|
||||
request.ask.slotSize,
|
||||
request.id,
|
||||
data.slotIndex
|
||||
), 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))
|
||||
|
||||
data.reservation = some reservation
|
||||
|
|
|
@ -1,21 +1,62 @@
|
|||
import std/unittest
|
||||
import pkg/chronos
|
||||
import pkg/questionable
|
||||
import pkg/datastore
|
||||
import pkg/stew/byteutils
|
||||
import pkg/codex/contracts/requests
|
||||
import pkg/codex/sales/states/preparing
|
||||
import pkg/codex/sales/states/downloading
|
||||
import pkg/codex/sales/states/cancelled
|
||||
import pkg/codex/sales/states/failed
|
||||
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 ../../helpers/mockmarket
|
||||
import ../../helpers/mockclock
|
||||
|
||||
suite "sales state 'preparing'":
|
||||
|
||||
asyncchecksuite "sales state 'preparing'":
|
||||
let request = StorageRequest.example
|
||||
let slotIndex = (request.ask.slots div 2).u256
|
||||
let market = MockMarket.new()
|
||||
let clock = MockClock.new()
|
||||
var agent: SalesAgent
|
||||
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()
|
||||
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":
|
||||
let next = state.onCancelled(request)
|
||||
|
@ -28,3 +69,40 @@ suite "sales state 'preparing'":
|
|||
test "switches to filled state when slot is filled":
|
||||
let next = state.onSlotFilled(request.id, slotIndex)
|
||||
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