litellm-rs 0.9.0

A high-performance AI Gateway written in Rust, providing OpenAI-compatible APIs with intelligent routing, load balancing, and enterprise features
Documentation
use tokio::sync::{Mutex, MutexGuard};

struct BatchState<T> {
    pending: Vec<T>,
    in_flight: Vec<T>,
}

impl<T> Default for BatchState<T> {
    fn default() -> Self {
        Self {
            pending: Vec::new(),
            in_flight: Vec::new(),
        }
    }
}

pub(super) struct DurableBatch<T> {
    state: Mutex<BatchState<T>>,
    flush_lock: Mutex<()>,
    capacity: usize,
}

#[derive(Debug, PartialEq, Eq)]
pub(super) struct BatchFull<T> {
    capacity: usize,
    value: T,
}

impl<T> BatchFull<T> {
    pub(super) fn into_value(self) -> T {
        self.value
    }
}

impl<T> std::fmt::Display for BatchFull<T> {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            formatter,
            "export batch capacity {} reached; event was not buffered",
            self.capacity
        )
    }
}

impl<T> DurableBatch<T> {
    pub(super) fn new(capacity: usize) -> Self {
        Self {
            state: Mutex::new(BatchState::default()),
            flush_lock: Mutex::new(()),
            capacity: capacity.max(1),
        }
    }
}

impl<T: Clone> DurableBatch<T> {
    pub(super) async fn push(&self, value: T) -> Result<usize, BatchFull<T>> {
        let mut state = self.state.lock().await;
        if state.pending.len().saturating_add(state.in_flight.len()) >= self.capacity {
            return Err(BatchFull {
                capacity: self.capacity,
                value,
            });
        }
        state.pending.push(value);
        Ok(state.pending.len())
    }

    pub(super) async fn serialize_flush(&self) -> MutexGuard<'_, ()> {
        self.flush_lock.lock().await
    }

    pub(super) async fn batch_for_export(&self) -> Vec<T> {
        let mut state = self.state.lock().await;
        if !state.pending.is_empty() {
            let pending = std::mem::take(&mut state.pending);
            state.in_flight.extend(pending);
        }
        state.in_flight.clone()
    }

    pub(super) async fn acknowledge(&self) {
        self.state.lock().await.in_flight.clear();
    }

    #[cfg(test)]
    pub(super) async fn snapshot(&self) -> Vec<T> {
        let state = self.state.lock().await;
        state
            .in_flight
            .iter()
            .chain(&state.pending)
            .cloned()
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use tokio::sync::Notify;

    #[tokio::test]
    async fn canceled_export_preserves_in_flight_batch_and_retry_order() {
        let batch = Arc::new(DurableBatch::new(2));
        batch.push(1).await.unwrap();
        let entered = Arc::new(Notify::new());
        let task = tokio::spawn({
            let batch = Arc::clone(&batch);
            let entered = Arc::clone(&entered);
            async move {
                let _flush = batch.serialize_flush().await;
                assert_eq!(batch.batch_for_export().await, [1]);
                entered.notify_one();
                std::future::pending::<()>().await;
            }
        });
        entered.notified().await;
        batch.push(2).await.unwrap();
        task.abort();
        assert!(task.await.unwrap_err().is_cancelled());
        assert_eq!(batch.snapshot().await, [1, 2]);

        let _flush = batch.serialize_flush().await;
        assert_eq!(batch.batch_for_export().await, [1, 2]);
        batch.acknowledge().await;
        assert!(batch.batch_for_export().await.is_empty());
    }

    #[tokio::test]
    async fn failed_export_retention_is_bounded() {
        let batch = DurableBatch::new(2);
        batch.push(1).await.unwrap();
        assert_eq!(batch.batch_for_export().await, [1]);
        batch.push(2).await.unwrap();

        let error = batch.push(3).await.unwrap_err();
        assert_eq!(
            error,
            BatchFull {
                capacity: 2,
                value: 3
            }
        );
        assert_eq!(batch.snapshot().await, [1, 2]);
        assert_eq!(batch.batch_for_export().await, [1, 2]);
    }
}