diff --git a/codex/sales/states/preparing.nim b/codex/sales/states/preparing.nim index 973446e2..5410c0c0 100644 --- a/codex/sales/states/preparing.nim +++ b/codex/sales/states/preparing.nim @@ -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 diff --git a/tests/codex/sales/states/testpreparing.nim b/tests/codex/sales/states/testpreparing.nim index df9a4c91..91650712 100644 --- a/tests/codex/sales/states/testpreparing.nim +++ b/tests/codex/sales/states/testpreparing.nim @@ -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 \ No newline at end of file