diff --git a/codex/clock.nim b/codex/clock.nim index 38ec7349..ab40eeb4 100644 --- a/codex/clock.nim +++ b/codex/clock.nim @@ -3,6 +3,7 @@ import pkg/chronos type Clock* = ref object of RootObj SecondsSince1970* = int64 + Timeout* = object of CatchableError method now*(clock: Clock): SecondsSince1970 {.base.} = raiseAssert "not implemented" @@ -10,3 +11,15 @@ method now*(clock: Clock): SecondsSince1970 {.base.} = proc waitUntil*(clock: Clock, time: SecondsSince1970) {.async.} = while clock.now() < time: await sleepAsync(1.seconds) + +proc withTimeout*(future: Future[void], + clock: Clock, + expiry: SecondsSince1970) {.async.} = + let timeout = clock.waitUntil(expiry) + try: + await future or timeout + finally: + await timeout.cancelAndWait() + if not future.completed: + await future.cancelAndWait() + raise newException(Timeout, "Timed out") diff --git a/codex/purchasing.nim b/codex/purchasing.nim index 1479e937..084ec9a4 100644 --- a/codex/purchasing.nim +++ b/codex/purchasing.nim @@ -25,6 +25,7 @@ type request*: StorageRequest offers*: seq[StorageOffer] selected*: ?StorageOffer + PurchaseTimeout* = Timeout const DefaultProofProbability = 100.u256 const DefaultRequestExpiryInterval = (10 * 60).u256 @@ -69,29 +70,30 @@ func getPurchase*(purchasing: Purchasing, id: array[32, byte]): ?Purchase = else: none Purchase -proc selectOffer(purchase: Purchase) {.async.} = - var cheapest: ?StorageOffer - for offer in purchase.offers: - without purchase.clock.now().u256 < offer.expiry - purchase.offerExpiryMargin: - continue - without current =? cheapest: - cheapest = some offer - continue - if current.price > offer.price: - cheapest = some offer - if cheapest =? cheapest: - await purchase.market.selectOffer(cheapest.id) - purchase.selected = some cheapest - proc run(purchase: Purchase) {.async.} = - proc onOffer(offer: StorageOffer) = - purchase.offers.add(offer) let market = purchase.market - purchase.request = await market.requestStorage(purchase.request) - let subscription = await market.subscribeOffers(purchase.request.id, onOffer) - await purchase.clock.waitUntil(purchase.request.expiry.truncate(int64)) - await purchase.selectOffer() - await subscription.unsubscribe() + let clock = purchase.clock + + proc requestStorage {.async.} = + purchase.request = await market.requestStorage(purchase.request) + + proc waitUntilFulfilled {.async.} = + let done = newFuture[void]() + proc callback(_: array[32, byte]) = + done.complete() + let request = purchase.request + let subscription = await market.subscribeFulfillment(request.id, callback) + try: + await done + finally: + await subscription.unsubscribe() + + proc withTimeout(future: Future[void]) {.async.} = + let expiry = purchase.request.expiry.truncate(int64) + await future.withTimeout(clock, expiry) + + await requestStorage() + await waitUntilFulfilled().withTimeout() proc start(purchase: Purchase) = purchase.future = purchase.run() diff --git a/tests/codex/testpurchasing.nim b/tests/codex/testpurchasing.nim index e4456593..e74f655d 100644 --- a/tests/codex/testpurchasing.nim +++ b/tests/codex/testpurchasing.nim @@ -25,13 +25,8 @@ suite "Purchasing": ) ) - proc purchaseAndWait(request: StorageRequest) {.async.} = - let purchase = purchasing.purchase(request) - clock.set(market.requested[^1].expiry.truncate(int64)) - await purchase.wait() - test "submits a storage request when asked": - await purchaseAndWait(request) + discard purchasing.purchase(request) let submitted = market.requested[0] check submitted.ask.duration == request.ask.duration check submitted.ask.size == request.ask.size @@ -48,12 +43,12 @@ suite "Purchasing": test "can change default value for proof probability": purchasing.proofProbability = 42.u256 - await purchaseAndWait(request) + discard purchasing.purchase(request) check market.requested[0].ask.proofProbability == 42.u256 test "can override proof probability per request": request.ask.proofProbability = 42.u256 - await purchaseAndWait(request) + discard purchasing.purchase(request) check market.requested[0].ask.proofProbability == 42.u256 test "has a default value for request expiration interval": @@ -62,66 +57,31 @@ suite "Purchasing": test "can change default value for request expiration interval": purchasing.requestExpiryInterval = 42.u256 let start = getTime().toUnix() - await purchaseAndWait(request) + discard purchasing.purchase(request) check market.requested[0].expiry == (start + 42).u256 test "can override expiry time per request": let expiry = (getTime().toUnix() + 42).u256 request.expiry = expiry - await purchaseAndWait(request) + discard purchasing.purchase(request) check market.requested[0].expiry == expiry test "includes a random nonce in every storage request": - await purchaseAndWait(request) - await purchaseAndWait(request) + discard purchasing.purchase(request) + discard purchasing.purchase(request) check market.requested[0].nonce != market.requested[1].nonce - proc createOffer(request: StorageRequest): StorageOffer = - StorageOffer( - requestId: request.id, - expiry: (getTime() + initDuration(hours = 1)).toUnix().u256 - ) - - test "selects the cheapest offer": + test "succeeds when request is fulfilled": let purchase = purchasing.purchase(request) let request = market.requested[0] - var offer1, offer2 = createOffer(request) - offer1.price = 20.u256 - offer2.price = 10.u256 - discard await market.offerStorage(offer1) - discard await market.offerStorage(offer2) - clock.set(request.expiry.truncate(int64)) + let proof = seq[byte].example + await market.fulfillRequest(request.id, proof) await purchase.wait() - check purchase.selected == some offer2 - check market.selected[0] == offer2.id + check purchase.error.isNone - test "ignores offers that expired": - let expired = (getTime() - initTimeInterval(hours = 1)).toUnix().u256 + test "fails when request times out": let purchase = purchasing.purchase(request) let request = market.requested[0] - var offer1, offer2 = request.createOffer() - offer1.price = 20.u256 - offer2.price = 10.u256 - offer2.expiry = expired - discard await market.offerStorage(offer1) - discard await market.offerStorage(offer2) clock.set(request.expiry.truncate(int64)) - await purchase.wait() - check market.selected[0] == offer1.id - - test "has a default expiration margin for offers": - check purchasing.offerExpiryMargin != 0.u256 - - test "ignores offers that are about to expire": - let expiryMargin = purchasing.offerExpiryMargin - let purchase = purchasing.purchase(request) - let request = market.requested[0] - var offer1, offer2 = request.createOffer() - offer1.price = 20.u256 - offer2.price = 10.u256 - offer2.expiry = getTime().toUnix().u256 + expiryMargin - 1 - discard await market.offerStorage(offer1) - discard await market.offerStorage(offer2) - clock.set(request.expiry.truncate(int64)) - await purchase.wait() - check market.selected[0] == offer1.id + expect PurchaseTimeout: + await purchase.wait()