use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::Mutex;
use tokio::sync::Semaphore;
use crate::error::{Error, Result};
use crate::stream::TorStream;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct PoolKey {
pub host: String,
pub port: u16,
pub is_tls: bool,
}
impl PoolKey {
pub fn new(host: impl Into<String>, port: u16, is_tls: bool) -> Self {
Self {
host: host.into(),
port,
is_tls,
}
}
}
struct PooledConnection {
stream: TorStream,
created_at: Instant,
last_used: Instant,
}
impl PooledConnection {
fn new(stream: TorStream) -> Self {
let now = Instant::now();
Self {
stream,
created_at: now,
last_used: now,
}
}
fn is_expired(&self, max_age: Duration, idle_timeout: Duration) -> bool {
let now = Instant::now();
now.duration_since(self.created_at) > max_age
|| now.duration_since(self.last_used) > idle_timeout
}
}
#[derive(Debug, Clone)]
pub struct PoolConfig {
pub max_connections_per_host: usize,
pub max_total_connections: usize,
pub max_connection_age: Duration,
pub idle_timeout: Duration,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
max_connections_per_host: 4,
max_total_connections: 20,
max_connection_age: Duration::from_secs(300), idle_timeout: Duration::from_secs(60), }
}
}
pub struct ConnectionPool {
connections: Mutex<HashMap<PoolKey, Vec<PooledConnection>>>,
semaphore: Arc<Semaphore>,
config: PoolConfig,
}
impl ConnectionPool {
pub fn new(config: PoolConfig) -> Self {
Self {
connections: Mutex::new(HashMap::new()),
semaphore: Arc::new(Semaphore::new(config.max_total_connections)),
config,
}
}
pub fn get(&self, key: &PoolKey) -> Option<TorStream> {
let mut connections = self.connections.lock();
if let Some(pool) = connections.get_mut(key) {
while let Some(mut conn) = pool.pop() {
if !conn.is_expired(self.config.max_connection_age, self.config.idle_timeout) {
conn.last_used = Instant::now();
return Some(conn.stream);
}
}
}
None
}
pub fn put(&self, key: PoolKey, stream: TorStream) {
let mut connections = self.connections.lock();
let pool = connections.entry(key).or_default();
if pool.len() < self.config.max_connections_per_host {
pool.push(PooledConnection::new(stream));
}
}
pub async fn acquire_permit(&self) -> Result<PoolPermit> {
match tokio::time::timeout(
Duration::from_secs(30),
self.semaphore.clone().acquire_owned(),
)
.await
{
Ok(Ok(permit)) => Ok(PoolPermit { _permit: permit }),
Ok(Err(_)) => Err(Error::PoolExhausted {
max_connections: self.config.max_total_connections,
}),
Err(_) => Err(Error::timeout("connection pool", Duration::from_secs(30))),
}
}
pub fn cleanup(&self) {
let mut connections = self.connections.lock();
for pool in connections.values_mut() {
pool.retain(|conn| {
!conn.is_expired(self.config.max_connection_age, self.config.idle_timeout)
});
}
connections.retain(|_, pool| !pool.is_empty());
}
pub fn len(&self) -> usize {
self.connections.lock().values().map(|v| v.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
self.connections.lock().clear();
}
}
pub struct PoolPermit {
_permit: tokio::sync::OwnedSemaphorePermit,
}
impl Default for ConnectionPool {
fn default() -> Self {
Self::new(PoolConfig::default())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn test_pool_key_equality() {
let k1 = PoolKey::new("example.com", 443, true);
let k2 = PoolKey::new("example.com", 443, true);
let k3 = PoolKey::new("example.com", 80, false);
assert_eq!(k1, k2);
assert_ne!(k1, k3);
}
#[test]
fn test_pool_config_default() {
let config = PoolConfig::default();
assert_eq!(config.max_connections_per_host, 4);
assert_eq!(config.max_total_connections, 20);
}
}