import std/sequtils import pkg/asynctest import pkg/chronicles import pkg/chronos import pkg/datastore import pkg/questionable import pkg/questionable/results import pkg/codex/sales/reservations import pkg/codex/sales/slotqueue import pkg/codex/stores import ../helpers/mockmarket import ../helpers/eventually import ../examples suite "Slot queue start/stop": var repo: RepoStore var repoDs: Datastore var metaDs: SQLiteDatastore var reservations: Reservations var queue: SlotQueue setup: repoDs = SQLiteDatastore.new(Memory).tryGet() metaDs = SQLiteDatastore.new(Memory).tryGet() repo = RepoStore.new(repoDs, metaDs) reservations = Reservations.new(repo) queue = SlotQueue.new(reservations) teardown: await queue.stop() test "starts out not running": check not queue.running test "can call start multiple times, and when already running": asyncSpawn queue.start() asyncSpawn queue.start() check queue.running test "can call stop when alrady stopped": await queue.stop() check not queue.running test "can call stop when running": asyncSpawn queue.start() await queue.stop() check not queue.running test "can call stop multiple times": asyncSpawn queue.start() await queue.stop() await queue.stop() check not queue.running suite "Slot queue workers": var repo: RepoStore var repoDs: Datastore var metaDs: SQLiteDatastore var availability: Availability var reservations: Reservations var queue: SlotQueue proc onProcessSlot(item: SlotQueueItem, doneProcessing: Future[void]) {.async.} = await sleepAsync(1000.millis) # this is not illustrative of the realistic scenario as the # `doneProcessing` future would be passed to another context before being # completed and therefore is not as simple as making the callback async doneProcessing.complete() setup: let request = StorageRequest.example repoDs = SQLiteDatastore.new(Memory).tryGet() metaDs = SQLiteDatastore.new(Memory).tryGet() let quota = request.ask.slotSize.truncate(uint) * 100 + 1 repo = RepoStore.new(repoDs, metaDs, quotaMaxBytes = quota) reservations = Reservations.new(repo) # create an availability that should always match availability = Availability.init( size = request.ask.slotSize * 100, duration = request.ask.duration * 100, minPrice = request.ask.pricePerSlot div 100, maxCollateral = request.ask.collateral * 100 ) queue = SlotQueue.new(reservations, maxSize = 5, maxWorkers = 3) queue.onProcessSlot = onProcessSlot discard await reservations.reserve(availability) proc startQueue = asyncSpawn queue.start() teardown: await queue.stop() test "activeWorkers should be 0 when not running": check queue.activeWorkers == 0 test "maxWorkers cannot be 0": expect ValueError: discard SlotQueue.new(reservations, maxSize = 1, maxWorkers = 0) test "maxWorkers cannot surpass maxSize": expect ValueError: discard SlotQueue.new(reservations, maxSize = 1, maxWorkers = 2) test "does not surpass max workers": startQueue() let item1 = SlotQueueItem.example let item2 = SlotQueueItem.example let item3 = SlotQueueItem.example let item4 = SlotQueueItem.example check (await queue.push(item1)).isOk check (await queue.push(item2)).isOk check (await queue.push(item3)).isOk check (await queue.push(item4)).isOk check eventually queue.activeWorkers == 3 test "discards workers once processing completed": proc processSlot(item: SlotQueueItem, done: Future[void]) {.async.} = await sleepAsync(1.millis) done.complete() queue.onProcessSlot = processSlot startQueue() let item1 = SlotQueueItem.example let item2 = SlotQueueItem.example let item3 = SlotQueueItem.example let item4 = SlotQueueItem.example check (await queue.push(item1)).isOk # finishes after 1.millis check (await queue.push(item2)).isOk # finishes after 1.millis check (await queue.push(item3)).isOk # finishes after 1.millis check (await queue.push(item4)).isOk check eventually queue.activeWorkers == 1 suite "Slot queue": var onProcessSlotCalled = false var onProcessSlotCalledWith: seq[(RequestId, uint16)] var repo: RepoStore var repoDs: Datastore var metaDs: SQLiteDatastore var availability: Availability var reservations: Reservations var queue: SlotQueue let maxWorkers = 2 var unpauseQueue: Future[void] var paused: bool proc newSlotQueue(maxSize, maxWorkers: int, processSlotDelay = 1.millis) = queue = SlotQueue.new(reservations, maxWorkers, maxSize.uint16) queue.onProcessSlot = proc(item: SlotQueueItem, done: Future[void]) {.async.} = await sleepAsync(processSlotDelay) trace "processing item", requestId = item.requestId, slotIndex = item.slotIndex onProcessSlotCalled = true onProcessSlotCalledWith.add (item.requestId, item.slotIndex) done.complete() asyncSpawn queue.start() setup: onProcessSlotCalled = false onProcessSlotCalledWith = @[] let request = StorageRequest.example repoDs = SQLiteDatastore.new(Memory).tryGet() metaDs = SQLiteDatastore.new(Memory).tryGet() let quota = request.ask.slotSize.truncate(uint) * 100 + 1 repo = RepoStore.new(repoDs, metaDs, quotaMaxBytes = quota) reservations = Reservations.new(repo) # create an availability that should always match availability = Availability.init( size = request.ask.slotSize * 100, duration = request.ask.duration * 100, minPrice = request.ask.pricePerSlot div 100, maxCollateral = request.ask.collateral * 100 ) discard await reservations.reserve(availability) teardown: paused = false await queue.stop() test "starts out empty": newSlotQueue(maxSize = 2, maxWorkers = 2) check queue.len == 0 check $queue == "[]" test "reports correct size": newSlotQueue(maxSize = 2, maxWorkers = 2) check queue.size == 2 test "correctly compares SlotQueueItems": var requestA = StorageRequest.example requestA.ask.duration = 1.u256 requestA.ask.reward = 1.u256 check requestA.ask.pricePerSlot == 1.u256 requestA.ask.collateral = 100000.u256 requestA.expiry = 1001.u256 var requestB = StorageRequest.example requestB.ask.duration = 100.u256 requestB.ask.reward = 1000.u256 check requestB.ask.pricePerSlot == 100000.u256 requestB.ask.collateral = 1.u256 requestB.expiry = 1000.u256 let itemA = SlotQueueItem.init(requestA, 0) let itemB = SlotQueueItem.init(requestB, 0) check itemB < itemA # B higher priority than A check itemA > itemB test "expands available all possible slot indices on init": let request = StorageRequest.example let items = SlotQueueItem.init(request) check items.len.uint64 == request.ask.slots var checked = 0 for slotIndex in 0'u16..