use std::sync::Arc;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use super::RequestHandler;
use crate::error::{ServerError, ServerResult};
use crate::store::tenant::TenantContext;
impl RequestHandler {
pub(crate) async fn acquire_tenant_slot(&self) -> ServerResult<Option<OwnedSemaphorePermit>> {
let Some(cap) = self
.tenant_limits()
.and_then(|limits| limits.max_concurrent_tasks)
else {
return Ok(None);
};
let tenant = TenantContext::current();
let semaphore = self.tenant_slot_semaphore(&tenant, cap).await;
semaphore.try_acquire_owned().map(Some).map_err(|_| {
ServerError::Overloaded(format!(
"tenant '{tenant}' already has {cap} task(s) in flight"
))
})
}
async fn tenant_slot_semaphore(&self, tenant: &str, cap: usize) -> Arc<Semaphore> {
let mut slots = self.tenant_slots.write().await;
if slots.len() >= self.limits.max_context_locks {
slots.retain(|_, sem| Arc::strong_count(sem) > 1);
}
Arc::clone(
slots
.entry(tenant.to_owned())
.or_insert_with(|| Arc::new(Semaphore::new(cap))),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent_executor;
use crate::builder::RequestHandlerBuilder;
use crate::handler::HandlerLimits;
struct Idle;
agent_executor!(Idle, |_ctx, _queue| async { Ok(()) });
fn handler_with_lock_cap(max: usize) -> RequestHandler {
RequestHandlerBuilder::new(Idle)
.with_handler_limits(HandlerLimits::default().with_max_context_locks(max))
.build()
.expect("handler builds")
}
#[tokio::test]
async fn the_sweep_keeps_busy_tenants_and_drops_idle_ones() {
let handler = handler_with_lock_cap(2);
let busy = handler.tenant_slot_semaphore("busy", 1).await;
let _permit = Arc::clone(&busy)
.try_acquire_owned()
.expect("a fresh semaphore has a free slot");
drop(busy);
drop(handler.tenant_slot_semaphore("idle", 1).await);
assert_eq!(
handler.tenant_slots.read().await.len(),
2,
"below the threshold nothing is swept"
);
drop(handler.tenant_slot_semaphore("third", 1).await);
let kept: std::collections::BTreeSet<String> =
handler.tenant_slots.read().await.keys().cloned().collect();
assert!(
kept.contains("busy"),
"a tenant with a permit outstanding is never swept"
);
assert!(
!kept.contains("idle"),
"a tenant only the map holds is swept at the threshold"
);
assert!(
kept.contains("third"),
"the tenant that triggered the sweep is kept"
);
assert_eq!(
kept.len(),
2,
"exactly the busy tenant and the new one remain"
);
}
}