use std::collections::HashMap;
use std::net::TcpStream;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
const DEFAULT_PER_KEY_CAP: usize = 4;
const DEFAULT_GLOBAL_CAP: usize = 32;
static PER_KEY_CAP: AtomicUsize = AtomicUsize::new(DEFAULT_PER_KEY_CAP);
static GLOBAL_CAP: AtomicUsize = AtomicUsize::new(DEFAULT_GLOBAL_CAP);
pub fn configure(per_key: usize, total: usize) {
PER_KEY_CAP.store(per_key.max(1), Ordering::Relaxed);
GLOBAL_CAP.store(total.max(1), Ordering::Relaxed);
}
pub(crate) fn per_key_cap() -> usize {
PER_KEY_CAP.load(Ordering::Relaxed)
}
pub(crate) fn global_cap() -> usize {
GLOBAL_CAP.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) static CAP_TEST_LOCK: Mutex<()> = Mutex::new(());
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub(crate) struct Key {
pub scheme: String,
pub host: String,
pub port: u16,
pub effective_target: Option<(String, u16)>,
pub partition: Option<String>,
}
pub(crate) struct CorePool<C> {
entries: HashMap<Key, Vec<C>>,
}
impl<C> CorePool<C> {
fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub(crate) fn checkout(&mut self, key: &Key) -> Option<C> {
let bucket = self.entries.get_mut(key)?;
let c = bucket.pop();
if bucket.is_empty() {
self.entries.remove(key);
}
c
}
pub(crate) fn release(&mut self, key: Key, conn: C) {
let total: usize = self.entries.values().map(Vec::len).sum();
if total >= global_cap() {
return;
}
let bucket = self.entries.entry(key).or_default();
if bucket.len() >= per_key_cap() {
return;
}
bucket.push(conn);
}
#[cfg(test)]
fn total_len(&self) -> usize {
self.entries.values().map(Vec::len).sum()
}
}
static POOL_CORE_PLAIN: OnceLock<Mutex<CorePool<TcpStream>>> = OnceLock::new();
pub(crate) fn core_plain() -> &'static Mutex<CorePool<TcpStream>> {
POOL_CORE_PLAIN.get_or_init(|| Mutex::new(CorePool::new()))
}
#[cfg(feature = "rustls-tls")]
pub(crate) type CoreTlsEngine = crate::proto::tls::RustlsEngine;
#[cfg(all(feature = "purecrypto-tls", not(feature = "rustls-tls")))]
pub(crate) type CoreTlsEngine = crate::proto::tls::PurecryptoEngine;
#[cfg(any(feature = "rustls-tls", feature = "purecrypto-tls"))]
pub(crate) type CoreTlsConn = (TcpStream, CoreTlsEngine);
#[cfg(any(feature = "rustls-tls", feature = "purecrypto-tls"))]
static POOL_CORE_TLS: OnceLock<Mutex<CorePool<CoreTlsConn>>> = OnceLock::new();
#[cfg(any(feature = "rustls-tls", feature = "purecrypto-tls"))]
pub(crate) fn core_tls() -> &'static Mutex<CorePool<CoreTlsConn>> {
POOL_CORE_TLS.get_or_init(|| Mutex::new(CorePool::new()))
}
#[cfg(test)]
mod tests {
use super::*;
fn k(host: &str, port: u16) -> Key {
Key {
scheme: "http".into(),
host: host.into(),
port,
effective_target: None,
partition: None,
}
}
use super::CAP_TEST_LOCK as CAP_LOCK;
#[test]
fn global_cap_enforced_across_keys() {
let _g = CAP_LOCK.lock().unwrap_or_else(|e| e.into_inner());
configure(DEFAULT_PER_KEY_CAP, DEFAULT_GLOBAL_CAP);
let cap = global_cap();
let mut p: CorePool<u32> = CorePool::new();
for i in 0..(cap as u32 + 5) {
p.release(k("h", i as u16), i);
}
assert_eq!(p.total_len(), cap);
}
#[test]
fn core_pool_lifo_and_caps() {
let _g = CAP_LOCK.lock().unwrap_or_else(|e| e.into_inner());
configure(DEFAULT_PER_KEY_CAP, DEFAULT_GLOBAL_CAP);
let mut p: CorePool<u32> = CorePool::new();
p.release(k("h", 80), 1);
p.release(k("h", 80), 2);
assert_eq!(p.checkout(&k("h", 80)), Some(2));
assert_eq!(p.checkout(&k("h", 80)), Some(1));
assert_eq!(p.checkout(&k("h", 80)), None);
assert_eq!(p.total_len(), 0);
let cap = per_key_cap();
for i in 0..(cap as u32 + 3) {
p.release(k("h", 80), i);
}
assert_eq!(p.total_len(), cap);
}
#[test]
fn configure_sets_and_clamps_caps() {
let _g = CAP_LOCK.lock().unwrap_or_else(|e| e.into_inner());
configure(2, 5);
assert_eq!(per_key_cap(), 2);
assert_eq!(global_cap(), 5);
configure(0, 0);
assert_eq!(per_key_cap(), 1);
assert_eq!(global_cap(), 1);
configure(DEFAULT_PER_KEY_CAP, DEFAULT_GLOBAL_CAP);
}
}