aria2_core/engine/
bt_connection_pool.rs1use 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}