Skip to main content

aria2_core/engine/
bt_connection_pool.rs

1// aria2-core/src/engine/bt_connection_pool.rs
2
3use std::collections::VecDeque;
4use std::net::SocketAddr;
5use std::time::{Duration, Instant};
6use tokio::net::TcpStream;
7use tracing::debug;
8
9pub struct ReadyConnection {
10    pub stream: TcpStream,
11    pub addr: SocketAddr,
12    ready_at: Instant,
13}
14
15impl ReadyConnection {
16    pub fn new(stream: TcpStream, addr: SocketAddr) -> Self {
17        Self {
18            stream,
19            addr,
20            ready_at: Instant::now(),
21        }
22    }
23
24    pub fn age(&self) -> Duration {
25        self.ready_at.elapsed()
26    }
27    pub fn is_stale(&self, max_age: Duration) -> bool {
28        self.age() > max_age
29    }
30}
31
32pub struct BtConnectionPool {
33    connections: VecDeque<ReadyConnection>,
34    max_idle: usize,
35    connect_timeout: Duration,
36    stale_threshold: Duration,
37}
38
39impl BtConnectionPool {
40    pub fn new() -> Self {
41        Self {
42            connections: VecDeque::new(),
43            max_idle: 20,
44            connect_timeout: Duration::from_secs(5),
45            stale_threshold: Duration::from_secs(30),
46        }
47    }
48
49    pub fn with_max_idle(mut self, n: usize) -> Self {
50        self.max_idle = n;
51        self
52    }
53    pub fn with_connect_timeout(mut self, d: Duration) -> Self {
54        self.connect_timeout = d;
55        self
56    }
57    pub fn with_stale_threshold(mut self, d: Duration) -> Self {
58        self.stale_threshold = d;
59        self
60    }
61
62    pub async fn prewarm(&mut self, addr: SocketAddr) -> Result<bool, String> {
63        if self.contains_addr(&addr) {
64            return Ok(false);
65        }
66
67        match tokio::time::timeout(self.connect_timeout, TcpStream::connect(addr)).await {
68            Ok(Ok(stream)) => {
69                self.evict_if_full();
70                self.connections
71                    .push_back(ReadyConnection::new(stream, addr));
72                debug!(
73                    "[ConnPool] Prewarmed connection to {} (pool={}/{})",
74                    addr,
75                    self.connections.len(),
76                    self.max_idle
77                );
78                Ok(true)
79            }
80            Ok(Err(e)) => Err(format!("Connect to {} failed: {}", addr, e)),
81            Err(_) => Err(format!(
82                "Connect to {} timed out after {:?}",
83                addr, self.connect_timeout
84            )),
85        }
86    }
87
88    pub fn try_take(&mut self, addr: &SocketAddr) -> Option<ReadyConnection> {
89        let pos = self.connections.iter().position(|c| &c.addr == addr)?;
90        let conn = self.connections.remove(pos).unwrap();
91        debug!(
92            "[ConnPool] Took connection to {} (pool={})",
93            addr,
94            self.connections.len()
95        );
96        Some(conn)
97    }
98
99    pub fn return_connection(&mut self, conn: ReadyConnection) {
100        if conn.is_stale(self.stale_threshold) {
101            debug!("[ConnPool] Dropping stale connection to {}", conn.addr);
102            return;
103        }
104        self.evict_if_full();
105        self.connections.push_back(conn);
106    }
107
108    pub fn contains_addr(&self, addr: &SocketAddr) -> bool {
109        self.connections.iter().any(|c| &c.addr == addr)
110    }
111
112    pub fn len(&self) -> usize {
113        self.connections.len()
114    }
115    pub fn is_empty(&self) -> bool {
116        self.connections.is_empty()
117    }
118
119    pub async fn evict_stale(&mut self) -> usize {
120        let before = self.connections.len();
121        self.connections
122            .retain(|c| !c.is_stale(self.stale_threshold));
123        before - self.connections.len()
124    }
125
126    fn evict_if_full(&mut self) {
127        while self.connections.len() >= self.max_idle {
128            if let Some(old) = self.connections.pop_front() {
129                debug!(
130                    "[ConnPool] Evicting oldest connection to {} (age={:?})",
131                    old.addr,
132                    old.age()
133                );
134            } else {
135                break;
136            }
137        }
138    }
139}
140
141impl Default for BtConnectionPool {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_pool_starts_empty() {
153        let pool = BtConnectionPool::new();
154        assert_eq!(pool.len(), 0);
155        assert!(pool.is_empty());
156    }
157
158    #[tokio::test]
159    async fn test_prewarm_adds_connection() {
160        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
161        let addr = listener.local_addr().unwrap();
162
163        let mut pool = BtConnectionPool::new();
164        let result = pool.prewarm(addr).await;
165
166        assert!(result.is_ok());
167        assert!(result.unwrap());
168        assert_eq!(pool.len(), 1);
169        assert!(pool.contains_addr(&addr));
170    }
171
172    #[tokio::test]
173    async fn test_try_take_removes_from_pool() {
174        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
175        let addr = listener.local_addr().unwrap();
176
177        let mut pool = BtConnectionPool::new();
178        pool.prewarm(addr).await.unwrap();
179
180        let conn = pool.try_take(&addr);
181        assert!(conn.is_some());
182        assert_eq!(pool.len(), 0);
183        assert!(!pool.contains_addr(&addr));
184
185        let taken = conn.unwrap();
186        assert_eq!(taken.addr, addr);
187    }
188
189    #[tokio::test]
190    async fn test_return_connection_readds() {
191        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
192        let addr = listener.local_addr().unwrap();
193
194        let mut pool = BtConnectionPool::new();
195        pool.prewarm(addr).await.unwrap();
196
197        let conn = pool.try_take(&addr).unwrap();
198        assert_eq!(pool.len(), 0);
199
200        pool.return_connection(conn);
201        assert_eq!(pool.len(), 1);
202        assert!(pool.contains_addr(&addr));
203    }
204
205    #[tokio::test]
206    async fn test_eviction_on_max_idle() {
207        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
208        let addr1 = listener1.local_addr().unwrap();
209
210        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
211        let addr2 = listener2.local_addr().unwrap();
212
213        let mut pool = BtConnectionPool::new().with_max_idle(1);
214
215        pool.prewarm(addr1).await.unwrap();
216        assert_eq!(pool.len(), 1);
217
218        pool.prewarm(addr2).await.unwrap();
219        assert_eq!(pool.len(), 1);
220        assert!(pool.contains_addr(&addr2));
221        assert!(!pool.contains_addr(&addr1));
222    }
223
224    #[tokio::test]
225    async fn test_duplicate_prewarm_skips() {
226        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
227        let addr = listener.local_addr().unwrap();
228
229        let mut pool = BtConnectionPool::new();
230
231        let result1 = pool.prewarm(addr).await.unwrap();
232        assert!(result1);
233        assert_eq!(pool.len(), 1);
234
235        let result2 = pool.prewarm(addr).await.unwrap();
236        assert!(!result2);
237        assert_eq!(pool.len(), 1);
238    }
239}