Skip to main content

rama_net/client/pool/
multiplex.rs

1//! Multiplexing connection pool.
2//!
3//! [`MultiplexPool`] keeps every connection in storage and hands out a cheap
4//! [`MultiplexedConnection`] that shares a connection connection through `&self`. A single
5//! connection serves up to `min(max_concurrent_streams, MaxConcurrency)` concurrent
6//! users (where [`MaxConcurrency`] is the connection's advertised capacity,
7//! defaulting to [`usize::MAX`] when unset), the exclusive [`super::LruDropPool`] is the
8//! special case of capacity = 1 for owned connections. If the connection pool is at max
9//! capacity the pool will wait until a connection with a matching ID has capacity again
10//! or it will evict an idle connection with a LRU policy.
11//!
12//! Because the connector stack runs for every request, a [`MultiplexedConnection`] is
13//! established, serves its single request, and is dropped, so a [`MultiplexedConnection`]
14//! is bound to exactly one connection (its [`super::ExtensionsRef`] forwards to that
15//! connection, which is required for extension propagation such as
16//! the negotiated http version) and concurrency is metered by counting live
17//! handouts. A [`MultiplexedConnection`] is not meant to outlive a single logical request and
18//! when it does it should only be used for one input/request at a time.
19
20use super::{ConnID, ConnectionResult, Pool, PoolSlot};
21use crate::conn::{ConnectionHealth, ConnectionHealthWatcher, MaxConcurrency};
22use parking_lot::Mutex;
23use rama_core::Service;
24use rama_core::error::BoxErrorExt as _;
25use rama_core::error::{BoxError, ErrorExt};
26use rama_core::extensions::{Extension, Extensions, ExtensionsRef, NetExtension};
27use rama_core::futures::StreamExt as _;
28use rama_core::futures::stream::FuturesUnordered;
29use rama_core::telemetry::tracing::trace;
30use rama_utils::macros::generate_set_and_with;
31use rama_utils::time::AtomicInstant;
32use std::fmt::Debug;
33use std::num::NonZeroUsize;
34use std::sync::Arc;
35use std::sync::atomic::{AtomicUsize, Ordering};
36use std::time::Duration;
37use tokio::sync::{Notify, Semaphore};
38
39#[cfg(feature = "opentelemetry")]
40use super::metrics;
41#[cfg(feature = "opentelemetry")]
42use std::time::Instant;
43
44/// Strategy used to pick a connection among several that share the same
45/// [`ConnID`] and still have stream capacity.
46#[derive(Debug, Clone, Copy, Default)]
47#[non_exhaustive]
48pub enum MuxSelection {
49    /// Pick the connection with the most free stream slots (best spread).
50    #[default]
51    LeastLoaded,
52    /// Pick the first connection with a free stream slot.
53    FirstAvailable,
54    /// Cycle through the eligible connections.
55    RoundRobin,
56}
57
58/// A connection stored in a [`MultiplexPool`].
59///
60/// The connection never leaves the pool, it is shared through [`MultiplexedConnection`]
61/// handles and only ever used via `&self`.
62struct StoredConnection<C, ID> {
63    conn: C,
64    id: ID,
65    max_concurrency: Option<Arc<MaxConcurrency>>,
66    active: AtomicUsize,
67    notify: Arc<Notify>,
68    last_idle: AtomicInstant,
69    _pool_slot: PoolSlot,
70}
71
72impl<C, ID> StoredConnection<C, ID> {
73    /// A connection is idle when none of its handouts are in flight.
74    fn is_idle(&self) -> bool {
75        self.active.load(Ordering::Relaxed) == 0
76    }
77
78    /// Effective per-connection concurrency: the connection's [`MaxConcurrency`]
79    /// extension ([`usize::MAX`] if unset), capped by the pool's
80    /// `max_concurrent_streams`. Read live on every admission, so it tracks
81    /// changes (e.g. h2 SETTINGS updates).
82    ///
83    /// A value of 0 is valid, e.g. a peer advertising `SETTINGS_MAX_CONCURRENT_STREAMS=0`
84    fn effective_capacity(&self, cap: usize) -> usize {
85        self.max_concurrency
86            .as_ref()
87            .map_or(usize::MAX, |m| m.get())
88            .min(cap)
89    }
90
91    /// Admit a new in-flight stream (while `active < limit`) and bind it to a
92    /// [`MultiplexedConnection`] in one step, so `active` is never incremented
93    /// without a handout to release it on drop. Returns `None` at capacity.
94    ///
95    /// Takes `&Arc<Self>` (not `&self`) since the handout needs to share the
96    /// `Arc`; `&Arc<Self>` as a method receiver is still unstable.
97    fn try_create_multiplexed(
98        self: &Arc<Self>,
99        cap: usize,
100    ) -> Option<MultiplexedConnection<C, ID>> {
101        let limit = self.effective_capacity(cap);
102        let mut active = self.active.load(Ordering::Relaxed);
103        loop {
104            if active >= limit {
105                return None;
106            }
107            match self.active.compare_exchange_weak(
108                active,
109                active + 1,
110                Ordering::Relaxed,
111                Ordering::Relaxed,
112            ) {
113                Ok(_) => {
114                    return Some(MultiplexedConnection {
115                        inner: self.clone(),
116                    });
117                }
118                Err(found) => active = found,
119            }
120        }
121    }
122}
123
124/// A cheap handle to a shared connection in a [`MultiplexPool`].
125///
126/// It implements [`Service`] by forwarding to the inner connection and counts as
127/// one of the connection's in-flight streams for its lifetime, the stream is
128/// released on drop.
129pub struct MultiplexedConnection<C, ID> {
130    inner: Arc<StoredConnection<C, ID>>,
131}
132
133impl<C, ID> Drop for MultiplexedConnection<C, ID> {
134    fn drop(&mut self) {
135        let prev = self.inner.active.fetch_sub(1, Ordering::Release);
136        if prev == 1 {
137            // last in-flight stream released: the connection just went idle
138            self.inner.last_idle.set_now();
139        }
140        // wake any waiters so they can re-check capacity on this connection
141        self.inner.notify.notify_waiters();
142    }
143}
144
145impl<C: ExtensionsRef, ID> ExtensionsRef for MultiplexedConnection<C, ID> {
146    fn extensions(&self) -> &Extensions {
147        self.inner.conn.extensions()
148    }
149}
150
151impl<Input, C, ID> Service<Input> for MultiplexedConnection<C, ID>
152where
153    C: Service<Input> + ExtensionsRef,
154    ID: Send + Sync + 'static,
155    Input: Send + 'static,
156{
157    type Output = C::Output;
158    type Error = C::Error;
159
160    async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
161        self.inner.conn.serve(input).await
162    }
163}
164
165impl<C, ID> Debug for MultiplexedConnection<C, ID>
166where
167    ID: Debug,
168{
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        f.debug_struct("MultiplexedConnection")
171            .field("id", &self.inner.id)
172            .field("active_streams", &self.inner.active.load(Ordering::Relaxed))
173            .finish()
174    }
175}
176
177/// Connection pool that multiplexes concurrent users over shared
178/// connections.
179pub struct MultiplexPool<C, ID> {
180    storage: Arc<Mutex<Vec<Arc<StoredConnection<C, ID>>>>>,
181    total_slots: Arc<Semaphore>,
182    idle_timeout: Option<Duration>,
183    max_concurrent_streams: usize,
184    selection: MuxSelection,
185    rr_cursor: Arc<AtomicUsize>,
186    notify: Arc<Notify>,
187    #[cfg(feature = "opentelemetry")]
188    metrics: Option<Arc<metrics::PoolMetrics>>,
189}
190
191// We need a manual impl, derive(Extension) adds a Debug bound on all generics otherwise
192
193impl<C: Send, ID> Extension for MultiplexPool<C, ID>
194where
195    C: Send + Sync + 'static,
196    ID: Send + Sync + Debug + 'static,
197{
198}
199impl<C, ID> NetExtension for MultiplexPool<C, ID>
200where
201    C: Send + Sync + 'static,
202    ID: Send + Sync + Debug + 'static,
203{
204}
205
206impl<C, ID> Debug for MultiplexPool<C, ID> {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        f.debug_struct("MultiplexPool")
209            .field("idle_timeout", &self.idle_timeout)
210            .field("max_concurrent_streams", &self.max_concurrent_streams)
211            .field("selection", &self.selection)
212            .finish()
213    }
214}
215
216impl<C, ID> Clone for MultiplexPool<C, ID> {
217    fn clone(&self) -> Self {
218        Self {
219            storage: self.storage.clone(),
220            total_slots: self.total_slots.clone(),
221            idle_timeout: self.idle_timeout,
222            max_concurrent_streams: self.max_concurrent_streams,
223            selection: self.selection,
224            rr_cursor: self.rr_cursor.clone(),
225            notify: self.notify.clone(),
226            #[cfg(feature = "opentelemetry")]
227            metrics: self.metrics.clone(),
228        }
229    }
230}
231
232impl<C, ID> MultiplexPool<C, ID> {
233    /// Create a new [`MultiplexPool`] from validated, non-zero limits.
234    #[must_use]
235    pub fn new(max_concurrent_streams: NonZeroUsize, max_total: NonZeroUsize) -> Self {
236        Self {
237            storage: Arc::new(Mutex::new(Vec::with_capacity(max_total.get()))),
238            total_slots: Arc::new(Semaphore::new(max_total.get())),
239            idle_timeout: None,
240            max_concurrent_streams: max_concurrent_streams.get(),
241            selection: MuxSelection::default(),
242            rr_cursor: Arc::new(AtomicUsize::new(0)),
243            notify: Arc::new(Notify::new()),
244            #[cfg(feature = "opentelemetry")]
245            metrics: None,
246        }
247    }
248
249    /// Create a new [`MultiplexPool`].
250    ///
251    /// - `max_concurrent_streams`: upper bound on the concurrent users a single
252    ///   connection serves. The actual per-connection concurrency is the minimum of
253    ///   this and the connection's [`MaxConcurrency`] extension ([`usize::MAX`] if unset), so
254    ///   use [`usize::MAX`] to defer entirely to what each connection advertises.
255    /// - `max_total`: max number of connections (across all ids).
256    pub fn try_new(max_concurrent_streams: usize, max_total: usize) -> Result<Self, BoxError> {
257        let (Some(max_concurrent_streams), Some(max_total)) = (
258            NonZeroUsize::new(max_concurrent_streams),
259            NonZeroUsize::new(max_total),
260        ) else {
261            return Err(BoxError::from_static_str(
262                "max_concurrent_streams and max_total must be greater than 0",
263            )
264            .context_field("max_concurrent_streams", max_concurrent_streams)
265            .context_field("max_total", max_total));
266        };
267        Ok(Self::new(max_concurrent_streams, max_total))
268    }
269
270    generate_set_and_with! {
271        /// Drop connections that have been idle (no active streams) for longer than
272        /// the given timeout. Only checked when a connection is requested.
273        pub fn idle_timeout(mut self, timeout: Option<Duration>) -> Self {
274            self.idle_timeout = timeout;
275            self
276        }
277    }
278
279    generate_set_and_with! {
280        /// Set the [`MuxSelection`] strategy used to pick among same-id connections.
281        pub fn selection(mut self, selection: MuxSelection) -> Self {
282            self.selection = selection;
283            self
284        }
285    }
286
287    #[cfg(feature = "opentelemetry")]
288    generate_set_and_with! {
289        #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))]
290        pub fn metrics(mut self, metrics: Option<Arc<metrics::PoolMetrics>>) -> Self {
291            self.metrics = metrics;
292            self
293        }
294    }
295}
296
297impl<C, ID> Pool<C, ID> for MultiplexPool<C, ID>
298where
299    C: Send + Sync + ExtensionsRef + 'static,
300    ID: ConnID,
301{
302    type Connection = MultiplexedConnection<C, ID>;
303    type CreatePermit = PoolSlot;
304
305    async fn get_conn(
306        &self,
307        id: &ID,
308    ) -> Result<ConnectionResult<Self::Connection, Self::CreatePermit>, BoxError> {
309        #[cfg(feature = "opentelemetry")]
310        let metrics = self
311            .metrics
312            .as_ref()
313            .map(|metrics| (metrics, metrics.attributes(id)));
314        #[cfg(feature = "opentelemetry")]
315        let start = Instant::now();
316
317        // On success returns the connection/permit, when want_caps = true
318        // and we find no connections for the given ID, return a FuturesUnordered
319        // set of which the futures resolve when connections for the given ID
320        // have capacity changes.
321        let attempt =
322            |want_cap_changes: bool| -> Result<ConnectionResult<_, _>, FuturesUnordered<_>> {
323                let mut storage = self.storage.lock();
324
325                // Drop idle connections past the idle timeout.
326                if let Some(idle_timeout) = self.idle_timeout {
327                    storage.retain(|conn| {
328                        let drop = conn.is_idle() && conn.last_idle.elapsed() >= idle_timeout;
329                        if drop {
330                            trace!(id = ?conn.id, "multiplex pool: dropping idle connection");
331                        }
332                        !drop
333                    });
334                }
335
336                // Drop broken connections (their in-flight streams keep them alive
337                // via the outstanding handles, but they are no longer handed out).
338                storage.retain(|conn| {
339                    let broken = conn
340                        .conn
341                        .extensions()
342                        .get_ref::<ConnectionHealthWatcher>()
343                        .is_some_and(|watcher| watcher.health() == ConnectionHealth::Broken);
344                    if broken {
345                        trace!(id = ?conn.id, "multiplex pool: dropping broken connection");
346                    }
347                    !broken
348                });
349
350                if let Some(conn) = select_and_admit(
351                    &storage,
352                    id,
353                    self.selection,
354                    &self.rr_cursor,
355                    self.max_concurrent_streams,
356                ) {
357                    trace!(?id, "multiplex pool: reusing connection");
358                    #[cfg(feature = "opentelemetry")]
359                    if let Some((metrics, attrs)) = &metrics {
360                        metrics.reused_connections.add(1, attrs);
361                        metrics.streams.add(1, attrs);
362                        metrics
363                            .concurrent_streams
364                            .record(conn.inner.active.load(Ordering::Relaxed) as f64, attrs);
365                        metrics
366                            .active_connection_delay_nanoseconds
367                            .record(start.elapsed().as_nanos() as f64, attrs);
368                    }
369                    return Ok(ConnectionResult::Connection(conn));
370                }
371
372                let saturation = storage.iter().any(|conn| &conn.id == id);
373
374                // Claim a fresh connection slot, evicting the least-recently-used idle
375                // connection (any id) if the pool is at its total capacity.
376                let pool_slot = if let Ok(permit) = self.total_slots.clone().try_acquire_owned() {
377                    Some(PoolSlot(permit))
378                } else {
379                    let lru_idle = storage
380                        .iter()
381                        .enumerate()
382                        .filter(|(_, conn)| conn.is_idle())
383                        .min_by_key(|(_, conn)| conn.last_idle.as_nanos())
384                        .map(|(pos, _)| pos);
385                    if let Some(pos) = lru_idle {
386                        storage.remove(pos);
387                        #[cfg(feature = "opentelemetry")]
388                        if let Some((metrics, attrs)) = &metrics {
389                            metrics.evicted_connections.add(1, attrs);
390                        }
391                        self.total_slots
392                            .clone()
393                            .try_acquire_owned()
394                            .ok()
395                            .map(PoolSlot)
396                    } else {
397                        None
398                    }
399                };
400
401                if let Some(pool_slot) = pool_slot {
402                    trace!(
403                        ?id,
404                        "multiplex pool: no connection with capacity, returning create permit"
405                    );
406                    #[cfg(feature = "opentelemetry")]
407                    if let Some((metrics, attrs)) = &metrics {
408                        if saturation {
409                            metrics.saturation_created_connections.add(1, attrs);
410                        }
411                        metrics
412                            .active_connection_delay_nanoseconds
413                            .record(start.elapsed().as_nanos() as f64, attrs);
414                    }
415                    #[cfg(not(feature = "opentelemetry"))]
416                    let _ = saturation;
417                    return Ok(ConnectionResult::CreatePermit(pool_slot));
418                }
419
420                let cap_changes = if want_cap_changes {
421                    storage
422                        .iter()
423                        .filter(|conn| &conn.id == id)
424                        .filter_map(|conn| conn.max_concurrency.clone())
425                        .map(|mc| async move { mc.watch().changed().await })
426                        .collect()
427                } else {
428                    FuturesUnordered::new()
429                };
430                Err(cap_changes)
431            };
432
433        loop {
434            // Fast path: try without registering as a waiter (no caps needed).
435            if let Ok(result) = attempt(false) {
436                return Ok(result);
437            }
438
439            // Saturated. Register as a waiter, and then re-check. This order is important
440            // to make sure we don't miss a notify while our check logic is running
441            let notified = self.notify.notified();
442            tokio::pin!(notified);
443            notified.as_mut().enable();
444            let mut cap_changes = match attempt(true) {
445                Ok(result) => return Ok(result),
446                Err(cap_changes) => cap_changes,
447            };
448
449            trace!(?id, "multiplex pool: saturated, waiting for capacity");
450            // Wake on a release/create notify, or on a same-id connection's
451            // capacity increase (its `MaxConcurrency` changing)..
452            tokio::select! {
453                _ = notified => {}
454                _ = cap_changes.next(), if !cap_changes.is_empty() => {}
455            }
456        }
457    }
458
459    async fn create(&self, id: ID, conn: C, pool_slot: PoolSlot) -> Self::Connection {
460        let conn = Arc::new(StoredConnection {
461            max_concurrency: conn.extensions().get_arc::<MaxConcurrency>(),
462            conn,
463            id,
464            active: AtomicUsize::new(1),
465            notify: self.notify.clone(),
466            last_idle: AtomicInstant::now(),
467            _pool_slot: pool_slot,
468        });
469
470        trace!(id = ?conn.id, "multiplex pool: adding new connection");
471        self.storage.lock().push(conn.clone());
472
473        // A freshly added connection has spare capacity beyond its establishing
474        // handout, so make sure to wake parked waiters.
475        self.notify.notify_waiters();
476
477        #[cfg(feature = "opentelemetry")]
478        if let Some(metrics) = self.metrics.as_ref() {
479            let attrs = metrics.attributes(&conn.id);
480            metrics.total_connections.add(1, &attrs);
481            metrics.created_connections.add(1, &attrs);
482            metrics.streams.add(1, &attrs);
483            metrics.concurrent_streams.record(1.0, &attrs);
484        }
485
486        MultiplexedConnection { inner: conn }
487    }
488}
489
490/// Select a same-id connection that still has capacity and admit a stream on it
491/// (see [`StoredConnection::try_create_multiplexed`]), returning a ready handout.
492fn select_and_admit<C, ID: PartialEq>(
493    storage: &[Arc<StoredConnection<C, ID>>],
494    id: &ID,
495    selection: MuxSelection,
496    rr_cursor: &AtomicUsize,
497    cap: usize,
498) -> Option<MultiplexedConnection<C, ID>> {
499    let same_id = |conn: &&Arc<StoredConnection<C, ID>>| &conn.id == id;
500    let has_capacity = |conn: &&Arc<StoredConnection<C, ID>>| {
501        same_id(conn) && conn.active.load(Ordering::Relaxed) < conn.effective_capacity(cap)
502    };
503    let create_conn = |conn: &Arc<StoredConnection<C, ID>>| conn.try_create_multiplexed(cap);
504
505    match selection {
506        // Admit on the first same-id connection that accepts a stream.
507        MuxSelection::FirstAvailable => storage.iter().filter(same_id).find_map(create_conn),
508        // Admit on the least-loaded (fewest active) same-id connection.
509        MuxSelection::LeastLoaded => storage
510            .iter()
511            .filter(has_capacity)
512            .min_by_key(|conn| conn.active.load(Ordering::Relaxed))
513            .and_then(create_conn),
514        MuxSelection::RoundRobin => {
515            let count = storage.iter().filter(has_capacity).count();
516            if count == 0 {
517                None
518            } else {
519                let idx = rr_cursor.fetch_add(1, Ordering::Relaxed) % count;
520                storage
521                    .iter()
522                    .filter(has_capacity)
523                    .nth(idx)
524                    .and_then(create_conn)
525            }
526        }
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::super::PooledConnector;
533    use super::*;
534    use crate::client::{
535        ConnectionErrorDomain, ConnectionErrorKind, ConnectorService, EstablishedClientConnection,
536    };
537    use rama_core::ServiceInput;
538    use std::convert::Infallible;
539
540    #[derive(Clone, Debug, PartialEq, Eq)]
541    struct TestId(u32);
542    impl ConnID for TestId {}
543
544    #[derive(Debug)]
545    struct Conn {
546        serial: usize,
547        extensions: Extensions,
548    }
549
550    impl ExtensionsRef for Conn {
551        fn extensions(&self) -> &Extensions {
552            &self.extensions
553        }
554    }
555
556    impl Service<()> for Conn {
557        type Output = usize;
558        type Error = Infallible;
559
560        async fn serve(&self, (): ()) -> Result<Self::Output, Self::Error> {
561            Ok(self.serial)
562        }
563    }
564
565    #[derive(Default)]
566    struct TestConnector {
567        created: AtomicUsize,
568        max_concurrency: Option<usize>,
569    }
570
571    impl<Input> Service<Input> for TestConnector
572    where
573        Input: Send + 'static,
574    {
575        type Output = EstablishedClientConnection<Conn, Input>;
576        type Error = Infallible;
577
578        async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
579            let serial = self.created.fetch_add(1, Ordering::Relaxed);
580            let conn = Conn {
581                serial,
582                extensions: Extensions::new(),
583            };
584            conn.extensions.insert(ConnectionHealthWatcher::default());
585            if let Some(mc) = self.max_concurrency {
586                conn.extensions.insert(MaxConcurrency::new(mc));
587            }
588            Ok(EstablishedClientConnection { input, conn })
589        }
590    }
591
592    /// Like [`TestConnector`] but takes `delay` to establish each connection,
593    /// so tests can park waiters while a connection is being created.
594    struct SlowConnector {
595        created: AtomicUsize,
596        delay: Duration,
597    }
598
599    impl<Input> Service<Input> for SlowConnector
600    where
601        Input: Send + 'static,
602    {
603        type Output = EstablishedClientConnection<Conn, Input>;
604        type Error = Infallible;
605
606        async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
607            tokio::time::sleep(self.delay).await;
608            let serial = self.created.fetch_add(1, Ordering::Relaxed);
609            let conn = Conn {
610                serial,
611                extensions: Extensions::new(),
612            };
613            conn.extensions.insert(ConnectionHealthWatcher::default());
614            Ok(EstablishedClientConnection { input, conn })
615        }
616    }
617
618    fn id_fn(input: &ServiceInput<u32>) -> Result<TestId, BoxError> {
619        Ok(TestId(input.input))
620    }
621
622    type MuxConnector = PooledConnector<
623        TestConnector,
624        MultiplexPool<Conn, TestId>,
625        fn(&ServiceInput<u32>) -> Result<TestId, BoxError>,
626    >;
627
628    fn connector_with(
629        pool: MultiplexPool<Conn, TestId>,
630        max_concurrency: Option<usize>,
631    ) -> MuxConnector {
632        let connector = TestConnector {
633            created: AtomicUsize::new(0),
634            max_concurrency,
635        };
636        PooledConnector::new(
637            connector,
638            pool,
639            id_fn as fn(&ServiceInput<u32>) -> Result<TestId, BoxError>,
640        )
641    }
642
643    fn connector(pool: MultiplexPool<Conn, TestId>) -> MuxConnector {
644        // No MaxConcurrency advertised means "no limit"
645        connector_with(pool, None)
646    }
647
648    async fn connect(
649        svc: &MuxConnector,
650        id: u32,
651    ) -> EstablishedClientConnection<MultiplexedConnection<Conn, TestId>, ServiceInput<u32>> {
652        svc.connect(ServiceInput::new(id)).await.unwrap()
653    }
654
655    fn created(svc: &MuxConnector) -> usize {
656        svc.inner.created.load(Ordering::Relaxed)
657    }
658
659    #[tokio::test]
660    async fn shares_one_connection() {
661        let pool = MultiplexPool::new(NonZeroUsize::new(4).unwrap(), NonZeroUsize::new(4).unwrap());
662        let svc = connector(pool);
663
664        let mut handles = Vec::new();
665        for _ in 0..4 {
666            handles.push(connect(&svc, 0).await);
667        }
668        assert_eq!(
669            created(&svc),
670            1,
671            "all 4 handouts should share one connection"
672        );
673        for h in &handles {
674            assert_eq!(h.conn.serve(()).await.unwrap(), 0);
675        }
676    }
677
678    #[tokio::test(start_paused = true)]
679    async fn maxconcurrency_increase_wakes_waiters() {
680        let pool = MultiplexPool::try_new(10, 1).unwrap();
681        let svc = Arc::new(connector_with(pool, Some(1)));
682
683        let c1 = svc.connect(ServiceInput::new(0)).await.unwrap();
684
685        let woke = Arc::new(std::sync::atomic::AtomicBool::new(false));
686        let waiter = {
687            let svc = svc.clone();
688            let woke = woke.clone();
689            tokio::spawn(async move {
690                let _h = svc.connect(ServiceInput::new(0)).await.unwrap();
691                woke.store(true, Ordering::Relaxed);
692            })
693        };
694
695        // The waiter parks: connection 0 is at capacity and the pool is full.
696        tokio::time::sleep(Duration::from_millis(50)).await;
697        assert!(!woke.load(Ordering::Relaxed), "waiter should be parked");
698
699        // Raise the connection's advertised capacity (as an h2 SETTINGS bump would):
700        // the parked waiter must wake and admit on the now-available stream slot.
701        c1.conn
702            .extensions()
703            .get_ref::<MaxConcurrency>()
704            .unwrap()
705            .set(2);
706
707        tokio::time::timeout(Duration::from_secs(1), waiter)
708            .await
709            .expect("a MaxConcurrency increase should wake the parked waiter")
710            .unwrap();
711        assert!(woke.load(Ordering::Relaxed));
712        // c1 is still held; the waiter admitted on the same connection, not a new one.
713        assert_eq!(svc.inner.created.load(Ordering::Relaxed), 1);
714    }
715
716    #[tokio::test]
717    async fn maxconcurrency_zero_admits_no_streams() {
718        let pool = MultiplexPool::try_new(4, 4).unwrap();
719        let svc = connector_with(pool, Some(0));
720
721        let c1 = connect(&svc, 0).await;
722        assert_eq!(created(&svc), 1);
723        drop(c1);
724
725        // Connection 0 advertises `MaxConcurrency(0)`: even while idle it must not
726        // admit a new stream (0 means "no streams", not clamp-to-1), so the pool
727        // creates a fresh connection instead of reusing it.
728        let _c2 = connect(&svc, 0).await;
729        assert_eq!(
730            created(&svc),
731            2,
732            "a connection advertising max_concurrency=0 must not admit new streams"
733        );
734    }
735
736    #[tokio::test(start_paused = true)]
737    async fn new_multiplexed_connection_wakes_waiters() {
738        let pool = MultiplexPool::try_new(2, 1).unwrap();
739        let svc = PooledConnector::new(
740            SlowConnector {
741                created: AtomicUsize::new(0),
742                delay: Duration::from_millis(100),
743            },
744            pool,
745            id_fn as fn(&ServiceInput<u32>) -> Result<TestId, BoxError>,
746        )
747        .with_wait_for_pool_timeout(Duration::from_millis(500));
748
749        let c1 = svc.connect(ServiceInput::new(1u32)).await.unwrap();
750
751        let waiter1 = svc.connect(ServiceInput::new(2u32));
752        let waiter2 = svc.connect(ServiceInput::new(2u32));
753
754        tokio::time::sleep(Duration::from_millis(20)).await;
755        drop(c1);
756
757        let (r1, r2) = tokio::join!(waiter1, waiter2);
758        assert!(r1.is_ok(), "first waiter should create a new connection");
759        assert!(
760            r2.is_ok(),
761            "second waiter should reuse the spare stream slot"
762        );
763    }
764
765    #[tokio::test]
766    async fn new_connection_when_saturated() {
767        let pool = MultiplexPool::try_new(2, 2).unwrap();
768        let svc = connector(pool);
769
770        let _c1 = connect(&svc, 0).await;
771        let _c2 = connect(&svc, 0).await;
772        assert_eq!(
773            created(&svc),
774            1,
775            "connection 0 should be reused while it has room"
776        );
777
778        let c3 = connect(&svc, 0).await;
779        assert_eq!(
780            created(&svc),
781            2,
782            "a 3rd concurrent handout needs a new connection"
783        );
784        assert_eq!(c3.conn.serve(()).await.unwrap(), 1);
785    }
786
787    #[tokio::test]
788    async fn extensions_propagate_at_establish() {
789        let pool = MultiplexPool::try_new(2, 2).unwrap();
790        let svc = connector(pool);
791
792        let c = connect(&svc, 0).await;
793
794        assert!(
795            c.conn
796                .extensions()
797                .get_ref::<ConnectionHealthWatcher>()
798                .is_some()
799        );
800    }
801
802    #[tokio::test]
803    async fn broken_removed_while_handles_survive() {
804        let pool = MultiplexPool::try_new(2, 2).unwrap();
805        let svc = connector(pool);
806
807        let c1 = connect(&svc, 0).await;
808        let c2 = connect(&svc, 0).await;
809        assert_eq!(created(&svc), 1);
810
811        // mark the shared connection broken
812        c1.conn
813            .extensions()
814            .get_ref::<ConnectionHealthWatcher>()
815            .unwrap()
816            .mark_broken();
817
818        // a fresh handout must not reuse the broken connection
819        let c3 = connect(&svc, 0).await;
820        assert_eq!(created(&svc), 2);
821        assert_eq!(c3.conn.serve(()).await.unwrap(), 1);
822
823        // the in-flight handles still work on the (removed but alive) connection
824        assert_eq!(c1.conn.serve(()).await.unwrap(), 0);
825        assert_eq!(c2.conn.serve(()).await.unwrap(), 0);
826
827        // once they drop, the slot frees and a new handout can be created again
828        drop(c1);
829        drop(c2);
830        drop(c3);
831        let _c4 = connect(&svc, 0).await;
832        // c4 reuses connection 1 (still in storage), no new connection
833        assert_eq!(created(&svc), 2);
834    }
835
836    #[tokio::test(start_paused = true)]
837    async fn idle_eviction() {
838        let pool = MultiplexPool::try_new(2, 5)
839            .unwrap()
840            .with_idle_timeout(Duration::from_micros(1));
841        let svc = connector(pool);
842
843        let c = connect(&svc, 0).await;
844        assert_eq!(created(&svc), 1);
845        drop(c);
846
847        tokio::time::sleep(Duration::from_millis(50)).await;
848
849        let _c = connect(&svc, 0).await;
850        assert_eq!(created(&svc), 2, "idle connection should have been evicted");
851    }
852
853    #[tokio::test]
854    async fn least_loaded_selection() {
855        let pool = MultiplexPool::try_new(3, 2)
856            .unwrap()
857            .with_selection(MuxSelection::LeastLoaded);
858        let svc = connector(pool);
859
860        let c1 = connect(&svc, 0).await;
861        let _c2 = connect(&svc, 0).await;
862        let _c3 = connect(&svc, 0).await;
863        let _c4 = connect(&svc, 0).await;
864        assert_eq!(created(&svc), 2);
865
866        drop(c1);
867
868        let c5 = connect(&svc, 0).await;
869        assert_eq!(
870            c5.conn.serve(()).await.unwrap(),
871            1,
872            "least-loaded should pick connection 1 (more free streams)"
873        );
874    }
875
876    #[tokio::test]
877    async fn first_available_selection() {
878        let pool = MultiplexPool::try_new(3, 2)
879            .unwrap()
880            .with_selection(MuxSelection::FirstAvailable);
881        let svc = connector(pool);
882
883        let c1 = connect(&svc, 0).await;
884        let _c2 = connect(&svc, 0).await;
885        let _c3 = connect(&svc, 0).await;
886        let _c4 = connect(&svc, 0).await;
887        assert_eq!(created(&svc), 2);
888
889        drop(c1);
890
891        let c5 = connect(&svc, 0).await;
892        assert_eq!(
893            c5.conn.serve(()).await.unwrap(),
894            0,
895            "first-available should pick connection 0 (first with a free slot)"
896        );
897    }
898
899    #[tokio::test]
900    async fn capacity_one_is_exclusive() {
901        let pool = MultiplexPool::try_new(1, 3).unwrap();
902        let svc = connector(pool);
903
904        let c1 = connect(&svc, 0).await;
905        let c2 = connect(&svc, 0).await;
906        let c3 = connect(&svc, 0).await;
907        assert_eq!(created(&svc), 3, "capacity 1 never shares a connection");
908        // each landed on a distinct connection
909        assert_eq!(c1.conn.serve(()).await.unwrap(), 0);
910        assert_eq!(c2.conn.serve(()).await.unwrap(), 1);
911        assert_eq!(c3.conn.serve(()).await.unwrap(), 2);
912    }
913
914    #[tokio::test(start_paused = true)]
915    async fn saturation_waits_and_times_out() {
916        let pool = MultiplexPool::try_new(1, 1).unwrap();
917        let svc = connector(pool).with_wait_for_pool_timeout(Duration::from_millis(50));
918
919        let c1 = connect(&svc, 0).await;
920        // connection full, no room to create -> get_conn waits, then times out
921        let error = svc
922            .connect(ServiceInput::new(0u32))
923            .await
924            .expect_err("saturated pool should time out");
925        assert_eq!(error.domain(), ConnectionErrorDomain::Local);
926        assert_eq!(error.kind(), ConnectionErrorKind::Timeout);
927
928        drop(c1);
929        // now a slot is free again
930        let _c2 = connect(&svc, 0).await;
931    }
932
933    #[tokio::test]
934    async fn capacity_from_extension() {
935        // pool cap 5, but each connection advertises only 2 -> effective 2
936        let pool = MultiplexPool::try_new(5, 5).unwrap();
937        let svc = connector_with(pool, Some(2));
938
939        let _c1 = connect(&svc, 0).await;
940        let _c2 = connect(&svc, 0).await;
941        assert_eq!(
942            created(&svc),
943            1,
944            "two streams share the connection (its advertised capacity)"
945        );
946
947        let _c3 = connect(&svc, 0).await;
948        assert_eq!(
949            created(&svc),
950            2,
951            "a 3rd stream exceeds the advertised capacity -> new connection"
952        );
953    }
954
955    #[tokio::test]
956    async fn capacity_is_read_live() {
957        // Connections start advertising 1, pool cap is high.
958        let pool = MultiplexPool::try_new(10, 5).unwrap();
959        let svc = connector_with(pool, Some(1));
960
961        let c1 = connect(&svc, 0).await; // conn A, now at its limit of 1
962        let _c2 = connect(&svc, 0).await; // A full -> conn B
963        assert_eq!(created(&svc), 2);
964
965        // Server raises A's SETTINGS_MAX_CONCURRENT_STREAMS to 3.
966        c1.conn
967            .extensions()
968            .get_ref::<MaxConcurrency>()
969            .unwrap()
970            .set(3);
971
972        // A now has spare capacity, so the next stream reuses A instead of
973        // opening a new connection — proving the limit is read live.
974        let _c3 = connect(&svc, 0).await;
975        assert_eq!(
976            created(&svc),
977            2,
978            "raising MaxConcurrency lets A take another stream (dynamic capacity)"
979        );
980    }
981
982    #[tokio::test]
983    async fn no_extension_uses_pool_cap() {
984        // Without a MaxConcurrency extension there is "no limit", so the pool's
985        // max_concurrent_streams governs: cap 2 -> 2 streams share one connection.
986        let pool = MultiplexPool::try_new(2, 8).unwrap();
987        let svc = connector_with(pool, None);
988
989        let _c1 = connect(&svc, 0).await;
990        let _c2 = connect(&svc, 0).await;
991        assert_eq!(
992            created(&svc),
993            1,
994            "two streams share one connection (pool cap 2)"
995        );
996
997        let _c3 = connect(&svc, 0).await;
998        assert_eq!(
999            created(&svc),
1000            2,
1001            "a 3rd stream exceeds the pool cap -> new connection"
1002        );
1003    }
1004
1005    #[tokio::test]
1006    async fn lru_eviction_when_full() {
1007        let pool = MultiplexPool::try_new(1, 2).unwrap();
1008        let svc = connector(pool);
1009
1010        // A (id 0) and B (id 1), both idle. Then touch A again so A becomes more
1011        // recently used than B -> B is the LRU, even though A is first in storage.
1012        drop(connect(&svc, 0).await);
1013        drop(connect(&svc, 1).await);
1014        tokio::time::sleep(Duration::from_millis(10)).await;
1015        drop(connect(&svc, 0).await); // reuse A; A.last_idle now newer than B's
1016        assert_eq!(created(&svc), 2);
1017
1018        // Pool is full (2 connections); a new id evicts the LRU idle connection (B).
1019        drop(connect(&svc, 2).await);
1020        assert_eq!(created(&svc), 3);
1021
1022        // A survived (more recently used) -> reused, no new connection. This also
1023        // proves we evicted the LRU (B), not the first-in-storage connection (A).
1024        drop(connect(&svc, 0).await);
1025        assert_eq!(
1026            created(&svc),
1027            3,
1028            "A survived: LRU evicted B, not first-in-storage A"
1029        );
1030
1031        // B was evicted -> a new connection is created for id 1.
1032        drop(connect(&svc, 1).await);
1033        assert_eq!(created(&svc), 4, "B (LRU) was evicted");
1034    }
1035
1036    #[test]
1037    fn virtual_conn_is_send_sync() {
1038        fn assert_send_sync<T: Send + Sync + 'static>() {}
1039        assert_send_sync::<MultiplexedConnection<Conn, TestId>>();
1040        fn assert_pool<P: Pool<Conn, TestId>>() {}
1041        assert_pool::<MultiplexPool<Conn, TestId>>();
1042    }
1043}