use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use parking_lot::{Condvar, Mutex, RwLock};
#[derive(Debug, Clone)]
pub struct BatchConfig {
pub max_size: usize,
pub max_wait: Duration,
pub min_size: usize,
pub adaptive: bool,
}
impl Default for BatchConfig {
fn default() -> Self {
Self {
max_size: 50,
max_wait: Duration::from_millis(100),
min_size: 1,
adaptive: true,
}
}
}
impl BatchConfig {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_max_size(mut self, size: usize) -> Self {
self.max_size = size;
self
}
#[must_use]
pub fn with_max_wait(mut self, wait: Duration) -> Self {
self.max_wait = wait;
self
}
#[must_use]
pub fn with_min_size(mut self, size: usize) -> Self {
self.min_size = size;
self
}
pub fn high_throughput() -> Self {
Self {
max_size: 200,
max_wait: Duration::from_millis(50),
min_size: 10,
adaptive: true,
}
}
pub fn low_latency() -> Self {
Self {
max_size: 10,
max_wait: Duration::from_millis(10),
min_size: 1,
adaptive: false,
}
}
}
#[derive(Debug)]
pub struct BatchItem<T> {
pub data: T,
pub added_at: Instant,
pub priority: u8,
}
impl<T> BatchItem<T> {
pub fn new(data: T) -> Self {
Self {
data,
added_at: Instant::now(),
priority: 5,
}
}
pub fn with_priority(data: T, priority: u8) -> Self {
Self {
data,
added_at: Instant::now(),
priority,
}
}
pub fn wait_time(&self) -> Duration {
self.added_at.elapsed()
}
}
#[derive(Debug)]
pub struct Batch<T> {
pub items: Vec<BatchItem<T>>,
pub created_at: Instant,
pub id: u64,
}
impl<T> Batch<T> {
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn avg_wait_time(&self) -> Duration {
if self.items.is_empty() {
return Duration::ZERO;
}
let total: Duration = self.items.iter().map(|i| i.wait_time()).sum();
total / self.items.len() as u32
}
pub fn max_wait_time(&self) -> Duration {
self.items
.iter()
.map(|i| i.wait_time())
.max()
.unwrap_or(Duration::ZERO)
}
pub fn into_sorted(mut self) -> Vec<BatchItem<T>> {
self.items.sort_by(|a, b| b.priority.cmp(&a.priority));
self.items
}
}
#[derive(Debug)]
pub struct Batcher<T> {
config: BatchConfig,
pending: Mutex<VecDeque<BatchItem<T>>>,
condvar: Condvar,
batch_counter: AtomicU64,
total_items: AtomicU64,
total_batches: AtomicU64,
adaptive_size: AtomicUsize,
last_batch: RwLock<Instant>,
}
impl<T> Batcher<T> {
pub fn new(config: BatchConfig) -> Self {
let initial_size = config.max_size;
Self {
pending: Mutex::new(VecDeque::new()),
condvar: Condvar::new(),
batch_counter: AtomicU64::new(0),
total_items: AtomicU64::new(0),
total_batches: AtomicU64::new(0),
adaptive_size: AtomicUsize::new(initial_size),
last_batch: RwLock::new(Instant::now()),
config,
}
}
pub fn add(&self, item: T) {
let batch_item = BatchItem::new(item);
self.add_item(batch_item);
}
pub fn add_with_priority(&self, item: T, priority: u8) {
let batch_item = BatchItem::with_priority(item, priority);
self.add_item(batch_item);
}
fn add_item(&self, item: BatchItem<T>) {
let mut pending = self.pending.lock();
pending.push_back(item);
self.total_items.fetch_add(1, Ordering::Relaxed);
let target = self.current_target_size();
if pending.len() >= target {
self.condvar.notify_all();
}
}
pub fn try_take(&self) -> Option<Batch<T>> {
let mut pending = self.pending.lock();
if pending.len() < self.config.min_size {
return None;
}
if let Some(oldest) = pending.front() {
if oldest.wait_time() < self.config.max_wait
&& pending.len() < self.current_target_size()
{
return None;
}
}
self.create_batch(&mut pending)
}
pub fn take(&self, timeout: Duration) -> Option<Batch<T>> {
let mut pending = self.pending.lock();
let deadline = Instant::now() + timeout;
loop {
let target = self.current_target_size();
if pending.len() >= target {
return self.create_batch(&mut pending);
}
if let Some(oldest) = pending.front() {
if oldest.wait_time() >= self.config.max_wait
&& pending.len() >= self.config.min_size
{
return self.create_batch(&mut pending);
}
}
let wait = if let Some(oldest) = pending.front() {
let remaining = self.config.max_wait.saturating_sub(oldest.wait_time());
remaining.min(deadline.saturating_duration_since(Instant::now()))
} else {
deadline.saturating_duration_since(Instant::now())
};
if wait.is_zero() {
return if pending.len() >= self.config.min_size {
self.create_batch(&mut pending)
} else {
None
};
}
let result = self.condvar.wait_for(&mut pending, wait);
if result.timed_out() && pending.len() < self.config.min_size {
return None;
}
}
}
fn create_batch(&self, pending: &mut VecDeque<BatchItem<T>>) -> Option<Batch<T>> {
if pending.is_empty() {
return None;
}
let target = self.current_target_size();
let count = pending.len().min(target);
let items: Vec<_> = pending.drain(..count).collect();
let batch_id = self.batch_counter.fetch_add(1, Ordering::Relaxed);
self.total_batches.fetch_add(1, Ordering::Relaxed);
if self.config.adaptive {
self.adapt_batch_size(&items);
}
*self.last_batch.write() = Instant::now();
Some(Batch {
items,
created_at: Instant::now(),
id: batch_id,
})
}
fn adapt_batch_size(&self, items: &[BatchItem<T>]) {
if items.is_empty() {
return;
}
let total_wait: Duration = items.iter().map(|i| i.wait_time()).sum();
let avg_wait = total_wait / items.len() as u32;
let current = self.adaptive_size.load(Ordering::Relaxed);
let new_size = if avg_wait > self.config.max_wait {
(current * 9 / 10).max(self.config.min_size)
} else if avg_wait < self.config.max_wait / 2 {
(current * 11 / 10).min(self.config.max_size)
} else {
current
};
self.adaptive_size.store(new_size, Ordering::Relaxed);
}
fn current_target_size(&self) -> usize {
if self.config.adaptive {
self.adaptive_size.load(Ordering::Relaxed)
} else {
self.config.max_size
}
}
pub fn pending_count(&self) -> usize {
self.pending.lock().len()
}
pub fn stats(&self) -> BatcherStats {
let pending = self.pending.lock();
let oldest_wait = pending
.front()
.map(|i| i.wait_time())
.unwrap_or(Duration::ZERO);
BatcherStats {
pending_items: pending.len(),
total_items: self.total_items.load(Ordering::Relaxed),
total_batches: self.total_batches.load(Ordering::Relaxed),
current_target_size: self.current_target_size(),
oldest_wait,
time_since_last_batch: self.last_batch.read().elapsed(),
}
}
pub fn flush(&self) -> Option<Batch<T>> {
let mut pending = self.pending.lock();
if pending.is_empty() {
return None;
}
let items: Vec<_> = pending.drain(..).collect();
let batch_id = self.batch_counter.fetch_add(1, Ordering::Relaxed);
self.total_batches.fetch_add(1, Ordering::Relaxed);
*self.last_batch.write() = Instant::now();
Some(Batch {
items,
created_at: Instant::now(),
id: batch_id,
})
}
}
impl<T> Default for Batcher<T> {
fn default() -> Self {
Self::new(BatchConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct BatcherStats {
pub pending_items: usize,
pub total_items: u64,
pub total_batches: u64,
pub current_target_size: usize,
pub oldest_wait: Duration,
pub time_since_last_batch: Duration,
}
impl BatcherStats {
pub fn avg_batch_size(&self) -> f64 {
if self.total_batches == 0 {
return 0.0;
}
self.total_items as f64 / self.total_batches as f64
}
}
pub trait BatchProcessor<T, R> {
fn process(&self, batch: Batch<T>) -> Vec<R>;
}
pub struct FnBatchProcessor<T, R, F>
where
F: Fn(Vec<T>) -> Vec<R>,
{
processor: F,
_phantom: std::marker::PhantomData<(T, R)>,
}
impl<T, R, F> FnBatchProcessor<T, R, F>
where
F: Fn(Vec<T>) -> Vec<R>,
{
pub fn new(processor: F) -> Self {
Self {
processor,
_phantom: std::marker::PhantomData,
}
}
}
impl<T, R, F> BatchProcessor<T, R> for FnBatchProcessor<T, R, F>
where
F: Fn(Vec<T>) -> Vec<R>,
{
fn process(&self, batch: Batch<T>) -> Vec<R> {
let data: Vec<T> = batch.items.into_iter().map(|i| i.data).collect();
(self.processor)(data)
}
}
pub struct BatchExecutor<T: Send + 'static> {
batcher: Arc<Batcher<T>>,
}
impl<T: Send + 'static> BatchExecutor<T> {
pub fn new(config: BatchConfig) -> Self {
Self {
batcher: Arc::new(Batcher::new(config)),
}
}
pub fn batcher(&self) -> &Arc<Batcher<T>> {
&self.batcher
}
pub fn submit(&self, item: T) {
self.batcher.add(item);
}
pub fn submit_with_priority(&self, item: T, priority: u8) {
self.batcher.add_with_priority(item, priority);
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_batch_creation() {
let batcher: Batcher<u32> = Batcher::default();
for i in 0..10 {
batcher.add(i);
}
assert_eq!(batcher.pending_count(), 10);
let batch = batcher.try_take();
assert!(batch.is_none());
}
#[test]
fn test_batch_full() {
let config = BatchConfig::default().with_max_size(5).with_min_size(5);
let batcher: Batcher<u32> = Batcher::new(config);
for i in 0..5 {
batcher.add(i);
}
let batch = batcher.try_take().unwrap();
assert_eq!(batch.len(), 5);
}
#[test]
fn test_flush() {
let batcher: Batcher<u32> = Batcher::default();
for i in 0..3 {
batcher.add(i);
}
let batch = batcher.flush().unwrap();
assert_eq!(batch.len(), 3);
assert_eq!(batcher.pending_count(), 0);
}
#[test]
fn test_priority() {
let batcher: Batcher<&str> = Batcher::default();
batcher.add_with_priority("low", 1);
batcher.add_with_priority("high", 9);
batcher.add_with_priority("medium", 5);
let batch = batcher.flush().unwrap();
let sorted = batch.into_sorted();
assert_eq!(sorted[0].data, "high");
assert_eq!(sorted[1].data, "medium");
assert_eq!(sorted[2].data, "low");
}
#[test]
fn test_stats() {
let batcher: Batcher<u32> = Batcher::default();
for i in 0..10 {
batcher.add(i);
}
let stats = batcher.stats();
assert_eq!(stats.pending_items, 10);
assert_eq!(stats.total_items, 10);
}
}