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

View File

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