use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::errors::{Error, Result};
use crate::session::{Session, SessionConfig, SessionManager};
#[derive(Debug, Clone)]
pub struct PoolConfig {
pub max_sessions: usize,
pub allow_duplicate_targets: bool,
pub session_defaults: SessionConfig,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
max_sessions: 100,
allow_duplicate_targets: true,
session_defaults: SessionConfig::default(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PoolStats {
pub live: usize,
pub started: u64,
pub ended: u64,
}
pub struct SessionPool {
manager: SessionManager,
config: PoolConfig,
sessions: Mutex<HashMap<String, Arc<Session>>>,
started: Mutex<u64>,
}
impl SessionPool {
pub async fn new(config: PoolConfig) -> Result<Self> {
Ok(Self::with_manager(config, SessionManager::new().await?))
}
pub fn with_manager(config: PoolConfig, manager: SessionManager) -> Self {
Self {
manager,
config,
sessions: Mutex::new(HashMap::new()),
started: Mutex::new(0),
}
}
pub async fn start(&self, target: impl Into<String>) -> Result<Arc<Session>> {
let config = SessionConfig {
target: target.into(),
..self.config.session_defaults.clone()
};
self.start_with(config).await
}
pub async fn start_with(&self, config: SessionConfig) -> Result<Arc<Session>> {
let target = config.target.clone();
self.check_admission(&target)?;
let session = Arc::new(self.manager.start_session(config).await?);
let rejection = {
let mut sessions = self.lock();
reap(&mut sessions);
match admit(&self.config, &sessions, &target) {
Ok(()) => {
sessions.insert(session.id().to_owned(), Arc::clone(&session));
None
}
Err(e) => Some(e),
}
};
if let Some(e) = rejection {
let _ = session.terminate().await;
return Err(e);
}
*lock(&self.started) += 1;
Ok(session)
}
pub fn insert(&self, session: Arc<Session>) -> Result<Arc<Session>> {
let target = session.config().target.clone();
let mut sessions = self.lock();
reap(&mut sessions);
admit(&self.config, &sessions, &target)?;
sessions.insert(session.id().to_owned(), Arc::clone(&session));
drop(sessions);
*lock(&self.started) += 1;
Ok(session)
}
pub fn get(&self, session_id: &str) -> Option<Arc<Session>> {
let mut sessions = self.lock();
reap(&mut sessions);
sessions.get(session_id).cloned()
}
pub fn for_target(&self, target: &str) -> Vec<Arc<Session>> {
let mut sessions = self.lock();
reap(&mut sessions);
sessions
.values()
.filter(|s| s.config().target == target)
.cloned()
.collect()
}
pub fn sessions(&self) -> Vec<Arc<Session>> {
let mut sessions = self.lock();
reap(&mut sessions);
sessions.values().cloned().collect()
}
pub fn session_ids(&self) -> Vec<String> {
let mut sessions = self.lock();
reap(&mut sessions);
sessions.keys().cloned().collect()
}
pub fn stats(&self) -> PoolStats {
let mut sessions = self.lock();
reap(&mut sessions);
let live = sessions.len();
let started = *lock(&self.started);
PoolStats {
live,
started,
ended: started.saturating_sub(live as u64),
}
}
pub async fn terminate(&self, session_id: &str) -> Result<()> {
let session = self.lock().remove(session_id);
match session {
Some(session) => session.terminate().await,
None => Ok(()),
}
}
pub async fn terminate_target(&self, target: &str) -> Result<()> {
let doomed: Vec<Arc<Session>> = {
let mut sessions = self.lock();
let ids: Vec<String> = sessions
.iter()
.filter(|(_, s)| s.config().target == target)
.map(|(id, _)| id.clone())
.collect();
ids.iter().filter_map(|id| sessions.remove(id)).collect()
};
terminate_all(doomed).await;
Ok(())
}
pub async fn shutdown(&self) {
let doomed: Vec<Arc<Session>> = self.lock().drain().map(|(_, s)| s).collect();
tracing::info!(count = doomed.len(), "shutting down the session pool");
terminate_all(doomed).await;
}
fn check_admission(&self, target: &str) -> Result<()> {
let mut sessions = self.lock();
reap(&mut sessions);
admit(&self.config, &sessions, target)
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Arc<Session>>> {
self.sessions.lock().unwrap_or_else(|e| e.into_inner())
}
}
impl std::fmt::Debug for SessionPool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionPool")
.field("stats", &self.stats())
.field("max_sessions", &self.config.max_sessions)
.finish()
}
}
fn reap(sessions: &mut HashMap<String, Arc<Session>>) {
sessions.retain(|_, session| !session.is_closed());
}
fn admit(
config: &PoolConfig,
sessions: &HashMap<String, Arc<Session>>,
target: &str,
) -> Result<()> {
if config.max_sessions > 0 && sessions.len() >= config.max_sessions {
return Err(Error::Config(format!(
"session pool is full ({} of {} in use)",
sessions.len(),
config.max_sessions
)));
}
if !config.allow_duplicate_targets && sessions.values().any(|s| s.config().target == target) {
return Err(Error::Config(format!(
"a session to {target} is already open and PoolConfig::allow_duplicate_targets is false"
)));
}
Ok(())
}
async fn terminate_all(sessions: Vec<Arc<Session>>) {
let outcomes = futures_util::future::join_all(
sessions
.iter()
.map(|session| async move { (session.id(), session.terminate().await) }),
)
.await;
for (id, result) in outcomes {
if let Err(e) = result {
tracing::warn!(session_id = %id, error = %e, "failed to terminate a pooled session");
}
}
}
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
fn config(max_sessions: usize, allow_duplicates: bool) -> PoolConfig {
PoolConfig {
max_sessions,
allow_duplicate_targets: allow_duplicates,
session_defaults: SessionConfig::default(),
}
}
#[test]
fn defaults_are_permissive_but_bounded() {
let config = PoolConfig::default();
assert_eq!(config.max_sessions, 100);
assert!(config.allow_duplicate_targets);
}
#[test]
fn an_empty_pool_admits_under_every_policy() {
let sessions = HashMap::new();
assert!(
admit(&config(0, true), &sessions, "i-a").is_ok(),
"0 means unlimited"
);
assert!(admit(&config(1, true), &sessions, "i-a").is_ok());
assert!(admit(&config(1, false), &sessions, "i-a").is_ok());
}
#[test]
fn stats_start_at_zero() {
assert_eq!(
PoolStats::default(),
PoolStats {
live: 0,
started: 0,
ended: 0
}
);
}
#[test]
fn reaping_an_empty_map_is_a_no_op() {
let mut sessions: HashMap<String, Arc<Session>> = HashMap::new();
reap(&mut sessions);
assert!(sessions.is_empty());
}
}