1use std::{
2 fmt::Display,
3 sync::{Arc, Mutex},
4};
5
6use lru::LruCache;
7
8use crate::{
9 request_settings::AppliedRequestSettings,
10 transport::{CoreGrpcClient, PlatformGrpcClient},
11 Uri,
12};
13
14#[derive(Debug, Clone)]
19pub struct ConnectionPool {
20 inner: Arc<Mutex<LruCache<String, PoolItem>>>,
21}
22
23impl ConnectionPool {
24 pub fn new(capacity: usize) -> Self {
31 Self {
32 inner: Arc::new(Mutex::new(LruCache::new(
33 capacity.try_into().expect("must be non-zero"),
34 ))),
35 }
36 }
37}
38
39impl Default for ConnectionPool {
40 fn default() -> Self {
41 Self::new(50)
42 }
43}
44
45impl ConnectionPool {
46 pub fn get(
53 &self,
54 prefix: PoolPrefix,
55 uri: &Uri,
56 settings: Option<&AppliedRequestSettings>,
57 ) -> Option<PoolItem> {
58 let key = Self::key(prefix, uri, settings);
59 self.inner.lock().expect("must lock").get(&key).cloned()
60 }
61
62 pub fn get_or_create<E>(
71 &self,
72 prefix: PoolPrefix,
73 uri: &Uri,
74 settings: Option<&AppliedRequestSettings>,
75 create: impl FnOnce() -> Result<PoolItem, E>,
76 ) -> Result<PoolItem, E> {
77 if let Some(cli) = self.get(prefix, uri, settings) {
78 return Ok(cli);
79 }
80
81 let cli = create();
82 if let Ok(cli) = &cli {
83 self.put(uri, settings, cli.clone());
84 }
85 cli
86 }
87
88 pub fn put(&self, uri: &Uri, settings: Option<&AppliedRequestSettings>, value: PoolItem) {
90 let key = Self::key(&value, uri, settings);
91 self.inner.lock().expect("must lock").put(key, value);
92 }
93
94 fn key<C: Into<PoolPrefix>>(
95 class: C,
96 uri: &Uri,
97 settings: Option<&AppliedRequestSettings>,
98 ) -> String {
99 let prefix: PoolPrefix = class.into();
100 match settings {
107 Some(settings) => format!("{}:{}:{}", prefix, uri, settings.connection_key()),
108 None => format!("{}:{}:none", prefix, uri),
109 }
110 }
111}
112
113#[derive(Clone, Debug)]
117pub enum PoolItem {
118 Core(CoreGrpcClient),
119 Platform(PlatformGrpcClient),
120}
121
122impl From<PlatformGrpcClient> for PoolItem {
123 fn from(client: PlatformGrpcClient) -> Self {
124 Self::Platform(client)
125 }
126}
127impl From<CoreGrpcClient> for PoolItem {
128 fn from(client: CoreGrpcClient) -> Self {
129 Self::Core(client)
130 }
131}
132
133impl From<PoolItem> for PlatformGrpcClient {
134 fn from(client: PoolItem) -> Self {
135 match client {
136 PoolItem::Platform(client) => client,
137 _ => {
138 tracing::error!(
139 ?client,
140 "invalid connection fetched from pool: expected platform client"
141 );
142 panic!("ClientType is not Platform: {:?}", client)
143 }
144 }
145 }
146}
147
148impl From<PoolItem> for CoreGrpcClient {
149 fn from(client: PoolItem) -> Self {
150 match client {
151 PoolItem::Core(client) => client,
152 _ => {
153 tracing::error!(
154 ?client,
155 "invalid connection fetched from pool: expected core client"
156 );
157 panic!("ClientType is not Core: {:?}", client)
158 }
159 }
160 }
161}
162
163pub enum PoolPrefix {
165 Core,
166 Platform,
167}
168impl Display for PoolPrefix {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 PoolPrefix::Core => write!(f, "Core"),
172 PoolPrefix::Platform => write!(f, "Platform"),
173 }
174 }
175}
176impl From<&PoolItem> for PoolPrefix {
177 fn from(item: &PoolItem) -> Self {
178 match item {
179 PoolItem::Core(_) => PoolPrefix::Core,
180 PoolItem::Platform(_) => PoolPrefix::Platform,
181 }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::RequestSettings;
189 use dapi_grpc::tonic::transport::Channel;
190 use std::str::FromStr;
191 use std::time::Duration;
192
193 fn test_uri() -> Uri {
194 Uri::from_str("http://127.0.0.1:3000").unwrap()
195 }
196
197 fn make_platform_pool_item() -> PoolItem {
198 let channel = Channel::builder(test_uri()).connect_lazy();
199 PoolItem::Platform(PlatformGrpcClient::new(channel))
200 }
201
202 fn make_core_pool_item() -> PoolItem {
203 let channel = Channel::builder(test_uri()).connect_lazy();
204 PoolItem::Core(CoreGrpcClient::new(channel))
205 }
206
207 #[test]
208 fn test_connection_pool_new() {
209 let pool = ConnectionPool::new(10);
210 let result = pool.get(PoolPrefix::Platform, &test_uri(), None);
211 assert!(result.is_none());
212 }
213
214 #[test]
215 fn test_connection_pool_default() {
216 let pool = ConnectionPool::default();
217 let result = pool.get(PoolPrefix::Core, &test_uri(), None);
218 assert!(result.is_none());
219 }
220
221 #[tokio::test]
222 async fn test_connection_pool_put_and_get_platform() {
223 let pool = ConnectionPool::new(10);
224 let uri = test_uri();
225 let item = make_platform_pool_item();
226
227 pool.put(&uri, None, item);
228
229 let result = pool.get(PoolPrefix::Platform, &uri, None);
230 assert!(result.is_some());
231 assert!(matches!(result.unwrap(), PoolItem::Platform(_)));
232 }
233
234 #[tokio::test]
235 async fn test_connection_pool_put_and_get_core() {
236 let pool = ConnectionPool::new(10);
237 let uri = test_uri();
238 let item = make_core_pool_item();
239
240 pool.put(&uri, None, item);
241
242 let result = pool.get(PoolPrefix::Core, &uri, None);
243 assert!(result.is_some());
244 assert!(matches!(result.unwrap(), PoolItem::Core(_)));
245 }
246
247 #[tokio::test]
248 async fn test_connection_pool_get_or_create_creates_new() {
249 let pool = ConnectionPool::new(10);
250 let uri = test_uri();
251
252 let result: Result<PoolItem, String> =
253 pool.get_or_create(PoolPrefix::Platform, &uri, None, || {
254 Ok(make_platform_pool_item())
255 });
256
257 assert!(result.is_ok());
258
259 let mut create_called = false;
261 let result2: Result<PoolItem, String> =
262 pool.get_or_create(PoolPrefix::Platform, &uri, None, || {
263 create_called = true;
264 Ok(make_platform_pool_item())
265 });
266
267 assert!(result2.is_ok());
268 assert!(
269 !create_called,
270 "create should not be called for cached item"
271 );
272 }
273
274 #[test]
275 fn test_connection_pool_get_or_create_error_not_cached() {
276 let pool = ConnectionPool::new(10);
277 let uri = test_uri();
278
279 let result: Result<PoolItem, String> =
280 pool.get_or_create(PoolPrefix::Platform, &uri, None, || {
281 Err("creation failed".to_string())
282 });
283
284 assert!(result.is_err());
285
286 let cached = pool.get(PoolPrefix::Platform, &uri, None);
288 assert!(cached.is_none());
289 }
290
291 #[test]
292 fn test_pool_prefix_display() {
293 assert_eq!(format!("{}", PoolPrefix::Core), "Core");
294 assert_eq!(format!("{}", PoolPrefix::Platform), "Platform");
295 }
296
297 #[tokio::test]
298 async fn test_pool_prefix_from_pool_item() {
299 let platform_item = make_platform_pool_item();
300 let prefix: PoolPrefix = (&platform_item).into();
301 assert!(matches!(prefix, PoolPrefix::Platform));
302
303 let core_item = make_core_pool_item();
304 let prefix: PoolPrefix = (&core_item).into();
305 assert!(matches!(prefix, PoolPrefix::Core));
306 }
307
308 #[tokio::test]
309 async fn test_pool_item_from_platform_client() {
310 let channel = Channel::builder(test_uri()).connect_lazy();
311 let client = PlatformGrpcClient::new(channel);
312 let item: PoolItem = client.into();
313 assert!(matches!(item, PoolItem::Platform(_)));
314 }
315
316 #[tokio::test]
317 async fn test_pool_item_from_core_client() {
318 let channel = Channel::builder(test_uri()).connect_lazy();
319 let client = CoreGrpcClient::new(channel);
320 let item: PoolItem = client.into();
321 assert!(matches!(item, PoolItem::Core(_)));
322 }
323
324 #[tokio::test]
325 async fn test_pool_item_into_platform_client() {
326 let item = make_platform_pool_item();
327 let _client: PlatformGrpcClient = item.into();
328 }
329
330 #[tokio::test]
331 async fn test_pool_item_into_core_client() {
332 let item = make_core_pool_item();
333 let _client: CoreGrpcClient = item.into();
334 }
335
336 #[tokio::test]
337 #[should_panic(expected = "ClientType is not Platform")]
338 async fn test_pool_item_core_into_platform_panics() {
339 let item = make_core_pool_item();
340 let _client: PlatformGrpcClient = item.into();
341 }
342
343 #[tokio::test]
344 #[should_panic(expected = "ClientType is not Core")]
345 async fn test_pool_item_platform_into_core_panics() {
346 let item = make_platform_pool_item();
347 let _client: CoreGrpcClient = item.into();
348 }
349
350 #[tokio::test]
351 async fn test_connection_pool_shares_client_across_per_request_settings() {
352 let pool = ConnectionPool::new(10);
353 let uri = test_uri();
354
355 let stored = RequestSettings {
358 timeout: Some(Duration::from_secs(30)),
359 retries: Some(3),
360 ban_failed_address: Some(false),
361 ..RequestSettings::default()
362 }
363 .finalize();
364 pool.put(&uri, Some(&stored), make_platform_pool_item());
365
366 let default = RequestSettings::default().finalize();
367 assert!(
368 pool.get(PoolPrefix::Platform, &uri, Some(&default))
369 .is_some(),
370 "per-request settings must not split pooled connections"
371 );
372
373 let connect = RequestSettings {
375 connect_timeout: Some(Duration::from_secs(3)),
376 ..RequestSettings::default()
377 }
378 .finalize();
379 assert!(
380 pool.get(PoolPrefix::Platform, &uri, Some(&connect))
381 .is_none(),
382 "connection-affecting settings must key separate connections"
383 );
384 }
385
386 #[tokio::test]
387 async fn test_connection_pool_different_prefixes_different_keys() {
388 let pool = ConnectionPool::new(10);
389 let uri = test_uri();
390
391 pool.put(&uri, None, make_platform_pool_item());
392
393 let result = pool.get(PoolPrefix::Core, &uri, None);
395 assert!(result.is_none());
396
397 let result = pool.get(PoolPrefix::Platform, &uri, None);
399 assert!(result.is_some());
400 }
401
402 #[tokio::test]
403 async fn test_connection_pool_clone_shares_data() {
404 let pool = ConnectionPool::new(10);
405 let pool_clone = pool.clone();
406 let uri = test_uri();
407
408 pool.put(&uri, None, make_platform_pool_item());
409
410 let result = pool_clone.get(PoolPrefix::Platform, &uri, None);
412 assert!(result.is_some());
413 }
414}