Adds tests and fix for race condition in sales preparing state.

This commit is contained in:
Ben 2024-05-28 15:17:42 +02:00
parent a4f4104cf0
commit a321cb2dd4
No known key found for this signature in database
GPG Key ID: 541B9D8C9F1426A1
2 changed files with 94 additions and 6 deletions

View File

@ -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

View File

@ -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()
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