use std::collections::VecDeque;
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::Mutex;
use tokio::sync::{oneshot, Notify};
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct BatchConfig {
pub max_batch_size: usize,
pub max_wait_ms: u64,
pub min_batch_size: usize,
pub dynamic_sizing: bool,
}
impl Default for BatchConfig {
fn default() -> Self {
Self {
max_batch_size: 32,
max_wait_ms: 50,
min_batch_size: 1,
dynamic_sizing: true,
}
}
}
impl BatchConfig {
pub fn new() -> Self {
Self::default()
}
pub fn max_size(mut self, size: usize) -> Self {
self.max_batch_size = size.max(1);
self
}
pub fn max_wait(mut self, ms: u64) -> Self {
self.max_wait_ms = ms;
self
}
pub fn min_size(mut self, size: usize) -> Self {
self.min_batch_size = size.max(1);
self
}
}
pub struct BatchRequest<T> {
pub payload: T,
response_tx: oneshot::Sender<BatchResult<T>>,
arrived_at: Instant,
}
#[derive(Debug)]
pub struct BatchResult<T> {
pub payload: T,
pub batch_size: usize,
pub queue_time_ms: u64,
pub process_time_ms: u64,
}
pub struct BatchProcessor<T: Send + 'static> {
config: BatchConfig,
queue: Arc<Mutex<VecDeque<BatchRequest<T>>>>,
notify: Arc<Notify>,
stats: Arc<Mutex<BatchStats>>,
}
#[derive(Debug, Default, Clone)]
pub struct BatchStats {
pub total_requests: u64,
pub total_batches: u64,
pub avg_batch_size: f64,
pub avg_queue_time_ms: f64,
pub avg_process_time_ms: f64,
}
impl<T: Send + 'static> BatchProcessor<T> {
pub fn new(config: BatchConfig) -> Self {
Self {
config,
queue: Arc::new(Mutex::new(VecDeque::new())),
notify: Arc::new(Notify::new()),
stats: Arc::new(Mutex::new(BatchStats::default())),
}
}
pub async fn submit(&self, payload: T) -> Result<BatchResult<T>, BatchError> {
let (tx, rx) = oneshot::channel();
let request = BatchRequest {
payload,
response_tx: tx,
arrived_at: Instant::now(),
};
{
let mut queue = self.queue.lock();
queue.push_back(request);
if queue.len() >= self.config.max_batch_size {
self.notify.notify_one();
}
}
self.notify.notify_one();
rx.await.map_err(|_| BatchError::Cancelled)
}
pub async fn run<F, Fut>(&self, mut executor: F)
where
F: FnMut(Vec<T>) -> Fut,
Fut: std::future::Future<Output = Vec<T>>,
{
info!(max_batch = self.config.max_batch_size, "batch serving loop started");
loop {
if !self.wait_for_batch().await {
continue;
}
let batch = self.collect_batch();
if batch.is_empty() {
continue;
}
let mut payloads = Vec::with_capacity(batch.len());
let mut meta = Vec::with_capacity(batch.len());
for (payload, tx, arrived_at) in batch {
payloads.push(payload);
meta.push((tx, arrived_at));
}
let responses = executor(payloads).await;
let results = meta
.into_iter()
.zip(responses)
.map(|((tx, arrived_at), response)| (tx, arrived_at, response))
.collect();
self.complete_batch(results);
}
}
pub fn queue_len(&self) -> usize {
self.queue.lock().len()
}
pub fn stats(&self) -> BatchStats {
self.stats.lock().clone()
}
pub fn collect_batch(&self) -> Vec<(T, oneshot::Sender<BatchResult<T>>, Instant)> {
let mut queue = self.queue.lock();
let batch_size = queue.len().min(self.config.max_batch_size);
let mut batch = Vec::with_capacity(batch_size);
for _ in 0..batch_size {
if let Some(req) = queue.pop_front() {
batch.push((req.payload, req.response_tx, req.arrived_at));
}
}
batch
}
pub fn complete_batch(&self, results: Vec<(oneshot::Sender<BatchResult<T>>, Instant, T)>) {
let batch_size = results.len();
let process_start = Instant::now();
let mut total_queue_time = 0u64;
for (tx, arrived_at, response) in results {
let queue_time = arrived_at.elapsed().as_millis() as u64;
total_queue_time += queue_time;
let result = BatchResult {
payload: response,
batch_size,
queue_time_ms: queue_time,
process_time_ms: process_start.elapsed().as_millis() as u64,
};
let _ = tx.send(result);
}
let mut stats = self.stats.lock();
stats.total_requests += batch_size as u64;
stats.total_batches += 1;
let n = stats.total_batches as f64;
stats.avg_batch_size = stats.avg_batch_size * (n - 1.0) / n + batch_size as f64 / n;
stats.avg_queue_time_ms = stats.avg_queue_time_ms * (n - 1.0) / n
+ (total_queue_time as f64 / batch_size as f64) / n;
debug!(batch_size = batch_size, "Batch completed");
}
pub async fn wait_for_batch(&self) -> bool {
let timeout = Duration::from_millis(self.config.max_wait_ms);
tokio::select! {
_ = self.notify.notified() => {
self.queue.lock().len() >= self.config.min_batch_size
}
_ = tokio::time::sleep(timeout) => {
!self.queue.lock().is_empty()
}
}
}
}
impl<T: Send + 'static> Clone for BatchProcessor<T> {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
queue: self.queue.clone(),
notify: self.notify.clone(),
stats: self.stats.clone(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum BatchError {
#[error("Request cancelled")]
Cancelled,
#[error("Queue full")]
QueueFull,
#[error("Processing failed: {0}")]
ProcessingFailed(String),
}
pub struct BatchCollector<T> {
items: Vec<T>,
max_size: usize,
created_at: Instant,
max_wait: Duration,
}
impl<T> BatchCollector<T> {
pub fn new(max_size: usize, max_wait_ms: u64) -> Self {
Self {
items: Vec::with_capacity(max_size),
max_size,
created_at: Instant::now(),
max_wait: Duration::from_millis(max_wait_ms),
}
}
pub fn add(&mut self, item: T) -> bool {
if self.items.len() < self.max_size {
self.items.push(item);
true
} else {
false
}
}
pub fn is_ready(&self) -> bool {
self.items.len() >= self.max_size || self.created_at.elapsed() >= self.max_wait
}
pub fn is_full(&self) -> bool {
self.items.len() >= self.max_size
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn take(self) -> Vec<T> {
self.items
}
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_collector() {
let mut collector: BatchCollector<i32> = BatchCollector::new(3, 100);
assert!(collector.add(1));
assert!(collector.add(2));
assert!(!collector.is_full());
assert!(collector.add(3));
assert!(collector.is_full());
assert!(!collector.add(4));
let items = collector.take();
assert_eq!(items, vec![1, 2, 3]);
}
#[tokio::test]
async fn test_batch_processor_stats() {
let processor: BatchProcessor<String> = BatchProcessor::new(BatchConfig::default());
let stats = processor.stats();
assert_eq!(stats.total_requests, 0);
assert_eq!(stats.total_batches, 0);
}
#[tokio::test]
async fn test_batch_processor_end_to_end() {
let processor: BatchProcessor<u32> = BatchProcessor::new(BatchConfig::default().max_wait(20));
let server = processor.clone();
tokio::spawn(async move {
server
.run(|inputs: Vec<u32>| async move {
inputs.into_iter().map(|x| x * 2).collect::<Vec<u32>>()
})
.await;
});
let r1 = processor.submit(21).await.unwrap();
assert_eq!(r1.payload, 42);
assert!(r1.batch_size >= 1);
let r2 = processor.submit(50).await.unwrap();
assert_eq!(r2.payload, 100);
assert!(processor.stats().total_requests >= 2);
}
}