Skip to content

Commit

Permalink
Add SaleSlotReserving
Browse files Browse the repository at this point in the history
Adds a new state, SaleSlotReserving, that attempts to reserve a slot before downloading.
If the slot cannot be reserved, the state moves to SaleIgnored.
On error, the state moves to SaleErrored.

SaleIgnored is also updated to pass in `reprocessSlot` and `returnBytes`, controlling the behaviour in the Sales module after the slot is ignored. This is because previously it was assumed that SaleIgnored was only reached when there was no Availability. This is no longer the case, since SaleIgnored can now be reached when a slot cannot be reserved.
  • Loading branch information
emizzle committed Sep 26, 2024
1 parent fb68bc5 commit 189c1c4
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 5 deletions.
11 changes: 7 additions & 4 deletions codex/sales/states/ignored.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import ./errorhandling
logScope:
topics = "marketplace sales ignored"

# Ignored slots could mean there was no availability or that the slot could
# not be reserved.

type
SaleIgnored* = ref object of ErrorHandlingState
reprocessSlot*: bool # readd slot to queue with `seen` flag
returnBytes*: bool # return unreleased bytes from Reservation to Availability

method `$`*(state: SaleIgnored): string = "SaleIgnored"

method run*(state: SaleIgnored, machine: Machine): Future[?State] {.async.} =
let agent = SalesAgent(machine)

if onCleanUp =? agent.onCleanUp:
# Ignored slots mean there was no availability. In order to prevent small
# availabilities from draining the queue, mark this slot as seen and re-add
# back into the queue.
await onCleanUp(reprocessSlot = true)
await onCleanUp(reprocessSlot = state.reprocessSlot,
returnBytes = state.returnBytes)
62 changes: 62 additions & 0 deletions codex/sales/states/slotreserving.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pkg/questionable
import pkg/questionable/results
import pkg/metrics

import ../../logutils
import ../../market
import ../salesagent
import ../statemachine
import ./errorhandling
import ./cancelled
import ./failed
import ./filled
import ./ignored
import ./downloading
import ./errored

type
SaleSlotReserving* = ref object of ErrorHandlingState

logScope:
topics = "marketplace sales reserving"

method `$`*(state: SaleSlotReserving): string = "SaleSlotReserving"

method onCancelled*(state: SaleSlotReserving, request: StorageRequest): ?State =
return some State(SaleCancelled())

method onFailed*(state: SaleSlotReserving, request: StorageRequest): ?State =
return some State(SaleFailed())

method onSlotFilled*(state: SaleSlotReserving, requestId: RequestId,
slotIndex: UInt256): ?State =
return some State(SaleFilled())

method run*(state: SaleSlotReserving, machine: Machine): Future[?State] {.async.} =
let agent = SalesAgent(machine)
let data = agent.data
let context = agent.context
let market = context.market
let slotId = slotId(data.requestId, data.slotIndex)

logScope:
slotIndex = data.slotIndex
slotId

let canReserve = await market.canReserveSlot(slotId)
if canReserve:
try:
trace "Reserving slot"
await market.reserveSlot(slotId)
except MarketError as e:
return some State( SaleErrored(error: e) )

trace "Slot successfully reserved"
return some State( SaleDownloading() )

else:
# do not re-add this slot to the queue, and return bytes from Reservation to
# the Availability
debug "Slot cannot be reserved, ignoring"
return some State( SaleIgnored(reprocessSlot: false, returnBytes: true) )

19 changes: 18 additions & 1 deletion tests/codex/helpers/mockmarket.nim
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type
signer: Address
subscriptions: Subscriptions
config*: MarketplaceConfig
canReserveSlot*: bool
reserveSlotThrowError*: ?(ref MarketError)
Fulfillment* = object
requestId*: RequestId
proof*: Groth16Proof
Expand Down Expand Up @@ -105,7 +107,7 @@ proc new*(_: type MockMarket): MockMarket =
downtimeProduct: 67.uint8
)
)
MockMarket(signer: Address.example, config: config)
MockMarket(signer: Address.example, config: config, canReserveSlot: true)

method getSigner*(market: MockMarket): Future[Address] {.async.} =
return market.signer
Expand Down Expand Up @@ -303,6 +305,21 @@ method canProofBeMarkedAsMissing*(market: MockMarket,
period: Period): Future[bool] {.async.} =
return market.canBeMarkedAsMissing.contains(id)

method reserveSlot*(market: MockMarket, id: SlotId) {.async.} =
if error =? market.reserveSlotThrowError:
raise error

method canReserveSlot*(market: MockMarket, id: SlotId): Future[bool] {.async.} =
return market.canReserveSlot

func setCanReserveSlot*(market: MockMarket, canReserveSlot: bool) =
market.canReserveSlot = canReserveSlot

func setReserveSlotThrowError*(
market: MockMarket, error: ?(ref MarketError)) =

market.reserveSlotThrowError = error

method subscribeRequests*(market: MockMarket,
callback: OnRequest):
Future[Subscription] {.async.} =
Expand Down
75 changes: 75 additions & 0 deletions tests/codex/sales/states/testslotreserving.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import pkg/chronos
import pkg/questionable
import pkg/datastore
import pkg/stew/byteutils
import pkg/codex/contracts/requests
import pkg/codex/sales/states/slotreserving
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/mockreservations
import ../../helpers/mockclock

asyncchecksuite "sales state 'preparing'":
let request = StorageRequest.example
let slotIndex = (request.ask.slots div 2).u256
var market: MockMarket
var clock: MockClock
var agent: SalesAgent
var state: SaleSlotReserving
var context: SalesContext

setup:
market = MockMarket.new()
clock = MockClock.new()

state = SaleSlotReserving.new()
context = SalesContext(
market: market,
clock: clock
)

agent = newSalesAgent(context,
request.id,
slotIndex,
request.some)

test "switches to cancelled state when request expires":
let next = state.onCancelled(request)
check !next of SaleCancelled

test "switches to failed state when request fails":
let next = state.onFailed(request)
check !next of SaleFailed

test "switches to filled state when slot is filled":
let next = state.onSlotFilled(request.id, slotIndex)
check !next of SaleFilled

test "run switches to downloading when slot successfully reserved":
let next = await state.run(agent)
check !next of SaleDownloading

test "run switches to ignored when slot reservation not allowed":
market.setCanReserveSlot(false)
let next = await state.run(agent)
check !next of SaleIgnored

test "run switches to errored when slot reservation errors":
let error = newException(MarketError, "some error")
market.setReserveSlotThrowError(some error)
let next = !(await state.run(agent))
check next of SaleErrored
let errored = SaleErrored(next)
check errored.error == error
1 change: 1 addition & 0 deletions tests/codex/sales/teststates.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ import ./states/testcancelled
import ./states/testerrored
import ./states/testignored
import ./states/testpreparing
import ./states/testslotreserving

{.warning[UnusedImport]: off.}

0 comments on commit 189c1c4

Please sign in to comment.