use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use pensieve_core::tenant::TenantId;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
fn limiter() -> Option<&'static Arc<Semaphore>> {
static S: OnceLock<Option<Arc<Semaphore>>> = OnceLock::new();
S.get_or_init(|| {
let n = std::env::var("PENSIEVE_QUERY_MAX_CONCURRENT")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
if n == 0 {
None
} else {
Some(Arc::new(Semaphore::new(n)))
}
})
.as_ref()
}
fn retry_after_secs() -> u64 {
std::env::var("PENSIEVE_QUERY_RETRY_AFTER_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(1)
.max(1)
}
pub struct QueryPermit(#[allow(dead_code)] Option<OwnedSemaphorePermit>);
pub fn acquire() -> Result<QueryPermit, u64> {
match limiter() {
None => Ok(QueryPermit(None)),
Some(sem) => match Arc::clone(sem).try_acquire_owned() {
Ok(p) => Ok(QueryPermit(Some(p))),
Err(_) => Err(retry_after_secs()),
},
}
}
fn agent_limiter() -> Option<&'static Arc<Semaphore>> {
static S: OnceLock<Option<Arc<Semaphore>>> = OnceLock::new();
S.get_or_init(|| {
let n = std::env::var("PENSIEVE_AGENT_MAX_CONCURRENT")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
if n == 0 {
None
} else {
Some(Arc::new(Semaphore::new(n)))
}
})
.as_ref()
}
fn agent_retry_after_secs() -> u64 {
std::env::var("PENSIEVE_AGENT_RETRY_AFTER_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(5)
.max(1)
}
pub struct AgentRunPermit(#[allow(dead_code)] Option<OwnedSemaphorePermit>);
pub fn acquire_agent_run() -> Result<AgentRunPermit, u64> {
match agent_limiter() {
None => Ok(AgentRunPermit(None)),
Some(sem) => match Arc::clone(sem).try_acquire_owned() {
Ok(p) => Ok(AgentRunPermit(Some(p))),
Err(_) => Err(agent_retry_after_secs()),
},
}
}
fn per_tenant_query_limit() -> Option<usize> {
static N: OnceLock<Option<usize>> = OnceLock::new();
*N.get_or_init(|| {
let n = std::env::var("PENSIEVE_QUERY_MAX_CONCURRENT_PER_TENANT")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
if n == 0 {
None
} else {
Some(n)
}
})
}
fn tenant_query_semaphores() -> &'static Mutex<HashMap<TenantId, (usize, Arc<Semaphore>)>> {
static M: OnceLock<Mutex<HashMap<TenantId, (usize, Arc<Semaphore>)>>> = OnceLock::new();
M.get_or_init(|| Mutex::new(HashMap::new()))
}
fn effective_tenant_semaphore(
tenant: TenantId,
override_limit: Option<u32>,
env_default: Option<usize>,
map: &'static Mutex<HashMap<TenantId, (usize, Arc<Semaphore>)>>,
) -> Option<Arc<Semaphore>> {
let n = override_limit.map(|v| v as usize).or(env_default)?;
let Ok(mut g) = map.lock() else {
return None; };
match g.get(&tenant) {
Some((size, sem)) if *size == n => Some(Arc::clone(sem)),
_ => {
let sem = Arc::new(Semaphore::new(n));
g.insert(tenant, (n, Arc::clone(&sem)));
Some(sem)
}
}
}
pub fn acquire_for_tenant(tenant: TenantId) -> Result<QueryPermit, u64> {
let sem = match effective_tenant_semaphore(
tenant,
crate::quota_cache::query_limit_override(tenant),
per_tenant_query_limit(),
tenant_query_semaphores(),
) {
Some(s) => s,
None => return Ok(QueryPermit(None)),
};
match sem.try_acquire_owned() {
Ok(p) => Ok(QueryPermit(Some(p))),
Err(_) => Err(retry_after_secs()),
}
}
fn per_tenant_agent_limit() -> Option<usize> {
static N: OnceLock<Option<usize>> = OnceLock::new();
*N.get_or_init(|| {
let n = std::env::var("PENSIEVE_AGENT_MAX_CONCURRENT_PER_TENANT")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
if n == 0 {
None
} else {
Some(n)
}
})
}
fn tenant_agent_semaphores() -> &'static Mutex<HashMap<TenantId, (usize, Arc<Semaphore>)>> {
static M: OnceLock<Mutex<HashMap<TenantId, (usize, Arc<Semaphore>)>>> = OnceLock::new();
M.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn acquire_agent_run_for_tenant(tenant: TenantId) -> Result<AgentRunPermit, u64> {
let sem = match effective_tenant_semaphore(
tenant,
crate::quota_cache::agent_limit_override(tenant),
per_tenant_agent_limit(),
tenant_agent_semaphores(),
) {
Some(s) => s,
None => return Ok(AgentRunPermit(None)),
};
match sem.try_acquire_owned() {
Ok(p) => Ok(AgentRunPermit(Some(p))),
Err(_) => Err(agent_retry_after_secs()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_when_unset_grants_inert_permits() {
for _ in 0..1000 {
assert!(acquire().is_ok());
}
}
#[test]
fn agent_run_disabled_when_unset_grants_inert_permits() {
for _ in 0..1000 {
assert!(acquire_agent_run().is_ok());
}
}
#[test]
fn per_tenant_disabled_when_unset_grants_inert_permits() {
let t = pensieve_core::tenant::DEFAULT_TENANT;
for _ in 0..1000 {
assert!(acquire_for_tenant(t).is_ok());
}
}
#[test]
fn tenant_quota_override_caps_query_without_env() {
let t = TenantId::from_uuid(uuid::Uuid::from_u128(0x9e57_0000_0000_0000_0000_0000_0000_0001));
crate::quota_cache::clear_for_test();
crate::quota_cache::set_for_test(pensieve_core::catalog::TenantQuota {
tenant: t,
max_query_concurrent: Some(1),
max_agent_concurrent: None,
updated_at: chrono::Utc::now(),
});
let p1 = acquire_for_tenant(t);
assert!(p1.is_ok(), "first query admitted under the tenant's quota of 1");
assert!(
acquire_for_tenant(t).is_err(),
"second query rejected — tenant at its configured quota of 1"
);
drop(p1);
assert!(
acquire_for_tenant(t).is_ok(),
"slot frees when the first permit drops"
);
crate::quota_cache::clear_for_test();
}
#[test]
fn per_tenant_agent_disabled_when_unset_grants_inert_permits() {
let t = pensieve_core::tenant::DEFAULT_TENANT;
for _ in 0..1000 {
assert!(acquire_agent_run_for_tenant(t).is_ok());
}
}
#[tokio::test]
async fn per_tenant_semaphores_isolate_tenants() {
let a = Arc::new(Semaphore::new(1));
let b = Arc::new(Semaphore::new(1));
let a1 = Arc::clone(&a).try_acquire_owned();
assert!(a1.is_ok(), "tenant A admitted");
assert!(
Arc::clone(&a).try_acquire_owned().is_err(),
"tenant A now saturated"
);
assert!(
Arc::clone(&b).try_acquire_owned().is_ok(),
"tenant B unaffected by A's saturation"
);
}
#[tokio::test]
async fn semaphore_admits_up_to_capacity_then_rejects() {
let sem = Arc::new(Semaphore::new(2));
let p1 = Arc::clone(&sem).try_acquire_owned();
let p2 = Arc::clone(&sem).try_acquire_owned();
assert!(p1.is_ok() && p2.is_ok(), "first two admitted");
assert!(
Arc::clone(&sem).try_acquire_owned().is_err(),
"third rejected at capacity"
);
drop(p1);
assert!(
Arc::clone(&sem).try_acquire_owned().is_ok(),
"slot freed after a permit drops"
);
}
}