1use std::collections::HashMap;
45use std::sync::Arc;
46use std::time::{Duration, Instant};
47
48use tokio::sync::Mutex;
49use tracing::{debug, info};
50
51use crate::error::Result;
52use crate::ftp::connection::{FtpClient, FtpMode};
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
56pub struct ConnectionKey {
57 pub host: String,
59 pub port: u16,
61 pub username: String,
63 pub password: String,
65}
66
67impl ConnectionKey {
68 pub fn new(host: &str, port: u16, username: &str, password: &str) -> Self {
70 Self {
71 host: host.to_string(),
72 port,
73 username: username.to_string(),
74 password: password.to_string(),
75 }
76 }
77}
78
79pub struct PooledConnection {
81 pub client: FtpClient,
83 pub key: ConnectionKey,
85 pub created_at: Instant,
87 pub last_used: Instant,
89 pub reuse_count: u64,
91 pub mode: FtpMode,
93}
94
95impl std::fmt::Debug for PooledConnection {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct("PooledConnection")
98 .field("key", &self.key)
99 .field("created_at", &self.created_at)
100 .field("last_used", &self.last_used)
101 .field("reuse_count", &self.reuse_count)
102 .field("mode", &self.mode)
103 .field("age", &self.age())
104 .field("idle_time", &self.idle_time())
105 .finish_non_exhaustive()
106 }
107}
108
109impl PooledConnection {
110 pub fn new(client: FtpClient, key: ConnectionKey, mode: FtpMode) -> Self {
112 let now = Instant::now();
113 Self {
114 client,
115 key,
116 created_at: now,
117 last_used: now,
118 reuse_count: 0,
119 mode,
120 }
121 }
122
123 pub fn mark_used(&mut self) {
125 self.last_used = Instant::now();
126 self.reuse_count += 1;
127 }
128
129 pub fn is_healthy(&self, max_idle_time: Duration) -> bool {
131 self.last_used.elapsed() < max_idle_time
133 }
134
135 pub fn age(&self) -> Duration {
137 self.created_at.elapsed()
138 }
139
140 pub fn idle_time(&self) -> Duration {
142 self.last_used.elapsed()
143 }
144}
145
146#[derive(Debug, Clone)]
148struct LruEntry {
149 key: ConnectionKey,
150 last_access: Instant,
151}
152
153#[derive(Debug, Clone)]
155pub struct PoolConfig {
156 pub max_connections: usize,
158 pub max_idle_time: Duration,
160 pub max_connection_age: Duration,
162 pub connect_timeout: Duration,
164 pub read_timeout: Duration,
166}
167
168impl Default for PoolConfig {
169 fn default() -> Self {
170 Self {
171 max_connections: crate::constants::FTP_POOL_DEFAULT_MAX_CONNECTIONS,
172 max_idle_time: Duration::from_secs(
173 crate::constants::FTP_POOL_DEFAULT_MAX_IDLE_TIME_SECS,
174 ),
175 max_connection_age: Duration::from_secs(
176 crate::constants::FTP_POOL_DEFAULT_MAX_CONNECTION_AGE_SECS,
177 ),
178 connect_timeout: Duration::from_secs(
179 crate::constants::FTP_POOL_DEFAULT_CONNECT_TIMEOUT_SECS,
180 ),
181 read_timeout: Duration::from_secs(crate::constants::FTP_POOL_DEFAULT_READ_TIMEOUT_SECS),
182 }
183 }
184}
185
186pub struct FtpConnectionPool {
188 connections: Arc<Mutex<HashMap<ConnectionKey, PooledConnection>>>,
190 lru_order: Arc<Mutex<Vec<LruEntry>>>,
192 config: PoolConfig,
194 stats: Arc<Mutex<PoolStats>>,
196}
197
198#[derive(Debug, Clone, Default)]
200pub struct PoolStats {
201 pub connections_created: u64,
203 pub connections_reused: u64,
205 pub connections_evicted: u64,
207 pub connection_failures: u64,
209 pub current_size: usize,
211 pub peak_size: usize,
213}
214
215impl FtpConnectionPool {
216 pub fn new(max_connections: usize) -> Self {
218 let config = PoolConfig {
219 max_connections,
220 ..Default::default()
221 };
222 Self::with_config(config)
223 }
224
225 pub fn with_config(config: PoolConfig) -> Self {
227 Self {
228 connections: Arc::new(Mutex::new(HashMap::new())),
229 lru_order: Arc::new(Mutex::new(Vec::new())),
230 config,
231 stats: Arc::new(Mutex::new(PoolStats::default())),
232 }
233 }
234
235 pub async fn get_connection(
243 &self,
244 host: &str,
245 port: u16,
246 username: &str,
247 password: &str,
248 mode: FtpMode,
249 ) -> Result<PooledConnection> {
250 let key = ConnectionKey::new(host, port, username, password);
251
252 {
254 let mut connections = self.connections.lock().await;
255 if let Some(conn) = connections.get_mut(&key) {
256 if conn.is_healthy(self.config.max_idle_time) {
258 conn.mark_used();
259 self.update_lru_access(&key).await;
260
261 let mut stats = self.stats.lock().await;
263 stats.connections_reused += 1;
264
265 debug!(
266 "Reusing FTP connection to {}:{} (reuse #{})",
267 host, port, conn.reuse_count
268 );
269
270 let conn = connections.remove(&key).unwrap();
274 return Ok(conn);
275 } else {
276 debug!("Removing stale FTP connection to {}:{}", host, port);
278 connections.remove(&key);
279 self.remove_from_lru(&key).await;
280
281 let mut stats = self.stats.lock().await;
282 stats.connections_evicted += 1;
283 }
284 }
285 }
286
287 self.evict_if_needed().await?;
290
291 debug!("Creating new FTP connection to {}:{}", host, port);
293 let client = FtpClient::connect(host, port, mode).await?;
294
295 {
297 let mut client = client;
298 client.login(username, password).await?;
299
300 client.set_binary_mode(true).await?;
302
303 let pooled = PooledConnection::new(client, key.clone(), mode);
304
305 let mut connections = self.connections.lock().await;
307 connections.insert(key.clone(), pooled);
308
309 self.add_to_lru(key.clone()).await;
311
312 let mut stats = self.stats.lock().await;
314 stats.connections_created += 1;
315 stats.current_size = connections.len();
316 if connections.len() > stats.peak_size {
317 stats.peak_size = connections.len();
318 }
319
320 info!(
321 "FTP connection pool: created new connection to {}:{}",
322 host, port
323 );
324
325 Ok(connections.remove(&key).unwrap())
327 }
328 }
329
330 pub async fn return_connection(&self, mut conn: PooledConnection) {
332 if !conn.is_healthy(self.config.max_idle_time) {
334 debug!(
335 "Not returning unhealthy connection to {}:{}",
336 conn.key.host, conn.key.port
337 );
338 let mut stats = self.stats.lock().await;
339 stats.connections_evicted += 1;
340 return;
341 }
342
343 if conn.age() > self.config.max_connection_age {
345 debug!(
346 "Not returning expired connection to {}:{} (age: {:?})",
347 conn.key.host,
348 conn.key.port,
349 conn.age()
350 );
351 let mut stats = self.stats.lock().await;
352 stats.connections_evicted += 1;
353 return;
354 }
355
356 conn.mark_used();
357
358 let mut connections = self.connections.lock().await;
359 let key = conn.key.clone();
360 connections.insert(key.clone(), conn);
361
362 self.update_lru_access(&key).await;
363
364 let mut stats = self.stats.lock().await;
365 stats.current_size = connections.len();
366
367 debug!("Returned FTP connection to pool: {}:{}", key.host, key.port);
368 }
369
370 async fn evict_if_needed(&self) -> Result<()> {
372 let mut connections = self.connections.lock().await;
373
374 while connections.len() >= self.config.max_connections {
375 let lru_key = self.find_lru_key().await;
377
378 if let Some(key) = lru_key {
379 debug!(
380 "Evicting LRU connection to {}:{} (pool full)",
381 key.host, key.port
382 );
383 connections.remove(&key);
384 self.remove_from_lru(&key).await;
385
386 let mut stats = self.stats.lock().await;
387 stats.connections_evicted += 1;
388 } else {
389 break;
390 }
391 }
392
393 Ok(())
394 }
395
396 async fn add_to_lru(&self, key: ConnectionKey) {
398 let mut lru = self.lru_order.lock().await;
399 lru.push(LruEntry {
400 key,
401 last_access: Instant::now(),
402 });
403 }
404
405 async fn update_lru_access(&self, key: &ConnectionKey) {
407 let mut lru = self.lru_order.lock().await;
408 if let Some(entry) = lru.iter_mut().find(|e| &e.key == key) {
409 entry.last_access = Instant::now();
410 }
411 }
412
413 async fn remove_from_lru(&self, key: &ConnectionKey) {
415 let mut lru = self.lru_order.lock().await;
416 lru.retain(|e| &e.key != key);
417 }
418
419 async fn find_lru_key(&self) -> Option<ConnectionKey> {
421 let lru = self.lru_order.lock().await;
422 lru.iter()
423 .min_by_key(|e| e.last_access)
424 .map(|e| e.key.clone())
425 }
426
427 pub async fn cleanup_stale(&self) {
429 let mut connections = self.connections.lock().await;
430 let mut to_remove = Vec::new();
431
432 for (key, conn) in connections.iter() {
433 if !conn.is_healthy(self.config.max_idle_time)
434 || conn.age() > self.config.max_connection_age
435 {
436 to_remove.push(key.clone());
437 }
438 }
439
440 for key in to_remove {
441 connections.remove(&key);
442 self.remove_from_lru(&key).await;
443
444 let mut stats = self.stats.lock().await;
445 stats.connections_evicted += 1;
446 }
447
448 let mut stats = self.stats.lock().await;
449 stats.current_size = connections.len();
450
451 debug!(
452 "FTP connection pool cleanup: {} connections remaining",
453 connections.len()
454 );
455 }
456
457 pub async fn stats(&self) -> PoolStats {
459 self.stats.lock().await.clone()
460 }
461
462 pub async fn size(&self) -> usize {
464 self.connections.lock().await.len()
465 }
466
467 pub async fn clear(&self) {
469 let mut connections = self.connections.lock().await;
470 let count = connections.len();
471 connections.clear();
472
473 let mut lru = self.lru_order.lock().await;
474 lru.clear();
475
476 let mut stats = self.stats.lock().await;
477 stats.connections_evicted += count as u64;
478 stats.current_size = 0;
479
480 info!("FTP connection pool cleared: {} connections removed", count);
481 }
482
483 pub async fn has_connection(&self, host: &str, port: u16, username: &str) -> bool {
485 let connections = self.connections.lock().await;
486 connections
487 .keys()
488 .any(|k| k.host == host && k.port == port && k.username == username)
489 }
490}
491
492pub fn create_pool(max_connections: usize) -> Arc<FtpConnectionPool> {
497 Arc::new(FtpConnectionPool::new(max_connections))
498}
499
500pub fn create_custom_pool(config: PoolConfig) -> Arc<FtpConnectionPool> {
502 Arc::new(FtpConnectionPool::with_config(config))
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 #[test]
510 fn test_connection_key_equality() {
511 let key1 = ConnectionKey::new("example.com", 21, "user", "pass");
512 let key2 = ConnectionKey::new("example.com", 21, "user", "pass");
513 let key3 = ConnectionKey::new("example.com", 21, "user2", "pass");
514
515 assert_eq!(key1, key2);
516 assert_ne!(key1, key3);
517 }
518
519 #[test]
520 fn test_pool_config_default() {
521 let config = PoolConfig::default();
522 assert_eq!(config.max_connections, 16);
523 assert_eq!(config.max_idle_time, Duration::from_secs(300));
524 assert_eq!(config.max_connection_age, Duration::from_secs(1800));
525 }
526
527 #[tokio::test]
528 async fn test_pool_creation() {
529 let pool = FtpConnectionPool::new(10);
530 assert_eq!(pool.size().await, 0);
531 }
532
533 #[tokio::test]
534 async fn test_pool_stats_initial() {
535 let pool = FtpConnectionPool::new(10);
536 let stats = pool.stats().await;
537 assert_eq!(stats.connections_created, 0);
538 assert_eq!(stats.connections_reused, 0);
539 assert_eq!(stats.connections_evicted, 0);
540 assert_eq!(stats.current_size, 0);
541 }
542
543 #[tokio::test]
544 async fn test_pool_clear() {
545 let pool = FtpConnectionPool::new(10);
546 pool.clear().await;
547 assert_eq!(pool.size().await, 0);
548 }
549
550 #[test]
551 fn test_pooled_connection_health() {
552 let max_idle_time = Duration::from_secs(300);
555
556 let idle_time = Duration::from_secs(10);
560 assert!(idle_time < max_idle_time);
561
562 let idle_time_long = Duration::from_secs(400);
564 assert!(idle_time_long >= max_idle_time);
565 }
566
567 #[test]
568 fn test_lru_entry_creation() {
569 let key = ConnectionKey::new("example.com", 21, "user", "pass");
570 let entry = LruEntry {
571 key: key.clone(),
572 last_access: Instant::now(),
573 };
574
575 assert_eq!(entry.key, key);
576 assert!(entry.last_access.elapsed() < Duration::from_secs(1));
577 }
578
579 #[tokio::test]
580 async fn test_create_pool_returns_shared_arc() {
581 let pool = create_pool(10);
582 let pool2 = pool.clone();
583
584 assert!(Arc::ptr_eq(&pool, &pool2));
586 }
587
588 #[tokio::test]
589 async fn test_custom_pool_is_different() {
590 let pool1 = create_pool(10);
591 let pool2 = create_custom_pool(PoolConfig::default());
592
593 assert!(!Arc::ptr_eq(&pool1, &pool2));
595 }
596
597 #[test]
598 fn test_pool_stats_default() {
599 let stats = PoolStats::default();
600 assert_eq!(stats.connections_created, 0);
601 assert_eq!(stats.connections_reused, 0);
602 assert_eq!(stats.connections_evicted, 0);
603 assert_eq!(stats.connection_failures, 0);
604 assert_eq!(stats.current_size, 0);
605 assert_eq!(stats.peak_size, 0);
606 }
607
608 #[test]
609 fn test_connection_key_hash() {
610 use std::collections::HashSet;
611
612 let mut set = HashSet::new();
613 let key1 = ConnectionKey::new("example.com", 21, "user", "pass");
614 let key2 = ConnectionKey::new("example.com", 21, "user", "pass");
615 let key3 = ConnectionKey::new("other.com", 21, "user", "pass");
616
617 set.insert(key1.clone());
618 assert!(set.contains(&key2)); assert!(!set.contains(&key3)); }
621}