Chrysostomos Nanakos bb6ab1befa
chore: Block exchange protocol rewrite (#1411)
Signed-off-by: Chrysostomos Nanakos <chris@include.gr>
2026-04-25 00:37:42 +00:00

521 lines
13 KiB
Nim

import std/options
import pkg/unittest2
import pkg/storage/blockexchange/engine/scheduler {.all.}
suite "Scheduler":
const
WindowSize = 16384'u64
Threshold = 0.75
var scheduler: Scheduler
setup:
scheduler = Scheduler.new()
test "Should initialize with correct parameters":
scheduler.init(1000, 100, WindowSize, Threshold)
check scheduler.totalBlockCount() == 1000
check scheduler.batchSizeCount() == 100
check scheduler.hasWork() == true
check scheduler.isEmpty() == false
test "Should take batches in order":
scheduler.init(1000, 100, WindowSize, Threshold)
let batch1 = scheduler.take()
check batch1.isSome
check batch1.get.start == 0
check batch1.get.count == 100
let batch2 = scheduler.take()
check batch2.isSome
check batch2.get.start == 100
check batch2.get.count == 100
test "Should handle last batch with fewer blocks":
scheduler.init(250, 100, WindowSize, Threshold)
discard scheduler.take()
discard scheduler.take()
let lastBatch = scheduler.take()
check lastBatch.isSome
check lastBatch.get.start == 200
check lastBatch.get.count == 50
test "Should mark batch as complete":
scheduler.init(300, 100, WindowSize, Threshold)
let batch = scheduler.take()
check batch.isSome
check batch.get.start == 0
scheduler.markComplete(0)
let next = scheduler.take()
check next.isSome
check next.get.start == 100
test "Should requeue batch at front":
scheduler.init(500, 100, WindowSize, Threshold)
let batch1 = scheduler.take()
check batch1.get.start == 0
let batch2 = scheduler.take()
check batch2.get.start == 100
scheduler.requeueFront(0, 100)
let requeued = scheduler.take()
check requeued.isSome
check requeued.get.start == 0
check requeued.get.count == 100
test "Should requeue batch at back":
scheduler.init(500, 100, WindowSize, Threshold)
let
batch1 = scheduler.take()
batch2 = scheduler.take()
scheduler.requeueBack(0, 100)
scheduler.requeueFront(100, 100)
let first = scheduler.take()
check first.get.start == 100
let second = scheduler.take()
check second.get.start == 0
test "Should handle partialComplete with single missing range":
scheduler.init(1000, 100, WindowSize, Threshold)
let batch = scheduler.take()
check batch.isSome
check batch.get.start == 0
check batch.get.count == 100
let missingRanges = @[(start: 50'u64, count: 50'u64)]
scheduler.partialComplete(0, missingRanges)
let next = scheduler.take()
check next.isSome
check next.get.start == 50
check next.get.count == 50
test "Should handle partialComplete with multiple missing ranges":
scheduler.init(1000, 100, WindowSize, Threshold)
let batch = scheduler.take()
check batch.isSome
check batch.get.start == 0
let missingRanges =
@[(start: 25'u64, count: 25'u64), (start: 75'u64, count: 25'u64)]
scheduler.partialComplete(0, missingRanges)
let next1 = scheduler.take()
check next1.isSome
check next1.get.start == 25
check next1.get.count == 25
let next2 = scheduler.take()
check next2.isSome
check next2.get.start == 75
check next2.get.count == 25
test "Should handle partialComplete with non-contiguous missing ranges":
scheduler.init(1000, 256, WindowSize, Threshold)
let batch = scheduler.take()
check batch.isSome
check batch.get.start == 0
check batch.get.count == 256
let missingRanges =
@[(start: 101'u64, count: 49'u64), (start: 201'u64, count: 55'u64)]
scheduler.partialComplete(0, missingRanges)
let next1 = scheduler.take()
check next1.isSome
check next1.get.start == 101
check next1.get.count == 49
let next2 = scheduler.take()
check next2.isSome
check next2.get.start == 201
check next2.get.count == 55
test "Should not skip completed batches after partialComplete":
scheduler.init(500, 100, WindowSize, Threshold)
let batch1 = scheduler.take()
check batch1.get.start == 0
scheduler.markComplete(0)
let batch2 = scheduler.take()
check batch2.get.start == 100
let missingRanges = @[(start: 150'u64, count: 50'u64)]
scheduler.partialComplete(100, missingRanges)
let next = scheduler.take()
check next.isSome
check next.get.start == 150
check next.get.count == 50
test "Should become empty after all batches complete":
scheduler.init(200, 100, WindowSize, Threshold)
let batch1 = scheduler.take()
scheduler.markComplete(batch1.get.start)
let batch2 = scheduler.take()
scheduler.markComplete(batch2.get.start)
check scheduler.isEmpty() == true
check scheduler.hasWork() == false
test "Should handle out-of-order completion":
scheduler.init(500, 100, WindowSize, Threshold)
let
batch0 = scheduler.take()
batch1 = scheduler.take()
batch2 = scheduler.take()
check batch0.get.start == 0
check batch1.get.start == 100
check batch2.get.start == 200
scheduler.markComplete(200)
scheduler.markComplete(0)
scheduler.markComplete(100)
let next = scheduler.take()
check next.isSome
check next.get.start == 300
test "Should initialize with range":
scheduler.initRange(500, 200, 100, WindowSize, Threshold)
check scheduler.totalBlockCount() == 700
check scheduler.batchSizeCount() == 100
check scheduler.completedWatermark() == 500
let batch1 = scheduler.take()
check batch1.isSome
check batch1.get.start == 500
check batch1.get.count == 100
let batch2 = scheduler.take()
check batch2.isSome
check batch2.get.start == 600
check batch2.get.count == 100
test "Should add specific batches":
scheduler.add(100, 50)
scheduler.add(300, 75)
check scheduler.totalBlockCount() == 375
check scheduler.batchSizeCount() == 50
let batch1 = scheduler.take()
check batch1.isSome
check batch1.get.start == 100
check batch1.get.count == 50
let batch2 = scheduler.take()
check batch2.isSome
check batch2.get.start == 300
check batch2.get.count == 75
test "Should clear scheduler":
scheduler.init(500, 100, WindowSize, Threshold)
discard scheduler.take()
discard scheduler.take()
scheduler.requeueFront(0, 100)
scheduler.clear()
check scheduler.hasWork() == false
check scheduler.isEmpty() == true
check scheduler.requeuedCount() == 0
check scheduler.totalBlockCount() == 0
check scheduler.batchSizeCount() == 0
let batch = scheduler.take()
check batch.isNone
test "Should return pending batches":
scheduler.init(500, 100, WindowSize, Threshold)
check scheduler.pending().len == 0
discard scheduler.take()
scheduler.requeueFront(0, 100)
let pending = scheduler.pending()
check pending.len == 1
check pending[0].start == 0
check pending[0].count == 100
test "Should return correct requeuedCount":
scheduler.init(500, 100, WindowSize, Threshold)
check scheduler.requeuedCount() == 0
discard scheduler.take()
discard scheduler.take()
scheduler.requeueFront(0, 100)
scheduler.requeueBack(100, 100)
check scheduler.requeuedCount() == 2
test "Should return none when exhausted":
scheduler.init(200, 100, WindowSize, Threshold)
let
b1 = scheduler.take()
b2 = scheduler.take()
check b1.isSome
check b2.isSome
let b3 = scheduler.take()
check b3.isNone
test "Should handle single block":
scheduler.init(1, 100, WindowSize, Threshold)
let batch = scheduler.take()
check batch.isSome
check batch.get.start == 0
check batch.get.count == 1
scheduler.markComplete(0)
check scheduler.isEmpty() == true
test "Should handle batch size larger than total":
scheduler.init(50, 100, WindowSize, Threshold)
let batch = scheduler.take()
check batch.isSome
check batch.get.start == 0
check batch.get.count == 50
scheduler.markComplete(0)
check scheduler.isEmpty() == true
test "Should handle zero blocks":
scheduler.init(0, 100, WindowSize, Threshold)
check scheduler.hasWork() == false
check scheduler.isEmpty() == true
let batch = scheduler.take()
check batch.isNone
test "Should ignore requeue of completed batch":
scheduler.init(300, 100, WindowSize, Threshold)
let batch = scheduler.take()
scheduler.markComplete(batch.get.start)
scheduler.requeueFront(0, 100)
scheduler.requeueBack(0, 100)
check scheduler.requeuedCount() == 0
test "Should track in-flight batches":
scheduler.init(300, 100, WindowSize, Threshold)
let batch = scheduler.take()
check batch.isSome
let batch2 = scheduler.take()
check batch2.isSome
check batch2.get.start == 100
scheduler.markComplete(0)
scheduler.requeueFront(100, 100)
let batch3 = scheduler.take()
check batch3.isSome
check batch3.get.start == 100
test "Should skip completed batches in requeued":
scheduler.init(500, 100, WindowSize, Threshold)
discard scheduler.take()
scheduler.requeueBack(0, 100)
discard scheduler.take()
scheduler.markComplete(0)
scheduler.requeueBack(0, 100)
let next = scheduler.take()
check next.isSome
check next.get.start == 100
test "Watermark advances after all sub-ranges of partial batch complete":
scheduler.init(16, 8, WindowSize, Threshold)
let batch = scheduler.take()
check batch.get.start == 0
check batch.get.count == 8
let missingRanges = @[
(start: 1'u64, count: 1'u64),
(start: 3'u64, count: 1'u64),
(start: 5'u64, count: 1'u64),
(start: 7'u64, count: 1'u64),
]
scheduler.partialComplete(0, missingRanges)
check scheduler.completedWatermark() == 0
let sub1 = scheduler.take()
check sub1.get.start == 1
scheduler.markComplete(1)
check scheduler.completedWatermark() == 0
let sub2 = scheduler.take()
check sub2.get.start == 3
scheduler.markComplete(3)
check scheduler.completedWatermark() == 0
let sub3 = scheduler.take()
check sub3.get.start == 5
scheduler.markComplete(5)
check scheduler.completedWatermark() == 0
let sub4 = scheduler.take()
check sub4.get.start == 7
scheduler.markComplete(7)
check scheduler.completedWatermark() == 8
test "Watermark merges OOO after partial batch completes":
scheduler.init(24, 8, WindowSize, Threshold)
let
batch0 = scheduler.take()
batch1 = scheduler.take()
batch2 = scheduler.take()
check batch0.get.start == 0
check batch1.get.start == 8
check batch2.get.start == 16
scheduler.markComplete(8)
scheduler.markComplete(16)
check scheduler.completedWatermark() == 0
scheduler.partialComplete(0, @[(start: 3'u64, count: 1'u64)])
check scheduler.completedWatermark() == 0
let sub = scheduler.take()
check sub.get.start == 3
scheduler.markComplete(3)
check scheduler.completedWatermark() == 24
check scheduler.isEmpty() == true
test "Nested partials, requeues, OOO merge, multiple partial batches":
scheduler.init(40, 8, WindowSize, Threshold)
let
b0 = scheduler.take()
b1 = scheduler.take()
b2 = scheduler.take()
b3 = scheduler.take()
b4 = scheduler.take()
check b0.get.start == 0
check b4.get.start == 32
scheduler.markComplete(32)
check scheduler.completedWatermark() == 0
scheduler.markComplete(16)
check scheduler.completedWatermark() == 0
scheduler.partialComplete(0, @[(start: 2'u64, count: 2'u64)])
check scheduler.completedWatermark() == 0
scheduler.partialComplete(
8, @[(start: 10'u64, count: 3'u64), (start: 13'u64, count: 3'u64)]
)
check scheduler.completedWatermark() == 0
scheduler.markComplete(24)
check scheduler.completedWatermark() == 0
let sub1a = scheduler.take()
check sub1a.get.start == 10
check sub1a.get.count == 3
let sub1b = scheduler.take()
check sub1b.get.start == 13
check sub1b.get.count == 3
let sub0a = scheduler.take()
check sub0a.get.start == 2
check sub0a.get.count == 2
scheduler.requeueFront(2, 2)
check scheduler.completedWatermark() == 0
scheduler.markComplete(13)
check scheduler.completedWatermark() == 0
scheduler.partialComplete(10, @[(start: 11'u64, count: 2'u64)])
check scheduler.completedWatermark() == 0
let sub1c = scheduler.take()
check sub1c.get.start == 11
check sub1c.get.count == 2
let sub0b = scheduler.take()
check sub0b.get.start == 2
scheduler.markComplete(2)
check scheduler.completedWatermark() == 8
scheduler.markComplete(11)
check scheduler.completedWatermark() == 40
check scheduler.isEmpty() == true
check scheduler.hasWork() == false
test "BlockBatch batchEnd":
let batch: BlockBatch = (start: 100'u64, count: 50'u64)
check batch.batchEnd == 150
test "BlockBatch contains":
let batch: BlockBatch = (start: 100'u64, count: 50'u64)
check batch.contains(100) == true
check batch.contains(149) == true
check batch.contains(99) == false
check batch.contains(150) == false
test "BlockBatch merge":
let
batch1: BlockBatch = (start: 100'u64, count: 50'u64)
batch2: BlockBatch = (start: 140'u64, count: 30'u64)
batch3: BlockBatch = (start: 200'u64, count: 20'u64)
let merged1 = merge(batch1, batch2)
check merged1.isSome
check merged1.get.start == 100
check merged1.get.count == 70
let merged2 = merge(batch1, batch3)
check merged2.isNone