1use std::collections::HashMap;
28use std::sync::{Arc, Mutex};
29
30use crate::errors::{Error, Result};
31use crate::session::{Session, SessionConfig, SessionManager};
32
33#[derive(Debug, Clone)]
35pub struct PoolConfig {
36 pub max_sessions: usize,
38 pub allow_duplicate_targets: bool,
40 pub session_defaults: SessionConfig,
42}
43
44impl Default for PoolConfig {
45 fn default() -> Self {
46 Self {
47 max_sessions: 100,
48 allow_duplicate_targets: true,
49 session_defaults: SessionConfig::default(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
56pub struct PoolStats {
57 pub live: usize,
59 pub started: u64,
61 pub ended: u64,
63}
64
65pub struct SessionPool {
67 manager: SessionManager,
68 config: PoolConfig,
69 sessions: Mutex<HashMap<String, Arc<Session>>>,
70 started: Mutex<u64>,
71}
72
73impl SessionPool {
74 pub async fn new(config: PoolConfig) -> Result<Self> {
76 Ok(Self::with_manager(config, SessionManager::new().await?))
77 }
78
79 pub fn with_manager(config: PoolConfig, manager: SessionManager) -> Self {
81 Self {
82 manager,
83 config,
84 sessions: Mutex::new(HashMap::new()),
85 started: Mutex::new(0),
86 }
87 }
88
89 pub async fn start(&self, target: impl Into<String>) -> Result<Arc<Session>> {
91 let config = SessionConfig {
92 target: target.into(),
93 ..self.config.session_defaults.clone()
94 };
95 self.start_with(config).await
96 }
97
98 pub async fn start_with(&self, config: SessionConfig) -> Result<Arc<Session>> {
105 let target = config.target.clone();
106 self.check_admission(&target)?;
107
108 let session = Arc::new(self.manager.start_session(config).await?);
109
110 let rejection = {
114 let mut sessions = self.lock();
115 reap(&mut sessions);
116 match admit(&self.config, &sessions, &target) {
117 Ok(()) => {
118 sessions.insert(session.id().to_owned(), Arc::clone(&session));
119 None
120 }
121 Err(e) => Some(e),
122 }
123 };
124
125 if let Some(e) = rejection {
126 let _ = session.terminate().await;
127 return Err(e);
128 }
129 *lock(&self.started) += 1;
130
131 Ok(session)
132 }
133
134 pub fn insert(&self, session: Arc<Session>) -> Result<Arc<Session>> {
141 let target = session.config().target.clone();
142 let mut sessions = self.lock();
143 reap(&mut sessions);
144 admit(&self.config, &sessions, &target)?;
145 sessions.insert(session.id().to_owned(), Arc::clone(&session));
146 drop(sessions);
147 *lock(&self.started) += 1;
148 Ok(session)
149 }
150
151 pub fn get(&self, session_id: &str) -> Option<Arc<Session>> {
153 let mut sessions = self.lock();
154 reap(&mut sessions);
155 sessions.get(session_id).cloned()
156 }
157
158 pub fn for_target(&self, target: &str) -> Vec<Arc<Session>> {
160 let mut sessions = self.lock();
161 reap(&mut sessions);
162 sessions
163 .values()
164 .filter(|s| s.config().target == target)
165 .cloned()
166 .collect()
167 }
168
169 pub fn sessions(&self) -> Vec<Arc<Session>> {
171 let mut sessions = self.lock();
172 reap(&mut sessions);
173 sessions.values().cloned().collect()
174 }
175
176 pub fn session_ids(&self) -> Vec<String> {
178 let mut sessions = self.lock();
179 reap(&mut sessions);
180 sessions.keys().cloned().collect()
181 }
182
183 pub fn stats(&self) -> PoolStats {
185 let mut sessions = self.lock();
186 reap(&mut sessions);
187 let live = sessions.len();
188 let started = *lock(&self.started);
189 PoolStats {
190 live,
191 started,
192 ended: started.saturating_sub(live as u64),
193 }
194 }
195
196 pub async fn terminate(&self, session_id: &str) -> Result<()> {
201 let session = self.lock().remove(session_id);
202 match session {
203 Some(session) => session.terminate().await,
204 None => Ok(()),
205 }
206 }
207
208 pub async fn terminate_target(&self, target: &str) -> Result<()> {
210 let doomed: Vec<Arc<Session>> = {
211 let mut sessions = self.lock();
212 let ids: Vec<String> = sessions
213 .iter()
214 .filter(|(_, s)| s.config().target == target)
215 .map(|(id, _)| id.clone())
216 .collect();
217 ids.iter().filter_map(|id| sessions.remove(id)).collect()
218 };
219 terminate_all(doomed).await;
220 Ok(())
221 }
222
223 pub async fn shutdown(&self) {
230 let doomed: Vec<Arc<Session>> = self.lock().drain().map(|(_, s)| s).collect();
231 tracing::info!(count = doomed.len(), "shutting down the session pool");
232 terminate_all(doomed).await;
233 }
234
235 fn check_admission(&self, target: &str) -> Result<()> {
236 let mut sessions = self.lock();
237 reap(&mut sessions);
238 admit(&self.config, &sessions, target)
239 }
240
241 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Arc<Session>>> {
242 self.sessions.lock().unwrap_or_else(|e| e.into_inner())
243 }
244}
245
246impl std::fmt::Debug for SessionPool {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.debug_struct("SessionPool")
249 .field("stats", &self.stats())
250 .field("max_sessions", &self.config.max_sessions)
251 .finish()
252 }
253}
254
255fn reap(sessions: &mut HashMap<String, Arc<Session>>) {
257 sessions.retain(|_, session| !session.is_closed());
258}
259
260fn admit(
261 config: &PoolConfig,
262 sessions: &HashMap<String, Arc<Session>>,
263 target: &str,
264) -> Result<()> {
265 if config.max_sessions > 0 && sessions.len() >= config.max_sessions {
266 return Err(Error::Config(format!(
267 "session pool is full ({} of {} in use)",
268 sessions.len(),
269 config.max_sessions
270 )));
271 }
272 if !config.allow_duplicate_targets && sessions.values().any(|s| s.config().target == target) {
273 return Err(Error::Config(format!(
274 "a session to {target} is already open and PoolConfig::allow_duplicate_targets is false"
275 )));
276 }
277 Ok(())
278}
279
280async fn terminate_all(sessions: Vec<Arc<Session>>) {
281 let outcomes = futures_util::future::join_all(
282 sessions
283 .iter()
284 .map(|session| async move { (session.id(), session.terminate().await) }),
285 )
286 .await;
287
288 for (id, result) in outcomes {
289 if let Err(e) = result {
290 tracing::warn!(session_id = %id, error = %e, "failed to terminate a pooled session");
291 }
292 }
293}
294
295fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
296 mutex.lock().unwrap_or_else(|e| e.into_inner())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn config(max_sessions: usize, allow_duplicates: bool) -> PoolConfig {
304 PoolConfig {
305 max_sessions,
306 allow_duplicate_targets: allow_duplicates,
307 session_defaults: SessionConfig::default(),
308 }
309 }
310
311 #[test]
312 fn defaults_are_permissive_but_bounded() {
313 let config = PoolConfig::default();
314 assert_eq!(config.max_sessions, 100);
315 assert!(config.allow_duplicate_targets);
316 }
317
318 #[test]
319 fn an_empty_pool_admits_under_every_policy() {
320 let sessions = HashMap::new();
321 assert!(
322 admit(&config(0, true), &sessions, "i-a").is_ok(),
323 "0 means unlimited"
324 );
325 assert!(admit(&config(1, true), &sessions, "i-a").is_ok());
326 assert!(admit(&config(1, false), &sessions, "i-a").is_ok());
327 }
328
329 #[test]
330 fn stats_start_at_zero() {
331 assert_eq!(
332 PoolStats::default(),
333 PoolStats {
334 live: 0,
335 started: 0,
336 ended: 0
337 }
338 );
339 }
340
341 #[test]
342 fn reaping_an_empty_map_is_a_no_op() {
343 let mut sessions: HashMap<String, Arc<Session>> = HashMap::new();
344 reap(&mut sessions);
345 assert!(sessions.is_empty());
346 }
347}