use crate::call::{Dialplan, TransactionCookie, TrunkContext};
use crate::proxy::call::{DialplanInspector, DialplanVerdict};
use lru::LruCache;
use rsipstack::sip::Request as SipRequest;
use std::num::NonZeroUsize;
use std::sync::{Mutex, OnceLock};
const NUMBER_POOL_USAGE_CAP: usize = 10_000;
static USAGE: OnceLock<Mutex<LruCache<String, u64>>> = OnceLock::new();
fn usage_counter() -> &'static Mutex<LruCache<String, u64>> {
USAGE.get_or_init(|| {
Mutex::new(LruCache::new(
NonZeroUsize::new(NUMBER_POOL_USAGE_CAP).expect("non-zero cap"),
))
})
}
pub struct NumberPoolInspector;
impl NumberPoolInspector {
pub fn new() -> Self {
Self
}
}
impl Default for NumberPoolInspector {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl DialplanInspector for NumberPoolInspector {
async fn inspect_dialplan(
&self,
mut dialplan: Dialplan,
cookie: &TransactionCookie,
_original: &SipRequest,
) -> DialplanVerdict {
let Some(ctx) = cookie.get_extension::<TrunkContext>() else {
return DialplanVerdict::Continue(dialplan);
};
if ctx.did_numbers.is_empty() {
return DialplanVerdict::Continue(dialplan);
}
let counter = usage_counter();
let did = {
let usage = counter.lock().unwrap();
ctx.did_numbers
.iter()
.min_by_key(|d| usage.peek(d.as_str()).copied().unwrap_or(0))
.cloned()
.expect("did_numbers is non-empty")
};
if let Ok(uri) = format!("sip:{}", did).parse() {
tracing::info!(
trunk = %ctx.name,
did = %did,
"number pool: assigned least-used DID as caller"
);
dialplan.caller = Some(uri);
}
let mut usage = counter.lock().unwrap();
if let Some(count) = usage.get_mut(&did) {
*count += 1;
} else {
usage.put(did.clone(), 1);
}
DialplanVerdict::Continue(dialplan)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usage_cache_is_bounded_and_increments() {
let counter = usage_counter();
let cap = NonZeroUsize::new(NUMBER_POOL_USAGE_CAP).unwrap();
assert_eq!(counter.lock().unwrap().cap(), cap);
let bump = |did: &str| {
let mut usage = counter.lock().unwrap();
if let Some(count) = usage.get_mut(did) {
*count += 1;
} else {
usage.put(did.to_string(), 1);
}
};
let read = |did: &str| -> Option<u64> { counter.lock().unwrap().peek(did).copied() };
bump("1001");
bump("1001");
bump("1002");
assert_eq!(read("1001"), Some(2));
assert_eq!(read("1002"), Some(1));
{
let mut usage = counter.lock().unwrap();
for i in 0..NUMBER_POOL_USAGE_CAP {
usage.put(format!("did-{i}"), 1);
}
assert_eq!(usage.len(), NUMBER_POOL_USAGE_CAP);
}
assert!(read("1001").is_none(), "1001 should have been evicted");
assert!(read("1002").is_none(), "1002 should have been evicted");
bump("1001");
assert_eq!(read("1001"), Some(1));
}
}