use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::{Mutex, Notify};
use super::handler::BatchHandler;
pub async fn batch_inference_loop<BH: BatchHandler, const S: usize>(
handler: &BH,
running: Arc<AtomicBool>,
notifier: Arc<Notify>,
waiting_requests: Arc<Mutex<Vec<BH::Request>>>,
) {
let active_count = Arc::new(Mutex::new(0));
let active_tensor: Arc<Mutex<Option<BH::ModelInput>>> = Default::default();
let mut active_requests: Vec<BH::Request> = vec![];
loop {
if !running.load(Ordering::SeqCst) {
break;
}
let should_process = should_process(active_count.clone(), waiting_requests.clone()).await;
if !should_process {
tokio::select! {
_ = notifier.notified() => {},
_ = tokio::time::sleep(Duration::from_millis(100)) => {}
}
}
let items = drain_possible_requests(
S, waiting_requests.clone(), active_count.clone(),
).await;
if !items.is_empty() || {
let active = active_count.lock().await;
*active > 0
} {
let mut tensor_lock = active_tensor.lock().await;
handler.make_batch_input(&mut tensor_lock, &items).await;
active_requests.extend(items);
let input = tensor_lock.clone();
match input {
None => {}
Some(input) => {
let output = handler.forward(&input).await;
handler.handle_outputs(
&mut active_requests,
&mut tensor_lock,
output,
active_count.clone()
).await;
}
}
}
}
}
#[inline]
async fn should_process<T>(
active_count: Arc<Mutex<usize>>,
waiting_requests: Arc<Mutex<Vec<T>>>,
) -> bool {
let active = active_count.lock().await;
let has_active_items = *active > 0;
if has_active_items {
true
} else {
let waiting = waiting_requests.lock().await;
!waiting.is_empty()
}
}
async fn drain_possible_requests<T>(
batch_size: usize,
waiting_requests: Arc<Mutex<Vec<T>>>,
active_count: Arc<Mutex<usize>>,
) -> Vec<T> {
let mut requests = waiting_requests.lock().await;
let mut active = active_count.lock().await;
let available_slots = batch_size.saturating_sub(*active);
if available_slots > 0 && !requests.is_empty() {
let items_to_take = std::cmp::min(available_slots, requests.len());
let batch = requests.drain(0..items_to_take).collect::<Vec<_>>();
*active += items_to_take;
return batch;
}
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::sync::{Mutex, Notify, mpsc};
use tokio::time::timeout;
struct TestRequest {
id: usize,
input: Vec<f32>,
sender: mpsc::Sender<Vec<f32>>,
is_complete: Arc<AtomicBool>,
}
#[derive(Clone)]
struct TestModelInput {
batch: Vec<(usize, Vec<f32>)>, }
#[derive(Clone)]
struct TestModelOutput {
results: Vec<(usize, Vec<f32>)>, }
#[derive(Clone)]
struct TestBatchHandler {
forward_calls: Arc<AtomicUsize>,
make_batch_calls: Arc<AtomicUsize>,
handle_output_calls: Arc<AtomicUsize>,
}
impl TestBatchHandler {
fn new() -> Self {
Self {
forward_calls: Arc::new(AtomicUsize::new(0)),
make_batch_calls: Arc::new(AtomicUsize::new(0)),
handle_output_calls: Arc::new(AtomicUsize::new(0)),
}
}
fn get_metrics(&self) -> (usize, usize, usize) {
(
self.forward_calls.load(Ordering::SeqCst),
self.make_batch_calls.load(Ordering::SeqCst),
self.handle_output_calls.load(Ordering::SeqCst),
)
}
}
#[async_trait]
impl BatchHandler for TestBatchHandler {
type Request = TestRequest;
type ModelInput = TestModelInput;
type ModelOutput = TestModelOutput;
async fn make_batch_input(
&self,
model_input: &mut Option<Self::ModelInput>,
requests: &[Self::Request]
) {
self.make_batch_calls.fetch_add(1, Ordering::SeqCst);
let mut batch = model_input.take().unwrap_or_else(||
TestModelInput { batch: Vec::new() }
);
for req in requests {
batch.batch.push((req.id, req.input.clone()));
}
*model_input = Some(batch);
}
async fn forward(&self, model_input: &Self::ModelInput) -> Self::ModelOutput {
self.forward_calls.fetch_add(1, Ordering::SeqCst);
let results = model_input.batch.iter()
.map(|(id, input)| {
let output = input.iter().map(|x| x * 2.0).collect();
(*id, output)
})
.collect();
TestModelOutput { results }
}
async fn handle_outputs(
&self,
batch: &mut Vec<Self::Request>,
input: &mut Option<Self::ModelInput>,
output: Self::ModelOutput,
active_count: Arc<Mutex<usize>>,
) {
self.handle_output_calls.fetch_add(1, Ordering::SeqCst);
let output_map: std::collections::HashMap<_, _> = output.results.into_iter().collect();
let mut i = 0;
let mut completed = 0;
while i < batch.len() {
let req = &batch[i];
if let Some(result) = output_map.get(&req.id) {
let _ = req.sender.try_send(result.clone());
req.is_complete.store(true, Ordering::SeqCst);
batch.swap_remove(i);
completed += 1;
} else {
i += 1;
}
}
if completed > 0 {
let mut count = active_count.lock().await;
*count = count.saturating_sub(completed);
}
if !batch.is_empty() {
let remaining_items = batch.iter()
.map(|req| (req.id, req.input.clone()))
.collect();
*input = Some(TestModelInput { batch: remaining_items });
} else {
*input = None;
}
}
}
async fn create_test_request(id: usize, input: Vec<f32>) -> (TestRequest, mpsc::Receiver<Vec<f32>>) {
let (tx, rx) = mpsc::channel(1);
let request = TestRequest {
id,
input,
sender: tx,
is_complete: Arc::new(AtomicBool::new(false)),
};
(request, rx)
}
#[tokio::test]
async fn test_empty_queue() {
const BATCH_SIZE: usize = 4;
let handler = TestBatchHandler::new();
let running = Arc::new(AtomicBool::new(true));
let notifier = Arc::new(Notify::new());
let waiting_requests = Arc::new(Mutex::new(Vec::<TestRequest>::new()));
let running_clone = running.clone();
let notifier_clone = notifier.clone();
let waiting_requests_clone = waiting_requests.clone();
let hc = handler.clone();
let handle = tokio::spawn(async move {
timeout(
Duration::from_millis(300),
batch_inference_loop::<TestBatchHandler, BATCH_SIZE>(
&hc,
running_clone,
notifier_clone,
waiting_requests_clone
)
).await.unwrap_or(());
});
tokio::time::sleep(Duration::from_millis(200)).await;
running.store(false, Ordering::SeqCst);
notifier.notify_one();
handle.await.unwrap();
let (forward_calls, make_batch_calls, handle_output_calls) = handler.get_metrics();
assert_eq!(forward_calls, 0, "No forward calls should happen with empty queue");
assert_eq!(make_batch_calls, 0, "No make_batch calls should happen with empty queue");
assert_eq!(handle_output_calls, 0, "No handle_output calls should happen with empty queue");
}
#[tokio::test]
async fn test_single_request_processing() {
const BATCH_SIZE: usize = 4;
let handler = TestBatchHandler::new();
let running = Arc::new(AtomicBool::new(true));
let notifier = Arc::new(Notify::new());
let waiting_requests = Arc::new(Mutex::new(Vec::<TestRequest>::new()));
let (request, mut result_rx) = create_test_request(1, vec![1.0, 2.0, 3.0]).await;
let is_complete = request.is_complete.clone();
{
let mut requests = waiting_requests.lock().await;
requests.push(request);
}
let running_clone = running.clone();
let notifier_clone = notifier.clone();
let waiting_requests_clone = waiting_requests.clone();
let hc = handler.clone();
let handle = tokio::spawn(async move {
batch_inference_loop::<TestBatchHandler, BATCH_SIZE>(
&hc,
running_clone,
notifier_clone,
waiting_requests_clone
).await;
});
notifier.notify_one();
let result = timeout(Duration::from_millis(500), result_rx.recv()).await;
running.store(false, Ordering::SeqCst);
notifier.notify_one();
handle.await.unwrap();
assert!(result.is_ok(), "Should receive result");
let result = result.unwrap();
assert!(result.is_some(), "Result should contain data");
let output = result.unwrap();
assert_eq!(output, vec![2.0, 4.0, 6.0], "Output should be doubled input");
assert!(is_complete.load(Ordering::SeqCst), "Request should be marked as complete");
let (forward_calls, make_batch_calls, handle_output_calls) = handler.get_metrics();
assert_eq!(forward_calls, 1, "Should have one forward call");
assert_eq!(make_batch_calls, 1, "Should have one make_batch call");
assert_eq!(handle_output_calls, 1, "Should have one handle_output call");
let queue = waiting_requests.lock().await;
assert!(queue.is_empty(), "Queue should be empty after processing");
}
#[tokio::test]
async fn test_batch_processing() {
const BATCH_SIZE: usize = 4;
let handler = TestBatchHandler::new();
let running = Arc::new(AtomicBool::new(true));
let notifier = Arc::new(Notify::new());
let waiting_requests = Arc::new(Mutex::new(Vec::<TestRequest>::new()));
let (req1, mut rx1) = create_test_request(1, vec![1.0, 2.0]).await;
let (req2, mut rx2) = create_test_request(2, vec![3.0, 4.0]).await;
let (req3, mut rx3) = create_test_request(3, vec![5.0, 6.0]).await;
let is_complete1 = req1.is_complete.clone();
let is_complete2 = req2.is_complete.clone();
let is_complete3 = req3.is_complete.clone();
{
let mut requests = waiting_requests.lock().await;
requests.push(req1);
requests.push(req2);
requests.push(req3);
}
let running_clone = running.clone();
let notifier_clone = notifier.clone();
let waiting_requests_clone = waiting_requests.clone();
let hc = handler.clone();
let handle = tokio::spawn(async move {
batch_inference_loop::<TestBatchHandler, BATCH_SIZE>(
&hc,
running_clone,
notifier_clone,
waiting_requests_clone
).await;
});
notifier.notify_one();
let result1 = timeout(Duration::from_millis(500), rx1.recv()).await;
let result2 = timeout(Duration::from_millis(500), rx2.recv()).await;
let result3 = timeout(Duration::from_millis(500), rx3.recv()).await;
running.store(false, Ordering::SeqCst);
notifier.notify_one();
handle.await.unwrap();
let res1 = result1.unwrap();
let res2 = result2.unwrap();
let res3 = result3.unwrap();
assert!(res1.is_some(), "Request 1 should have result");
assert!(res2.is_some(), "Request 2 should have result");
assert!(res3.is_some(), "Request 3 should have result");
assert_eq!(res1.unwrap(), vec![2.0, 4.0], "Output 1 should be doubled input");
assert_eq!(res2.unwrap(), vec![6.0, 8.0], "Output 2 should be doubled input");
assert_eq!(res3.unwrap(), vec![10.0, 12.0], "Output 3 should be doubled input");
assert!(is_complete1.load(Ordering::SeqCst), "Request 1 should be complete");
assert!(is_complete2.load(Ordering::SeqCst), "Request 2 should be complete");
assert!(is_complete3.load(Ordering::SeqCst), "Request 3 should be complete");
let (forward_calls, make_batch_calls, handle_output_calls) = handler.get_metrics();
assert_eq!(forward_calls, 1, "Should have one forward call for the batch");
assert_eq!(make_batch_calls, 1, "Should have one make_batch call for the batch");
assert_eq!(handle_output_calls, 1, "Should have one handle_output call for the batch");
}
#[tokio::test]
async fn test_exceeding_batch_size() {
const BATCH_SIZE: usize = 2;
let handler = TestBatchHandler::new();
let running = Arc::new(AtomicBool::new(true));
let notifier = Arc::new(Notify::new());
let waiting_requests = Arc::new(Mutex::new(Vec::<TestRequest>::new()));
let (req1, mut rx1) = create_test_request(1, vec![1.0]).await;
let (req2, mut rx2) = create_test_request(2, vec![2.0]).await;
let (req3, mut rx3) = create_test_request(3, vec![3.0]).await;
{
let mut requests = waiting_requests.lock().await;
requests.push(req1);
requests.push(req2);
requests.push(req3);
}
let running_clone = running.clone();
let notifier_clone = notifier.clone();
let waiting_requests_clone = waiting_requests.clone();
let hc = handler.clone();
let handle = tokio::spawn(async move {
batch_inference_loop::<TestBatchHandler, BATCH_SIZE>(
&hc,
running_clone,
notifier_clone,
waiting_requests_clone
).await;
});
notifier.notify_one();
let result1 = timeout(Duration::from_millis(500), rx1.recv()).await;
let result2 = timeout(Duration::from_millis(500), rx2.recv()).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let result3 = timeout(Duration::from_millis(500), rx3.recv()).await;
running.store(false, Ordering::SeqCst);
notifier.notify_one();
handle.await.unwrap();
assert!(result1.is_ok() && result1.unwrap().is_some(), "Request 1 should have result");
assert!(result2.is_ok() && result2.unwrap().is_some(), "Request 2 should have result");
assert!(result3.is_ok() && result3.unwrap().is_some(), "Request 3 should have result");
let (forward_calls, make_batch_calls, handle_output_calls) = handler.get_metrics();
assert!(forward_calls >= 2, "Should have at least two forward calls");
assert!(make_batch_calls >= 2, "Should have at least two make_batch calls");
assert!(handle_output_calls >= 2, "Should have at least two handle_output calls");
}
#[tokio::test]
async fn test_dynamic_request_addition() {
const BATCH_SIZE: usize = 4;
let handler = TestBatchHandler::new();
let running = Arc::new(AtomicBool::new(true));
let notifier = Arc::new(Notify::new());
let waiting_requests = Arc::new(Mutex::new(Vec::<TestRequest>::new()));
let (req1, mut rx1) = create_test_request(1, vec![1.0]).await;
{
let mut requests = waiting_requests.lock().await;
requests.push(req1);
}
let running_clone = running.clone();
let notifier_clone = notifier.clone();
let waiting_requests_clone = waiting_requests.clone();
let hc = handler.clone();
let handle = tokio::spawn(async move {
batch_inference_loop::<TestBatchHandler, BATCH_SIZE>(
&hc,
running_clone,
notifier_clone,
waiting_requests_clone
).await;
});
notifier.notify_one();
let result1 = timeout(Duration::from_millis(500), rx1.recv()).await;
assert!(result1.is_ok() && result1.unwrap().is_some(), "First request should complete");
let (req2, mut rx2) = create_test_request(2, vec![2.0]).await;
{
let mut requests = waiting_requests.lock().await;
requests.push(req2);
}
notifier.notify_one();
let result2 = timeout(Duration::from_millis(500), rx2.recv()).await;
assert!(result2.is_ok() && result2.unwrap().is_some(), "Second request should complete");
running.store(false, Ordering::SeqCst);
notifier.notify_one();
handle.await.unwrap();
let (forward_calls, make_batch_calls, handle_output_calls) = handler.get_metrics();
assert_eq!(forward_calls, 2, "Should have two forward calls");
assert_eq!(make_batch_calls, 2, "Should have two make_batch calls");
assert_eq!(handle_output_calls, 2, "Should have two handle_output calls");
}
#[tokio::test]
async fn test_graceful_shutdown() {
const BATCH_SIZE: usize = 4;
let handler = TestBatchHandler::new();
let running = Arc::new(AtomicBool::new(true));
let notifier = Arc::new(Notify::new());
let waiting_requests = Arc::new(Mutex::new(Vec::<TestRequest>::new()));
let (req1, _) = create_test_request(1, vec![1.0]).await;
let (req2, _) = create_test_request(2, vec![2.0]).await;
{
let mut requests = waiting_requests.lock().await;
requests.push(req1);
requests.push(req2);
}
let running_clone = running.clone();
let notifier_clone = notifier.clone();
let waiting_requests_clone = waiting_requests.clone();
let hc = handler.clone();
let handle = tokio::spawn(async move {
batch_inference_loop::<TestBatchHandler, BATCH_SIZE>(
&hc,
running_clone,
notifier_clone,
waiting_requests_clone
).await;
});
tokio::time::sleep(Duration::from_millis(50)).await;
running.store(false, Ordering::SeqCst);
notifier.notify_one();
let shutdown_result = timeout(Duration::from_millis(300), handle).await;
assert!(shutdown_result.is_ok(), "Loop should terminate gracefully");
assert!(shutdown_result.unwrap().is_ok(), "Loop should complete without error");
}
}