use super::{Policy, PolicyOutput, PolicyResult};
use crate::std::sync::Arc;
use parking_lot::Mutex;
use rama_utils::backoff::Backoff;
#[derive(Debug, Clone)]
pub struct ConcurrentPolicy<B, C> {
tracker: C,
backoff: B,
}
impl<B, C> ConcurrentPolicy<B, C> {
pub fn with_backoff(backoff: B, tracker: C) -> Self {
Self { tracker, backoff }
}
}
impl<C> ConcurrentPolicy<(), C> {
pub const fn new(tracker: C) -> Self {
Self {
tracker,
backoff: (),
}
}
}
impl ConcurrentPolicy<(), ConcurrentCounter> {
#[must_use]
pub fn max(max: usize) -> Self {
Self {
tracker: ConcurrentCounter::new(max),
backoff: (),
}
}
}
impl<B> ConcurrentPolicy<B, ConcurrentCounter> {
pub fn max_with_backoff(max: usize, backoff: B) -> Self {
Self {
tracker: ConcurrentCounter::new(max),
backoff,
}
}
}
impl<B, C, Input> Policy<Input> for ConcurrentPolicy<B, C>
where
B: Backoff,
Input: Send + 'static,
C: ConcurrentTracker,
{
type Guard = C::Guard;
type Error = C::Error;
async fn check(&self, input: Input) -> PolicyResult<Input, Self::Guard, Self::Error> {
let tracker_err = match self.tracker.try_access() {
Ok(guard) => {
return PolicyResult {
input,
output: PolicyOutput::Ready(guard),
};
}
Err(err) => err,
};
let output = if !self.backoff.next_backoff().await {
PolicyOutput::Abort(tracker_err)
} else {
PolicyOutput::Retry
};
PolicyResult { input, output }
}
}
rama_utils::macros::error::static_str_error! {
#[doc = "serve aborted due to exhausted concurrency limit"]
pub struct LimitReached;
}
pub trait ConcurrentTracker: Send + Sync + 'static {
type Guard: Send + 'static;
type Error: Send + 'static;
fn try_access(&self) -> Result<Self::Guard, Self::Error>;
}
#[derive(Debug, Clone)]
pub struct ConcurrentCounter {
max: usize,
current: Arc<Mutex<usize>>,
}
impl ConcurrentCounter {
#[must_use]
pub fn new(max: usize) -> Self {
Self {
max,
current: Arc::new(Mutex::new(0)),
}
}
}
impl ConcurrentTracker for ConcurrentCounter {
type Guard = ConcurrentCounterGuard;
type Error = LimitReached;
fn try_access(&self) -> Result<Self::Guard, Self::Error> {
let mut current = self.current.lock();
if *current < self.max {
*current += 1;
Ok(ConcurrentCounterGuard {
current: self.current.clone(),
})
} else {
Err(LimitReached)
}
}
}
#[derive(Debug)]
pub struct ConcurrentCounterGuard {
current: Arc<Mutex<usize>>,
}
impl Drop for ConcurrentCounterGuard {
fn drop(&mut self) {
let mut current = self.current.lock();
*current -= 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_ready<R, G, E>(result: PolicyResult<R, G, E>) -> Option<G> {
let guard = match result.output {
PolicyOutput::Ready(guard) => Some(guard),
PolicyOutput::Abort(_) | PolicyOutput::Retry => None,
};
assert!(guard.is_some(), "unexpected output, expected ready");
guard
}
fn assert_abort<R, G, E>(result: &PolicyResult<R, G, E>) {
assert!(
matches!(result.output, PolicyOutput::Abort(_)),
"unexpected output, expected abort"
);
}
#[tokio::test]
async fn concurrent_policy_zero() {
let policy = ConcurrentPolicy::max(0);
assert_abort(&policy.check(()).await);
}
#[tokio::test]
async fn concurrent_policy() {
let policy = ConcurrentPolicy::max(2);
let guard_1 = assert_ready(policy.check(()).await);
let guard_2 = assert_ready(policy.check(()).await);
assert_abort(&policy.check(()).await);
drop(guard_1);
let _guard_3 = assert_ready(policy.check(()).await);
assert_abort(&policy.check(()).await);
drop(guard_2);
drop(assert_ready(policy.check(()).await));
}
#[tokio::test]
async fn concurrent_policy_clone() {
let policy = ConcurrentPolicy::max(2);
let policy_clone = policy.clone();
let guard_1 = assert_ready(policy.check(()).await);
let _guard_2 = assert_ready(policy_clone.check(()).await);
assert_abort(&policy.check(()).await);
drop(guard_1);
drop(assert_ready(policy.check(()).await));
}
}