use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use vortex_error::VortexExpect;
use vortex_mask::Mask;
#[derive(Clone)]
pub(crate) struct RowLimit(Arc<AtomicU64>);
#[derive(Clone)]
pub(crate) enum ScanLimit {
PerExecution(u64),
Shared(RowLimit),
}
impl ScanLimit {
pub(crate) fn budget(&self) -> RowLimit {
match self {
Self::PerExecution(limit) => RowLimit::new(*limit),
Self::Shared(limit) => limit.clone(),
}
}
}
impl RowLimit {
pub(crate) fn new(limit: u64) -> Self {
Self(Arc::new(AtomicU64::new(limit)))
}
pub(crate) fn limit(&self, mask: Mask) -> Mask {
let granted = self.take(mask.true_count());
mask.limit(granted)
}
pub(crate) fn take(&self, rows: usize) -> usize {
let requested = u64::try_from(rows).unwrap_or(u64::MAX);
usize::try_from(self.reserve(requested)).unwrap_or(usize::MAX)
}
pub(crate) fn is_exhausted(&self) -> bool {
self.0.load(Ordering::Relaxed) == 0
}
fn reserve(&self, requested: u64) -> u64 {
let remaining = self
.0
.try_update(Ordering::Relaxed, Ordering::Relaxed, |remaining| {
Some(remaining.saturating_sub(requested))
})
.ok()
.vortex_expect("row reservation always updates the budget");
remaining.min(requested)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::Barrier;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::thread;
use vortex_mask::Mask;
use super::RowLimit;
#[test]
fn reserve_grants_up_to_the_remaining_budget() {
let limit = RowLimit::new(5);
assert_eq!(limit.reserve(3), 3);
assert!(!limit.is_exhausted());
assert_eq!(limit.reserve(10), 2);
assert!(limit.is_exhausted());
assert_eq!(limit.reserve(1), 0);
}
#[test]
fn limit_keeps_the_earliest_granted_rows() {
let limit = RowLimit::new(2);
let mask = Mask::from_iter([true, false, true, true, false, true]);
let limited = limit.limit(mask);
assert_eq!(limited.true_count(), 2);
assert!(limited.value(0));
assert!(limited.value(2));
assert!(!limited.value(3));
assert!(!limited.value(5));
assert!(limit.is_exhausted());
}
#[test]
fn concurrent_reservations_never_exceed_the_budget() {
const THREADS: usize = 8;
const PER_THREAD: u64 = 10_000;
const LIMIT: u64 = 25_000;
let limit = RowLimit::new(LIMIT);
let granted_total = Arc::new(AtomicU64::new(0));
let barrier = Arc::new(Barrier::new(THREADS));
thread::scope(|scope| {
for _ in 0..THREADS {
let limit = limit.clone();
let granted_total = Arc::clone(&granted_total);
let barrier = Arc::clone(&barrier);
scope.spawn(move || {
barrier.wait();
let mut local = 0;
for _ in 0..PER_THREAD {
local += limit.reserve(1);
}
granted_total.fetch_add(local, Ordering::Relaxed);
});
}
});
assert_eq!(granted_total.load(Ordering::Relaxed), LIMIT);
assert!(limit.is_exhausted());
}
}