use crate::{Result, QsshError, QsshConfig, client::QsshClient};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use tokio::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub struct PoolConfig {
pub max_per_host: usize,
pub max_total: usize,
pub idle_timeout: Duration,
pub max_lifetime: Duration,
pub health_check: bool,
pub health_check_interval: Duration,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
max_per_host: 5,
max_total: 20,
idle_timeout: Duration::from_secs(300), max_lifetime: Duration::from_secs(3600), health_check: true,
health_check_interval: Duration::from_secs(30),
}
}
}
struct PooledConnection {
client: QsshClient,
created_at: Instant,
last_used: Instant,
use_count: usize,
host_key: String,
}
impl PooledConnection {
fn new(client: QsshClient, host_key: String) -> Self {
let now = Instant::now();
Self {
client,
created_at: now,
last_used: now,
use_count: 0,
host_key,
}
}
fn is_expired(&self, config: &PoolConfig) -> bool {
let now = Instant::now();
if now.duration_since(self.created_at) > config.max_lifetime {
return true;
}
if now.duration_since(self.last_used) > config.idle_timeout {
return true;
}
false
}
async fn is_healthy(&self) -> bool {
if let Some(transport) = self.client.transport() {
use crate::transport::Message;
let ping = Message::Ping(rand::random());
transport.send_message(&ping).await.is_ok()
} else {
false
}
}
}
pub struct ConnectionPool {
config: PoolConfig,
connections: Arc<RwLock<HashMap<String, Vec<Arc<Mutex<PooledConnection>>>>>>,
total_count: Arc<Mutex<usize>>,
}
impl ConnectionPool {
pub fn new(config: PoolConfig) -> Self {
let pool = Self {
config: config.clone(),
connections: Arc::new(RwLock::new(HashMap::new())),
total_count: Arc::new(Mutex::new(0)),
};
if config.health_check {
let pool_clone = pool.clone();
tokio::spawn(async move {
pool_clone.health_check_task().await;
});
}
let pool_clone = pool.clone();
tokio::spawn(async move {
pool_clone.cleanup_task().await;
});
pool
}
pub async fn get(&self, config: &QsshConfig) -> Result<PoolHandle> {
let host_key = Self::make_host_key(config);
if let Some(conn) = self.get_existing(&host_key).await? {
return Ok(conn);
}
self.create_new(config, host_key).await
}
async fn get_existing(&self, host_key: &str) -> Result<Option<PoolHandle>> {
let connections = self.connections.read().await;
if let Some(host_conns) = connections.get(host_key) {
for conn_arc in host_conns {
let mut conn = conn_arc.lock().await;
if conn.is_expired(&self.config) {
continue;
}
if self.config.health_check && !conn.is_healthy().await {
continue;
}
conn.last_used = Instant::now();
conn.use_count += 1;
return Ok(Some(PoolHandle {
pool: self.clone(),
connection: conn_arc.clone(),
}));
}
}
Ok(None)
}
async fn create_new(&self, config: &QsshConfig, host_key: String) -> Result<PoolHandle> {
let total = *self.total_count.lock().await;
if total >= self.config.max_total {
return Err(QsshError::Connection("Connection pool limit reached".into()));
}
{
let connections = self.connections.read().await;
if let Some(host_conns) = connections.get(&host_key) {
if host_conns.len() >= self.config.max_per_host {
return Err(QsshError::Connection(
format!("Per-host connection limit reached for {}", host_key)
));
}
}
}
let mut client = QsshClient::new(config.clone());
client.connect().await?;
let pooled = Arc::new(Mutex::new(PooledConnection::new(client, host_key.clone())));
{
let mut connections = self.connections.write().await;
connections.entry(host_key.clone())
.or_insert_with(Vec::new)
.push(pooled.clone());
}
{
let mut total = self.total_count.lock().await;
*total += 1;
}
Ok(PoolHandle {
pool: self.clone(),
connection: pooled,
})
}
fn make_host_key(config: &QsshConfig) -> String {
format!("{}@{}", config.username, config.server)
}
async fn health_check_task(&self) {
let mut interval = tokio::time::interval(self.config.health_check_interval);
loop {
interval.tick().await;
let connections = self.connections.read().await;
for (_host, conns) in connections.iter() {
for conn_arc in conns {
let conn = conn_arc.lock().await;
if !conn.is_healthy().await {
log::warn!("Unhealthy connection detected for {}", conn.host_key);
}
}
}
}
}
async fn cleanup_task(&self) {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
let mut connections = self.connections.write().await;
let mut total_removed = 0;
for (_host, conns) in connections.iter_mut() {
conns.retain(|conn_arc| {
if let Ok(conn) = conn_arc.try_lock() {
if conn.is_expired(&self.config) {
log::debug!("Removing expired connection for {}", conn.host_key);
total_removed += 1;
return false;
}
}
true
});
}
connections.retain(|_, conns| !conns.is_empty());
if total_removed > 0 {
let mut total = self.total_count.lock().await;
*total = (*total).saturating_sub(total_removed);
}
}
}
pub async fn stats(&self) -> PoolStats {
let connections = self.connections.read().await;
let total = *self.total_count.lock().await;
let mut stats = PoolStats {
total_connections: total,
connections_per_host: HashMap::new(),
total_use_count: 0,
expired_count: 0,
};
for (host, conns) in connections.iter() {
stats.connections_per_host.insert(host.clone(), conns.len());
for conn_arc in conns {
if let Ok(conn) = conn_arc.try_lock() {
stats.total_use_count += conn.use_count;
if conn.is_expired(&self.config) {
stats.expired_count += 1;
}
}
}
}
stats
}
pub async fn clear(&self) {
let mut connections = self.connections.write().await;
connections.clear();
let mut total = self.total_count.lock().await;
*total = 0;
}
}
impl Clone for ConnectionPool {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
connections: self.connections.clone(),
total_count: self.total_count.clone(),
}
}
}
pub struct PoolHandle {
pool: ConnectionPool,
connection: Arc<Mutex<PooledConnection>>,
}
impl PoolHandle {
pub async fn with_client<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut QsshClient) -> R,
{
let mut conn = self.connection.lock().await;
f(&mut conn.client)
}
pub fn release(self) {
drop(self);
}
}
#[derive(Debug, Clone)]
pub struct PoolStats {
pub total_connections: usize,
pub connections_per_host: HashMap<String, usize>,
pub total_use_count: usize,
pub expired_count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_pool_config_default() {
let config = PoolConfig::default();
assert_eq!(config.max_per_host, 5);
assert_eq!(config.max_total, 20);
assert_eq!(config.idle_timeout, Duration::from_secs(300));
}
#[tokio::test]
async fn test_host_key_generation() {
let config = QsshConfig {
server: "test.example.com:22".to_string(),
username: "alice".to_string(),
password: None,
port_forwards: vec![],
use_qkd: false,
pq_algorithm: crate::PqAlgorithm::Falcon512,
key_rotation_interval: 3600,
};
let host_key = ConnectionPool::make_host_key(&config);
assert_eq!(host_key, "alice@test.example.com:22");
}
#[tokio::test]
async fn test_pool_stats() {
let pool = ConnectionPool::new(PoolConfig::default());
let stats = pool.stats().await;
assert_eq!(stats.total_connections, 0);
assert_eq!(stats.total_use_count, 0);
assert_eq!(stats.expired_count, 0);
}
}