Skip to main content

rama_net/client/pool/
exclusive.rs

1//! Exclusive single-use connection pool.
2//!
3//! [`LruDropPool`] hands out a [`LeasedConnection`] that the caller owns for the
4//! duration of its use and that is returned to the pool on drop. Each pooled
5//! connection serves a single user at a time.
6
7#[cfg(feature = "opentelemetry")]
8use super::metrics;
9use super::{ActiveSlot, ConnID, ConnectionResult, Pool, PoolSlot};
10use crate::address::SocketAddress;
11use crate::conn::{ConnectionHealth, ConnectionHealthWatcher};
12use crate::stream::Socket;
13use parking_lot::Mutex;
14use rama_core::Service;
15use rama_core::error::BoxErrorExt as _;
16use rama_core::error::{BoxError, ErrorContext, ErrorExt};
17use rama_core::extensions::{Extension, Extensions, ExtensionsRef};
18use rama_core::telemetry::tracing::trace;
19use rama_utils::macros::generate_set_and_with;
20use std::collections::VecDeque;
21use std::fmt::Debug;
22use std::mem::ManuallyDrop;
23use std::ops::{Deref, DerefMut};
24use std::pin::Pin;
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::{Arc, Weak};
27use std::time::{Duration, Instant};
28use tokio::io::{AsyncRead, AsyncWrite};
29use tokio::sync::Semaphore;
30
31/// [`LeasedConnection`] is a connection that is temporarily leased from a pool
32///
33/// It will be returned to the pool once dropped if the user didn't
34/// take ownership of the connection `C` with [`LeasedConnection::into_connection()`].
35/// [`LeasedConnection`]s are considered active pool connections until dropped or
36/// ownership is taken of the internal connection.
37pub struct LeasedConnection<C: ExtensionsRef, ID> {
38    pooled_conn: ManuallyDrop<PooledConnection<C, ID>>,
39    pooled_conn_taken: bool,
40    active_slot: ActiveSlot,
41    returner: ConnReturner<C, ID>,
42    got_response: AtomicBool,
43    drop_connection_if_no_response: bool,
44}
45
46impl<C: ExtensionsRef, ID> LeasedConnection<C, ID> {
47    pub fn into_connection(mut self) -> C {
48        // We cannot use ::into_inner as we still require a Drop impl as well, so
49        // we assign pooled_conn_taken to true to avoid double-dropping.
50        self.pooled_conn_taken = true;
51        // SAFETY: value is only dropped in `Self::Drop`, and value is only taken
52        // here if we move out of leased drop.
53        unsafe { ManuallyDrop::take(&mut self.pooled_conn) }.conn
54    }
55}
56
57impl<C: ExtensionsRef, ID> ExtensionsRef for LeasedConnection<C, ID> {
58    fn extensions(&self) -> &Extensions {
59        self.pooled_conn.extensions()
60    }
61}
62
63/// A connection which is stored in a pool.
64///
65/// A ID is used to determine which connections can be used for a request.
66/// This ID encodes all the details that make a connection unique/suitable for a request.
67struct PooledConnection<C, ID> {
68    conn: C,
69    id: ID,
70    pool_slot: PoolSlot,
71    last_used: Instant,
72}
73
74impl<C: ExtensionsRef, ID> ExtensionsRef for PooledConnection<C, ID> {
75    fn extensions(&self) -> &Extensions {
76        self.conn.extensions()
77    }
78}
79
80/// Connection pool that uses LRU to evict connections
81pub struct LruDropPool<C, ID> {
82    storage: Arc<Mutex<VecDeque<PooledConnection<C, ID>>>>,
83    total_slots: Arc<Semaphore>,
84    active_slots: Arc<Semaphore>,
85    idle_timeout: Option<Duration>,
86    returner: ConnReturner<C, ID>,
87    reuse_strategy: ReuseStrategy,
88    drop_connection_if_no_response: bool,
89    #[cfg(feature = "opentelemetry")]
90    metrics: Option<Arc<metrics::PoolMetrics>>,
91}
92
93#[non_exhaustive]
94#[derive(Clone, Copy, Debug, Default)]
95pub enum ReuseStrategy {
96    #[default]
97    FiFo,
98    RoundRobin,
99}
100
101struct ConnReturner<C, ID> {
102    weak_storage: Weak<Mutex<VecDeque<PooledConnection<C, ID>>>>,
103}
104
105impl<C, ID> Clone for ConnReturner<C, ID> {
106    fn clone(&self) -> Self {
107        Self {
108            weak_storage: self.weak_storage.clone(),
109        }
110    }
111}
112
113impl<C, ID> ConnReturner<C, ID> {
114    fn return_conn(&self, mut conn: PooledConnection<C, ID>) {
115        if let Some(storage) = self.weak_storage.upgrade() {
116            // Ensure correct ordering by locking storage before loading the
117            // last used time.
118            let mut storage = storage.lock();
119            conn.last_used = Instant::now();
120            storage.push_front(conn);
121        }
122    }
123}
124
125impl<C, ID> Clone for LruDropPool<C, ID> {
126    fn clone(&self) -> Self {
127        Self {
128            storage: self.storage.clone(),
129            total_slots: self.total_slots.clone(),
130            active_slots: self.active_slots.clone(),
131            returner: self.returner.clone(),
132            idle_timeout: self.idle_timeout,
133            reuse_strategy: self.reuse_strategy,
134            drop_connection_if_no_response: self.drop_connection_if_no_response,
135            #[cfg(feature = "opentelemetry")]
136            metrics: self.metrics.clone(),
137        }
138    }
139}
140
141impl<C, ID> LruDropPool<C, ID> {
142    pub fn try_new(max_active: usize, max_total: usize) -> Result<Self, BoxError> {
143        if max_active == 0 || max_total == 0 {
144            return Err(BoxError::from_static_str(
145                "max_active or max_total of 0 will make this pool unusable",
146            )
147            .context_field("max_active", max_active)
148            .context_field("max_total", max_total));
149        }
150        if max_active > max_total {
151            return Err(BoxError::from_static_str(
152                "max_active should be smaller or equal to max_total",
153            )
154            .context_field("max_active", max_active)
155            .context_field("max_total", max_total));
156        }
157        let storage = Arc::new(Mutex::new(VecDeque::with_capacity(max_total)));
158        let weak_storage = Arc::downgrade(&storage);
159        Ok(Self {
160            storage,
161            returner: ConnReturner { weak_storage },
162            total_slots: Arc::new(Semaphore::const_new(max_total)),
163            active_slots: Arc::new(Semaphore::const_new(max_active)),
164            idle_timeout: None,
165            reuse_strategy: ReuseStrategy::default(),
166            drop_connection_if_no_response: true,
167            #[cfg(feature = "opentelemetry")]
168            metrics: None,
169        })
170    }
171
172    generate_set_and_with! {
173        /// If connections have been idle for longer then the provided timeout they
174        /// will be dropped and removed from the pool
175        ///
176        /// Note: timeout is only checked when a connection is requested from the pool,
177        /// it is not something that is done periodically
178        pub fn idle_timeout(mut self, timeout: Option<Duration>) -> Self {
179            self.idle_timeout = timeout;
180            self
181        }
182    }
183
184    generate_set_and_with! {
185        pub fn reuse_strategy(mut self, strategy: ReuseStrategy) -> Self {
186            self.reuse_strategy = strategy;
187            self
188        }
189    }
190
191    generate_set_and_with! {
192        /// If enabled (the default), connections that did not receive a response
193        /// will be evicted from the pool instead of being returned for reuse.
194        ///
195        /// This includes timeouts, cancellations, and errors.
196        pub fn drop_connection_if_no_response(mut self, drop_connection_if_no_response: bool) -> Self {
197            self.drop_connection_if_no_response = drop_connection_if_no_response;
198            self
199        }
200    }
201
202    #[cfg(feature = "opentelemetry")]
203    generate_set_and_with! {
204        #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))]
205        pub fn metrics(mut self, metrics: Option<Arc<metrics::PoolMetrics>>) -> Self {
206            self.metrics = metrics;
207            self
208        }
209    }
210}
211
212impl<C, ID> Pool<C, ID> for LruDropPool<C, ID>
213where
214    C: Send + ExtensionsRef + 'static,
215    ID: ConnID,
216{
217    type Connection = LeasedConnection<C, ID>;
218    type CreatePermit = (ActiveSlot, PoolSlot);
219
220    async fn get_conn(
221        &self,
222        id: &ID,
223    ) -> Result<ConnectionResult<Self::Connection, Self::CreatePermit>, BoxError> {
224        #[cfg(feature = "opentelemetry")]
225        let metrics = self
226            .metrics
227            .as_ref()
228            .map(|metrics| (metrics, metrics.attributes(id)));
229
230        #[cfg(feature = "opentelemetry")]
231        let start = Instant::now();
232        let active_slot = ActiveSlot(
233            self.active_slots
234                .clone()
235                .acquire_owned()
236                .await
237                .context("get active pool slot")?,
238        );
239
240        #[cfg(feature = "opentelemetry")]
241        if let Some((metrics, metric_attrs)) = &metrics {
242            let active_connection_delay_nanoseconds = start.elapsed().as_nanos() as f64;
243            metrics
244                .active_connection_delay_nanoseconds
245                .record(active_connection_delay_nanoseconds, metric_attrs);
246        };
247
248        let mut storage = self.storage.lock();
249
250        if let Some(timeout) = self.idle_timeout {
251            // Since new connections are always returned to the front of the
252            // queue, they are ordered from most to least recently used. To
253            // provide a stable predicate, we load `now` once and use it for all
254            // comparisons, rather than using `conn.last_used.elapsed()`, which
255            // would use an updated "current" time for every comparison. The
256            // `partition_point` method performs a binary search to find the
257            // index of the first element for which the predicate returns false,
258            // i.e. the first connection past the idle timeout. All connections
259            // from that index onwards are timed out and can be dropped.
260            let now = Instant::now();
261            let idx = storage.partition_point(|conn| now.duration_since(conn.last_used) <= timeout);
262            if idx < storage.len() {
263                trace!(
264                    "LRU connection pool: idle timeout was triggered, dropping connections with index {idx:?} and later"
265                );
266                storage.drain(idx..);
267            }
268        }
269
270        let mut get_conn = || loop {
271            let idx = match self.reuse_strategy {
272                ReuseStrategy::FiFo => storage.iter().position(|stored| &stored.id == id)?,
273                ReuseStrategy::RoundRobin => storage.iter().rposition(|stored| &stored.id == id)?,
274            };
275
276            let pooled_conn = storage.remove(idx)?;
277
278            // This will make sure we skip and drop broken connections
279            if let Some(watcher) = pooled_conn
280                .extensions()
281                .get_ref::<ConnectionHealthWatcher>()
282                && watcher.health() == ConnectionHealth::Broken
283            {
284                continue;
285            }
286
287            return Some((idx, pooled_conn));
288        };
289
290        if let Some((idx, pooled_conn)) = get_conn() {
291            trace!("LRU connection pool: connection #{idx} found for given id {id:?}");
292
293            #[cfg(feature = "opentelemetry")]
294            if let Some((metrics, metric_attrs)) = &metrics {
295                metrics.total_connections.add(1, metric_attrs);
296                metrics.reused_connections.add(1, metric_attrs);
297            }
298
299            return Ok(ConnectionResult::Connection(LeasedConnection {
300                active_slot,
301                pooled_conn: ManuallyDrop::new(pooled_conn),
302                pooled_conn_taken: false,
303                returner: self.returner.clone(),
304                got_response: AtomicBool::new(false),
305                drop_connection_if_no_response: self.drop_connection_if_no_response,
306            }));
307        }
308
309        let pool_slot = match self.total_slots.clone().try_acquire_owned() {
310            Ok(permit) => PoolSlot(permit),
311            Err(err) => {
312                // By poping from back when we have no new Poolslot available we implement LRU drop policy
313                trace!(
314                    error = %err,
315                    "LRU connection pool: evicting lru connection (#{id:?}) to create a new one"
316                );
317                #[cfg(feature = "opentelemetry")]
318                if let Some((metrics, metric_attrs)) = &metrics {
319                    metrics.evicted_connections.add(1, metric_attrs);
320                }
321                storage
322                    .pop_back()
323                    .context("get least recently used connection from storage")?
324                    .pool_slot
325            }
326        };
327
328        trace!(
329            "LRU connection pool: no connection for given id {id:?} found, returning create permit"
330        );
331        Ok(ConnectionResult::CreatePermit((active_slot, pool_slot)))
332    }
333
334    async fn create(&self, id: ID, conn: C, permit: Self::CreatePermit) -> Self::Connection {
335        trace!("adding new connection (w/ id {id:?}) to pool");
336        let (active_slot, pool_slot) = permit;
337
338        #[cfg(feature = "opentelemetry")]
339        if let Some(metrics) = &self.metrics.as_ref() {
340            let metric_attrs = metrics.attributes(&id);
341            metrics.total_connections.add(1, &metric_attrs);
342            metrics.created_connections.add(1, &metric_attrs);
343        }
344
345        LeasedConnection {
346            active_slot,
347            returner: self.returner.clone(),
348            pooled_conn: ManuallyDrop::new(PooledConnection {
349                id,
350                conn,
351                pool_slot,
352                last_used: Instant::now(),
353            }),
354            pooled_conn_taken: false,
355            got_response: AtomicBool::new(false),
356            drop_connection_if_no_response: self.drop_connection_if_no_response,
357        }
358    }
359}
360impl<C: Debug, ID: Debug> Debug for PooledConnection<C, ID> {
361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362        f.debug_struct("PooledConnection")
363            .field("conn", &self.conn)
364            .field("id", &self.id)
365            .field("pool_slot", &self.pool_slot)
366            .finish()
367    }
368}
369
370impl<C, ID> Debug for LeasedConnection<C, ID>
371where
372    C: Debug + ExtensionsRef,
373    ID: Debug,
374{
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        f.debug_struct("LeasedConnection")
377            .field("pooled_conn", self.pooled_conn.deref())
378            .field("active_slot", &self.active_slot)
379            .finish()
380    }
381}
382
383impl<C: ExtensionsRef, ID> Deref for LeasedConnection<C, ID> {
384    type Target = C;
385
386    fn deref(&self) -> &Self::Target {
387        &self.pooled_conn.conn
388    }
389}
390
391impl<C: ExtensionsRef, ID> DerefMut for LeasedConnection<C, ID> {
392    fn deref_mut(&mut self) -> &mut Self::Target {
393        &mut self.pooled_conn.conn
394    }
395}
396
397impl<C: ExtensionsRef, ID> AsRef<C> for LeasedConnection<C, ID> {
398    fn as_ref(&self) -> &C {
399        self
400    }
401}
402
403impl<C: ExtensionsRef, ID> AsMut<C> for LeasedConnection<C, ID> {
404    fn as_mut(&mut self) -> &mut C {
405        self
406    }
407}
408
409impl<C: ExtensionsRef, ID> Drop for LeasedConnection<C, ID> {
410    fn drop(&mut self) {
411        if !self.pooled_conn_taken {
412            if self.drop_connection_if_no_response && !self.got_response.load(Ordering::Relaxed) {
413                trace!("LRU connection pool: dropping connection that didn't receive a response");
414                unsafe { ManuallyDrop::drop(&mut self.pooled_conn) };
415                return;
416            }
417            if let Some(watcher) = self.extensions().get_ref::<ConnectionHealthWatcher>()
418                && watcher.health() == ConnectionHealth::Broken
419            {
420                trace!("LRU connection pool: dropping pooled connection that was marked as failed");
421
422                // SAFETY: pooled_conn_taken is false,
423                // indicating we didn't move ownership yet by
424                // using Self::into_inner, and we are neither
425                // returning it as is done in the other (else)
426                // branch only.
427                unsafe { ManuallyDrop::drop(&mut self.pooled_conn) };
428            } else {
429                trace!("LRU connection pool: returning pooled connection back to pool");
430
431                // SAFETY: pooled_conn_taken is false,
432                // indicating we didn't move ownership yet by
433                // using Self::into_inner, and neither do we drop it as that is only
434                // done in the 'truth' variant of this if-else branching
435                // as can be seen above.
436                let pooled_conn = unsafe { ManuallyDrop::take(&mut self.pooled_conn) };
437                self.returner.return_conn(pooled_conn);
438            }
439        }
440    }
441}
442
443// We want to be able to use LeasedConnection as a transparent wrapper around our connection.
444// To achieve that we conditially implement all traits that are used by our Connectors
445
446impl<C, ID> Socket for LeasedConnection<C, ID>
447where
448    ID: Send + Sync + 'static,
449    C: Socket + ExtensionsRef,
450{
451    fn local_addr(&self) -> std::io::Result<SocketAddress> {
452        self.as_ref().local_addr()
453    }
454
455    fn peer_addr(&self) -> std::io::Result<SocketAddress> {
456        self.as_ref().peer_addr()
457    }
458}
459
460#[warn(clippy::missing_trait_methods)]
461impl<C, ID> AsyncWrite for LeasedConnection<C, ID>
462where
463    C: AsyncWrite + Unpin + ExtensionsRef,
464    ID: Unpin,
465{
466    fn poll_write(
467        mut self: std::pin::Pin<&mut Self>,
468        cx: &mut std::task::Context<'_>,
469        buf: &[u8],
470    ) -> std::task::Poll<Result<usize, std::io::Error>> {
471        Pin::new(self.deref_mut().as_mut()).poll_write(cx, buf)
472    }
473
474    fn poll_flush(
475        mut self: std::pin::Pin<&mut Self>,
476        cx: &mut std::task::Context<'_>,
477    ) -> std::task::Poll<Result<(), std::io::Error>> {
478        Pin::new(self.deref_mut().as_mut()).poll_flush(cx)
479    }
480
481    fn poll_shutdown(
482        mut self: std::pin::Pin<&mut Self>,
483        cx: &mut std::task::Context<'_>,
484    ) -> std::task::Poll<Result<(), std::io::Error>> {
485        Pin::new(self.deref_mut().as_mut()).poll_shutdown(cx)
486    }
487
488    fn is_write_vectored(&self) -> bool {
489        self.deref().is_write_vectored()
490    }
491
492    fn poll_write_vectored(
493        mut self: Pin<&mut Self>,
494        cx: &mut std::task::Context<'_>,
495        bufs: &[std::io::IoSlice<'_>],
496    ) -> std::task::Poll<Result<usize, std::io::Error>> {
497        Pin::new(self.deref_mut().as_mut()).poll_write_vectored(cx, bufs)
498    }
499}
500
501#[warn(clippy::missing_trait_methods)]
502impl<C, ID> AsyncRead for LeasedConnection<C, ID>
503where
504    C: AsyncRead + Unpin + ExtensionsRef,
505    ID: Unpin,
506{
507    fn poll_read(
508        mut self: Pin<&mut Self>,
509        cx: &mut std::task::Context<'_>,
510        buf: &mut tokio::io::ReadBuf<'_>,
511    ) -> std::task::Poll<std::io::Result<()>> {
512        Pin::new(self.deref_mut().as_mut()).poll_read(cx, buf)
513    }
514}
515
516impl<Input, C, ID> Service<Input> for LeasedConnection<C, ID>
517where
518    ID: Send + Sync + Debug + 'static,
519    C: Service<Input> + ExtensionsRef,
520    Input: Send + 'static,
521{
522    type Output = C::Output;
523    type Error = C::Error;
524
525    async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
526        self.got_response.store(false, Ordering::Relaxed);
527        let result = self.as_ref().serve(input).await;
528        if result.is_ok() {
529            self.got_response.store(true, Ordering::Relaxed);
530        }
531        result
532    }
533}
534
535/// Helper needed so we can implement debug for LruDropPool
536///
537/// Implementing debug_list and debug_struct at the same time is not
538/// possible, so we have to split it up
539struct StorageDebugHelper<'a, C, ID: Debug> {
540    deque: &'a VecDeque<PooledConnection<C, ID>>,
541}
542
543impl<'a, C, ID: Debug> Debug for StorageDebugHelper<'a, C, ID> {
544    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545        f.debug_list()
546            .entries(self.deque.iter().map(|item| &item.id))
547            .finish()
548    }
549}
550
551impl<C, ID: Debug> Debug for LruDropPool<C, ID> {
552    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
553        let mut builder = f.debug_struct("LruDropPool");
554
555        // Don't block on this, it's only for debugging
556        match self.storage.try_lock() {
557            Some(guard) => {
558                let storage_debugger = StorageDebugHelper { deque: &*guard };
559                builder.field("storage", &storage_debugger);
560            }
561            None => {
562                builder.field("storage", &"Mutex(locked)");
563            }
564        };
565
566        builder
567            .field("total_slots", &self.total_slots)
568            .field("active_slots", &self.active_slots)
569            .field("idle_timeout", &self.idle_timeout)
570            .field("reuse_strategy", &self.reuse_strategy)
571            .finish()
572    }
573}
574
575impl<C, ID> Extension for LruDropPool<C, ID>
576where
577    C: Send + 'static,
578    ID: Send + Sync + Debug + 'static,
579{
580}
581
582#[cfg(test)]
583mod tests {
584    use super::super::{PooledConnector, ReqToConnID};
585    use super::*;
586    use crate::client::{ConnectorService, EstablishedClientConnection};
587    use rama_core::ServiceInput;
588    use rama_core::extensions::ExtensionsRef;
589    use rama_core::{Service, extensions::Extensions};
590    use std::sync::atomic::AtomicBool;
591    use std::{
592        convert::Infallible,
593        sync::atomic::{AtomicI16, Ordering},
594    };
595    use tokio_test::assert_ok;
596
597    struct TestService {
598        pub created_connection: AtomicI16,
599    }
600
601    impl Default for TestService {
602        fn default() -> Self {
603            Self {
604                created_connection: AtomicI16::new(0),
605            }
606        }
607    }
608
609    #[derive(Debug)]
610    struct Conn {
611        items: Vec<u32>,
612        extensions: Extensions,
613    }
614
615    impl Conn {
616        fn new() -> Self {
617            Self {
618                items: vec![],
619                extensions: Extensions::new(),
620            }
621        }
622    }
623
624    impl ExtensionsRef for Conn {
625        fn extensions(&self) -> &Extensions {
626            &self.extensions
627        }
628    }
629
630    impl Deref for Conn {
631        type Target = Vec<u32>;
632
633        fn deref(&self) -> &Self::Target {
634            &self.items
635        }
636    }
637
638    impl DerefMut for Conn {
639        fn deref_mut(&mut self) -> &mut Self::Target {
640            &mut self.items
641        }
642    }
643
644    impl<Input> Service<Input> for TestService
645    where
646        Input: Send + 'static,
647    {
648        type Output = EstablishedClientConnection<Conn, Input>;
649        type Error = Infallible;
650
651        async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
652            self.created_connection.fetch_add(1, Ordering::Relaxed);
653            Ok(EstablishedClientConnection {
654                input,
655                conn: Conn::new(),
656            })
657        }
658    }
659
660    #[derive(Clone)]
661    /// [`StringInputLengthID`] will map inputs of type ServiceInput<String>, to usize id representing their
662    /// chars length. In practise this will mean that inputs of the same char length will be
663    /// able to reuse the same connections
664    struct StringInputLengthID;
665
666    impl ReqToConnID<ServiceInput<String>> for StringInputLengthID {
667        type ID = usize;
668
669        fn id(&self, input: &ServiceInput<String>) -> Result<Self::ID, BoxError> {
670            Ok(input.input.chars().count())
671        }
672    }
673
674    impl ConnID for usize {}
675    impl ConnID for () {}
676
677    #[tokio::test]
678    async fn test_should_reuse_connections() {
679        let pool = LruDropPool::try_new(5, 10)
680            .unwrap()
681            .with_drop_connection_if_no_response(false);
682        // We use a closure here to maps all requests to `()` id, this will result in all connections being shared and the pool
683        // acting like like a global connection pool (eg database connection pool where all connections can be used).
684        let svc = PooledConnector::new(
685            TestService::default(),
686            pool,
687            |__req: &ServiceInput<String>| Ok(()),
688        );
689
690        let iterations = 10;
691        for _i in 0..iterations {
692            let _conn = svc.connect(ServiceInput::new(String::new())).await.unwrap();
693        }
694
695        let created_connection = svc.inner.created_connection.load(Ordering::Relaxed);
696        assert_eq!(created_connection, 1);
697    }
698
699    #[tokio::test]
700    async fn test_conn_id_to_separate() {
701        let pool = LruDropPool::try_new(5, 10)
702            .unwrap()
703            .with_drop_connection_if_no_response(false);
704        let svc = PooledConnector::new(TestService::default(), pool, StringInputLengthID {});
705
706        {
707            let mut conn = svc
708                .connect(ServiceInput::new(String::from("a")))
709                .await
710                .unwrap()
711                .conn;
712
713            conn.push(1);
714            assert_eq!(conn.as_ref().deref(), &vec![1]);
715            assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 1);
716        }
717
718        // Should reuse the same connections
719        {
720            let mut conn = svc
721                .connect(ServiceInput::new(String::from("B")))
722                .await
723                .unwrap()
724                .conn;
725
726            conn.push(2);
727            assert_eq!(conn.as_ref().deref(), &vec![1, 2]);
728            assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 1);
729        }
730
731        // Should make a new one
732        {
733            let mut conn = svc
734                .connect(ServiceInput::new(String::from("aa")))
735                .await
736                .unwrap()
737                .conn;
738
739            conn.push(3);
740            assert_eq!(conn.as_ref().deref(), &vec![3]);
741            assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 2);
742        }
743
744        // Should reuse
745        {
746            let mut conn = svc
747                .connect(ServiceInput::new(String::from("bb")))
748                .await
749                .unwrap()
750                .conn;
751
752            conn.push(4);
753            assert_eq!(conn.as_ref().deref(), &vec![3, 4]);
754            assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 2);
755        }
756    }
757
758    #[tokio::test]
759    async fn test_pool_max_size() {
760        let pool = LruDropPool::try_new(1, 1)
761            .unwrap()
762            .with_drop_connection_if_no_response(false);
763        let svc = PooledConnector::new(TestService::default(), pool, StringInputLengthID {})
764            .with_wait_for_pool_timeout(Duration::from_millis(50));
765
766        let conn1 = svc
767            .connect(ServiceInput::new(String::from("a")))
768            .await
769            .unwrap();
770
771        let conn2 = svc.connect(ServiceInput::new(String::from("a"))).await;
772        let _error = conn2.unwrap_err();
773
774        drop(conn1);
775        let _conn3 = svc
776            .connect(ServiceInput::new(String::from("aaa")))
777            .await
778            .unwrap();
779    }
780
781    #[derive(Default)]
782    struct TestConnector {
783        pub created_connection: AtomicI16,
784    }
785
786    impl<Input> Service<Input> for TestConnector
787    where
788        Input: Send + 'static,
789    {
790        type Output = EstablishedClientConnection<InnerService, Input>;
791        type Error = Infallible;
792
793        async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
794            let conn = InnerService::default();
795
796            conn.extensions().insert(ConnectionHealthWatcher::default());
797
798            self.created_connection.fetch_add(1, Ordering::Relaxed);
799            Ok(EstablishedClientConnection { input, conn })
800        }
801    }
802
803    #[derive(Default, Debug)]
804    struct InnerService {
805        should_error: Arc<AtomicBool>,
806        extensions: Extensions,
807    }
808
809    impl ExtensionsRef for InnerService {
810        fn extensions(&self) -> &Extensions {
811            &self.extensions
812        }
813    }
814
815    impl Service<bool> for InnerService {
816        type Output = ();
817        type Error = BoxError;
818
819        async fn serve(&self, should_error: bool) -> Result<Self::Output, Self::Error> {
820            // Once this service is broken it will stay in this state, similar to a closed tcp connection
821            if should_error {
822                self.extensions
823                    .get_ref::<ConnectionHealthWatcher>()
824                    .unwrap()
825                    .mark_broken();
826                self.should_error.store(true, Ordering::Relaxed);
827            }
828
829            if self.should_error.load(Ordering::Relaxed) {
830                Err(BoxError::from_static_str("service is in broken state"))
831            } else {
832                Ok(())
833            }
834        }
835    }
836
837    impl Service<Duration> for InnerService {
838        type Output = ();
839        type Error = BoxError;
840
841        async fn serve(&self, delay: Duration) -> Result<Self::Output, Self::Error> {
842            tokio::time::sleep(delay).await;
843            Ok(())
844        }
845    }
846
847    #[tokio::test]
848    async fn test_cancellated_fut_should_drop_connection_by_default() {
849        let pool = LruDropPool::try_new(1, 1).unwrap();
850        let svc = PooledConnector::new(TestConnector::default(), pool, StringInputLengthID {});
851
852        let conn = svc
853            .connect(ServiceInput::new(String::from("")))
854            .await
855            .unwrap();
856        assert_ok!(conn.conn.serve(false).await);
857        drop(conn);
858        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 1);
859
860        // Get the (reused) connection, start a slow request, and cancel it via timeout
861        let conn = svc
862            .connect(ServiceInput::new(String::from("")))
863            .await
864            .unwrap();
865        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 1);
866
867        let timeout_result = tokio::time::timeout(
868            Duration::from_millis(10),
869            conn.conn.serve(Duration::from_secs(60)),
870        )
871        .await;
872
873        assert!(timeout_result.is_err(), "should have timed out");
874        drop(conn);
875
876        // Next connection must be a fresh one: the cancelled one should not have been returned
877        let conn = svc
878            .connect(ServiceInput::new(String::from("")))
879            .await
880            .unwrap();
881        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 2);
882        assert_ok!(conn.conn.serve(false).await);
883    }
884
885    #[tokio::test]
886    async fn test_cancellated_fut_should_not_drop_connection_if_this_is_disabled() {
887        let pool = LruDropPool::try_new(1, 1)
888            .unwrap()
889            .with_drop_connection_if_no_response(false);
890        let svc = PooledConnector::new(TestConnector::default(), pool, StringInputLengthID {});
891
892        let conn = svc
893            .connect(ServiceInput::new(String::from("")))
894            .await
895            .unwrap();
896        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 1);
897
898        let timeout_result = tokio::time::timeout(
899            Duration::from_millis(10),
900            conn.conn.serve(Duration::from_secs(60)),
901        )
902        .await;
903        assert!(timeout_result.is_err(), "should have timed out");
904        drop(conn);
905
906        // With drop_connection_if_no_response disabled, the connection should be returned to the pool
907        let conn = svc
908            .connect(ServiceInput::new(String::from("")))
909            .await
910            .unwrap();
911        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 1);
912        assert_ok!(conn.conn.serve(false).await);
913    }
914
915    #[tokio::test]
916    async fn test_dont_return_broken_connections_to_pool() {
917        let pool = LruDropPool::try_new(1, 1).unwrap();
918        let svc = PooledConnector::new(TestConnector::default(), pool, StringInputLengthID {});
919
920        let conn = svc
921            .connect(ServiceInput::new(String::from("")))
922            .await
923            .unwrap();
924
925        let result = conn.conn.serve(false).await;
926        assert_ok!(result);
927        let result = conn.conn.serve(true).await;
928        let _error = result.unwrap_err();
929
930        // this dropped connection should not return to the pool, otherwise it will be permanently broken
931        drop(conn);
932
933        let conn = svc
934            .connect(ServiceInput::new(String::from("")))
935            .await
936            .unwrap();
937
938        let result = conn.conn.serve(false).await;
939        assert_ok!(result);
940
941        // this connection is not broken so it should return to the pool
942        drop(conn);
943
944        let conn = svc
945            .connect(ServiceInput::new(String::from("")))
946            .await
947            .unwrap();
948
949        let result = conn.conn.serve(false).await;
950        assert_ok!(result);
951
952        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 2);
953    }
954
955    #[tokio::test]
956    async fn test_pool_drops_broken_connections_in_get_conn() {
957        let pool = LruDropPool::try_new(1, 1).unwrap();
958        let svc = PooledConnector::new(TestConnector::default(), pool, StringInputLengthID {});
959
960        let conn = svc
961            .connect(ServiceInput::new(String::from("")))
962            .await
963            .unwrap();
964
965        let result = conn.conn.serve(false).await;
966        assert_ok!(result);
967
968        // This dropped connection should return to the pool, since it's not broken yet
969        let conn_extensions = conn.conn.extensions().clone();
970        drop(conn);
971
972        // Break connection -> eg go-away / tcp connection dropped by remote...
973        // Normally the connection would edit this in extensions but since we don't have ownership here
974        // we just clone the extensions and edit it like this
975        conn_extensions
976            .get_ref::<ConnectionHealthWatcher>()
977            .unwrap()
978            .mark_broken();
979
980        // We should get a new working connection here since health check has detect that the stored one was broken
981        let conn = svc
982            .connect(ServiceInput::new(String::from("")))
983            .await
984            .unwrap();
985
986        let result = conn.conn.serve(false).await;
987        assert_ok!(result);
988
989        // This connection is not broken so it should return to the pool
990        drop(conn);
991
992        // And we should be able to reuse it
993        let conn = svc
994            .connect(ServiceInput::new(String::from("")))
995            .await
996            .unwrap();
997
998        let result = conn.conn.serve(false).await;
999        assert_ok!(result);
1000
1001        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 2);
1002    }
1003
1004    #[tokio::test]
1005    async fn drop_idle_connections() {
1006        let pool = LruDropPool::try_new(5, 10)
1007            .unwrap()
1008            .with_idle_timeout(Duration::from_micros(1))
1009            .with_drop_connection_if_no_response(false);
1010
1011        let svc = PooledConnector::new(TestService::default(), pool, StringInputLengthID {});
1012
1013        let conn = svc
1014            .connect(ServiceInput::new(String::from("")))
1015            .await
1016            .unwrap()
1017            .conn;
1018
1019        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 1);
1020        drop(conn);
1021        // Need for this to consistently work in ci, we only need this sleep here
1022        // because we have a very very short idle timeout, this is never the problem
1023        // if we use realistic values
1024        tokio::time::sleep(Duration::from_millis(100)).await;
1025
1026        let conn = svc
1027            .connect(ServiceInput::new(String::from("")))
1028            .await
1029            .unwrap()
1030            .conn;
1031
1032        assert_eq!(svc.inner.created_connection.load(Ordering::Relaxed), 2);
1033        drop(conn);
1034    }
1035
1036    #[tokio::test]
1037    async fn fifo_reuse() {
1038        test_reuse(ReuseStrategy::FiFo, 1).await;
1039    }
1040
1041    #[tokio::test]
1042    async fn round_robin_reuse() {
1043        test_reuse(ReuseStrategy::RoundRobin, 0).await;
1044    }
1045
1046    async fn test_reuse(strategy: ReuseStrategy, expected: u32) {
1047        let pool = LruDropPool::try_new(5, 10)
1048            .unwrap()
1049            .with_reuse_strategy(strategy)
1050            .with_drop_connection_if_no_response(false);
1051
1052        let svc = PooledConnector::new(TestService::default(), pool, |_: &ServiceInput<()>| Ok(()));
1053
1054        // Open two concurrent connections and drop them.
1055        let mut conns = Vec::new();
1056        for i in 0..2 {
1057            let mut conn = svc.connect(ServiceInput::new(())).await.unwrap().conn;
1058            conn.pooled_conn.conn.push(i);
1059            conns.push(conn);
1060        }
1061
1062        drop(conns);
1063
1064        // We should now have two connections with the same key in the pool,
1065        // from most to least recently used. ([conn2, conn1]). Requesting
1066        // another connection should return the first or last one depending on
1067        // the reuse policy.
1068        let conn = svc.connect(ServiceInput::new(())).await.unwrap().conn;
1069
1070        assert_eq!(conn.pooled_conn.conn[0], expected);
1071    }
1072}