use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, mpsc};
use std::time::Duration;
use super::{
EbrRetireQueue, MAX_EBR_RECLAIM_SLOTS_PER_CYCLE, RetiredBatch, VersionIdx, epoch_batch_position,
};
#[test]
fn ebr_batch_caller_iterator_can_reenter_queue_without_deadlock() {
let queue = Arc::new(EbrRetireQueue::new());
let worker_queue = Arc::clone(&queue);
let (finished, completion) = mpsc::channel();
let worker = std::thread::spawn(move || {
worker_queue.retire_batch(
(0..64_u32).map(|slot| {
let _ = worker_queue.pending_count();
VersionIdx::new(0, slot, 0)
}),
7,
);
finished.send(()).expect("report completed retirement");
});
completion
.recv_timeout(Duration::from_secs(10))
.expect("retire_batch ran its iterator under the pending-queue mutex");
worker.join().expect("retiring thread did not panic");
assert_eq!(queue.pending_count(), 64);
assert_eq!(queue.total_retired(), 64);
assert!(queue.drain_if_safe(100, Some(7)).is_empty());
assert_eq!(queue.drain_if_safe(8, None).len(), 64);
}
#[test]
fn ebr_batch_lookup_handles_wrapped_deque_and_extreme_epochs() {
let mut batches = VecDeque::with_capacity(8);
for epoch in 0..8 {
batches.push_back(RetiredBatch {
retire_epoch: epoch * 2,
indices: vec![VersionIdx::new(0, epoch as u32, 0)],
});
}
for _ in 0..5 {
batches.pop_front();
}
for epoch in 8..13 {
batches.push_back(RetiredBatch {
retire_epoch: epoch * 2,
indices: vec![VersionIdx::new(0, epoch as u32, 0)],
});
}
for epoch in (0..30).chain([u64::MAX]) {
let expected = batches
.iter()
.position(|batch| batch.retire_epoch == epoch)
.map_or_else(
|| {
Err(batches
.iter()
.take_while(|batch| batch.retire_epoch < epoch)
.count())
},
Ok,
);
assert_eq!(epoch_batch_position(&batches, epoch), expected);
}
assert_eq!(epoch_batch_position(&VecDeque::new(), u64::MAX), Err(0));
}
#[test]
fn ebr_batch_delayed_producer_merges_once_and_preserves_epoch_order() {
let queue = EbrRetireQueue::new();
for epoch in (1..=64_u64).rev() {
queue.retire(VersionIdx::new(0, epoch as u32, 0), epoch);
}
queue.retire_batch((100..1_100).map(|slot| VersionIdx::new(0, slot, 0)), 3);
queue.retire_batch(std::iter::empty(), 2);
let pending = queue.pending.lock();
assert_eq!(pending.len(), 64);
assert_eq!(pending[2].retire_epoch, 3);
assert_eq!(pending[2].indices.len(), 1_001);
assert!(
pending
.iter()
.zip(pending.iter().skip(1))
.all(|(a, b)| a.retire_epoch < b.retire_epoch)
);
drop(pending);
assert_eq!(queue.total_retired(), 1_064);
assert_eq!(queue.pending_count(), 1_064);
assert_eq!(queue.drain_if_safe(100, Some(4)).len(), 1_003);
assert_eq!(queue.pending_count(), 61);
}
#[test]
fn ebr_batch_large_retirement_obeys_cycle_budget_without_losing_slots() {
let queue = EbrRetireQueue::new();
let count = MAX_EBR_RECLAIM_SLOTS_PER_CYCLE * 3 + 17;
queue.retire_batch((0..count).map(|slot| VersionIdx::new(0, slot as u32, 0)), 9);
assert!(queue.drain_if_safe(20, Some(9)).is_empty());
let mut all = HashSet::new();
while queue.pending_count() != 0 {
let drained = queue.drain_if_safe(10, None);
assert!(!drained.is_empty());
assert!(drained.len() <= MAX_EBR_RECLAIM_SLOTS_PER_CYCLE);
for index in drained {
assert!(all.insert(index), "slot was recycled twice");
}
}
assert_eq!(all.len(), count);
assert_eq!(queue.total_retired(), count as u64);
assert_eq!(queue.total_recycled(), count as u64);
assert_eq!(queue.reclaim_cycle_receipt().collection_cycles_total, 4);
}
#[test]
fn ebr_batch_max_epoch_does_not_wrap_reclamation_boundary() {
let queue = EbrRetireQueue::new();
let older = VersionIdx::new(0, 1, 0);
let newest = VersionIdx::new(0, 2, 0);
queue.retire_batch([newest], u64::MAX);
queue.retire_batch([older], u64::MAX - 1);
assert_eq!(queue.drain_if_safe(u64::MAX, None), vec![older]);
assert!(queue.drain_if_safe(u64::MAX, None).is_empty());
assert_eq!(queue.force_drain(), vec![newest]);
}
#[test]
fn ebr_batch_randomized_retire_and_reclaim_match_slot_oracle() {
let queue = EbrRetireQueue::new();
let mut oracle: HashMap<VersionIdx, u64> = HashMap::new();
let mut random = 0x4d56_4343_4542_5201_u64;
let mut next_slot = 0_u32;
let mut recycled = 0_u64;
for _ in 0..5_000 {
random = random
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let epoch = (random >> 24) % 128;
if random & 3 != 0 {
let count = ((random >> 40) % 19) as usize;
let mut batch = Vec::with_capacity(count);
for _ in 0..count {
let index = VersionIdx::new(0, next_slot, 0);
next_slot += 1;
assert!(oracle.insert(index, epoch).is_none());
batch.push(index);
}
queue.retire_batch(batch, epoch);
} else {
let current = epoch;
let pinned = (random & 8 != 0).then_some((random >> 48) % 128);
let safe = pinned.map_or(current, |pin| pin.min(current));
let eligible = oracle.values().filter(|epoch| **epoch < safe).count();
let drained = queue.drain_if_safe(current, pinned);
assert_eq!(drained.len(), eligible.min(MAX_EBR_RECLAIM_SLOTS_PER_CYCLE));
for index in drained {
let retired_epoch = oracle.remove(&index).expect("slot belongs to the queue");
assert!(retired_epoch < safe, "live-reader boundary was crossed");
recycled += 1;
}
}
assert_eq!(queue.pending_count(), oracle.len());
assert_eq!(queue.total_retired(), u64::from(next_slot));
assert_eq!(queue.total_recycled(), recycled);
}
for index in queue.force_drain() {
assert!(oracle.remove(&index).is_some());
}
assert!(oracle.is_empty());
assert_eq!(queue.total_retired(), queue.total_recycled());
}