AWS SDK

AWS SDK

rev. 174400987dccd7e137fefa96b1143d21c7ddfb78

Files changed:

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/builder.rs

@@ -0,1 +0,716 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
//! Connection pool builder.
           7  +
//!
           8  +
//! Entry point: [`SharedPool::builder`](super::SharedPool::builder).
           9  +
          10  +
use std::sync::Arc;
          11  +
use std::time::Duration;
          12  +
          13  +
use aws_smithy_runtime_api::client::dns::{ResolveDns, SharedDnsResolver};
          14  +
use aws_smithy_runtime_api::shared::IntoShared;
          15  +
use hyper_util::client::legacy::connect::dns::Name as DnsName;
          16  +
use hyper_util::client::legacy::connect::HttpConnector as HyperHttpConnector;
          17  +
use hyper_util::client::proxy::matcher::Matcher as ProxyMatcher;
          18  +
          19  +
use super::connection::ConnectionEventListener;
          20  +
use super::partition::{CrossPartitionPolicy, Partition};
          21  +
use super::{BoxError, ConnectionPool, PoolConfig};
          22  +
use crate::client::dns::HyperUtilResolver;
          23  +
use crate::client::proxy::ProxyConfig;
          24  +
use crate::client::tls;
          25  +
use crate::client::{TlsProviderSelected, TlsUnset};
          26  +
use crate::tls::TlsContext;
          27  +
          28  +
/// Default idle-connection eviction timeout, applied when the caller does
          29  +
/// not configure one.
          30  +
const DEFAULT_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
          31  +
          32  +
/// Builder for a [`SharedPool`].
          33  +
///
          34  +
/// Configures pool-wide settings: TLS, DNS, connection limits, idle
          35  +
/// eviction, proxy, partition topology, and connection event listening.
          36  +
///
          37  +
/// Type-state ensures TLS is configured before [`build_https`] is
          38  +
/// callable; calling [`tls_provider`] transitions the builder into the
          39  +
/// state where [`build_https`] is available.
          40  +
///
          41  +
/// [`build_https`]: Builder::build_https
          42  +
/// [`tls_provider`]: Builder::tls_provider
          43  +
/// [`SharedPool`]: super::SharedPool
          44  +
#[derive(Clone)]
          45  +
pub struct Builder<Tls = TlsUnset> {
          46  +
    pool_idle_timeout: Option<Option<Duration>>,
          47  +
    tcp_nodelay: bool,
          48  +
    tcp_keepalive: Option<Option<Duration>>,
          49  +
    max_connections: Option<usize>,
          50  +
    max_connections_per_host: Option<usize>,
          51  +
    proxy_config: Option<ProxyConfig>,
          52  +
    connection_event_listener: Option<Arc<dyn ConnectionEventListener>>,
          53  +
    cross_partition_policy: CrossPartitionPolicy,
          54  +
    dns_resolver: Option<SharedDnsResolver>,
          55  +
    partitions: Vec<Partition>,
          56  +
    tls: Tls,
          57  +
}
          58  +
          59  +
impl<Tls: std::fmt::Debug> std::fmt::Debug for Builder<Tls> {
          60  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
          61  +
        f.debug_struct("Builder")
          62  +
            .field("pool_idle_timeout", &self.pool_idle_timeout)
          63  +
            .field("tcp_nodelay", &self.tcp_nodelay)
          64  +
            .field("tcp_keepalive", &self.tcp_keepalive)
          65  +
            .field("max_connections", &self.max_connections)
          66  +
            .field("max_connections_per_host", &self.max_connections_per_host)
          67  +
            .field("proxy_config", &self.proxy_config)
          68  +
            .field(
          69  +
                "connection_event_listener",
          70  +
                &self.connection_event_listener.as_ref().map(|_| ".."),
          71  +
            )
          72  +
            .field("cross_partition_policy", &self.cross_partition_policy)
          73  +
            .field("dns_resolver", &self.dns_resolver.as_ref().map(|_| ".."))
          74  +
            .field("partitions", &self.partitions.len())
          75  +
            .finish()
          76  +
    }
          77  +
}
          78  +
          79  +
impl Default for Builder<TlsUnset> {
          80  +
    fn default() -> Self {
          81  +
        Self {
          82  +
            pool_idle_timeout: None,
          83  +
            tcp_nodelay: true,
          84  +
            tcp_keepalive: None,
          85  +
            max_connections: None,
          86  +
            max_connections_per_host: None,
          87  +
            proxy_config: None,
          88  +
            connection_event_listener: None,
          89  +
            cross_partition_policy: CrossPartitionPolicy::default(),
          90  +
            dns_resolver: None,
          91  +
            partitions: Vec::new(),
          92  +
            tls: TlsUnset {},
          93  +
        }
          94  +
    }
          95  +
}
          96  +
          97  +
// Methods available in any TLS state.
          98  +
impl<Tls> Builder<Tls> {
          99  +
    /// Set the pool idle timeout.
         100  +
    ///
         101  +
    /// Connections idle longer than this duration are evicted from the
         102  +
    /// pool. Set below the server's idle timeout to avoid dispatching on a
         103  +
    /// connection the server has already closed.
         104  +
    ///
         105  +
    /// Unset, the pool uses a default of 60 seconds. Pass `Some(duration)`
         106  +
    /// to override, or `None` to disable idle eviction entirely.
         107  +
    pub fn pool_idle_timeout<D>(mut self, timeout: D) -> Self
         108  +
    where
         109  +
        D: Into<Option<Duration>>,
         110  +
    {
         111  +
        self.pool_idle_timeout = Some(timeout.into());
         112  +
        self
         113  +
    }
         114  +
         115  +
    /// This is the mutable version of [`pool_idle_timeout`](Self::pool_idle_timeout).
         116  +
    ///
         117  +
    /// The outer `None` selects the default; `Some(None)` disables idle
         118  +
    /// eviction; `Some(Some(d))` sets the timeout to `d`.
         119  +
    pub fn set_pool_idle_timeout(&mut self, timeout: Option<Option<Duration>>) -> &mut Self {
         120  +
        self.pool_idle_timeout = timeout;
         121  +
        self
         122  +
    }
         123  +
         124  +
    /// Set TCP_NODELAY on connections. Default: `true`.
         125  +
    pub fn tcp_nodelay(mut self, nodelay: bool) -> Self {
         126  +
        self.tcp_nodelay = nodelay;
         127  +
        self
         128  +
    }
         129  +
         130  +
    /// This is the mutable version of [`tcp_nodelay`](Self::tcp_nodelay).
         131  +
    pub fn set_tcp_nodelay(&mut self, nodelay: bool) -> &mut Self {
         132  +
        self.tcp_nodelay = nodelay;
         133  +
        self
         134  +
    }
         135  +
         136  +
    /// Set the TCP keepalive idle time.
         137  +
    ///
         138  +
    /// Enables `SO_KEEPALIVE` with the given idle time before the first
         139  +
    /// probe. Keepalive detects dead peers faster than idle eviction
         140  +
    /// alone, notably for long-lived H2 connections.
         141  +
    ///
         142  +
    /// Keepalive is disabled by default. Pass `Some(duration)` to enable
         143  +
    /// it with that idle time; `None` leaves it disabled.
         144  +
    pub fn tcp_keepalive<D>(mut self, time: D) -> Self
         145  +
    where
         146  +
        D: Into<Option<Duration>>,
         147  +
    {
         148  +
        self.tcp_keepalive = Some(time.into());
         149  +
        self
         150  +
    }
         151  +
         152  +
    /// This is the mutable version of [`tcp_keepalive`](Self::tcp_keepalive).
         153  +
    ///
         154  +
    /// The outer `None` selects the default; `Some(None)` disables
         155  +
    /// keepalive; `Some(Some(d))` sets the idle time to `d`.
         156  +
    pub fn set_tcp_keepalive(&mut self, time: Option<Option<Duration>>) -> &mut Self {
         157  +
        self.tcp_keepalive = time;
         158  +
        self
         159  +
    }
         160  +
         161  +
    /// Set the global maximum number of concurrent connections.
         162  +
    ///
         163  +
    /// Caps the total live connections in the pool across all hosts,
         164  +
    /// including idle (cached) connections. New connection attempts wait
         165  +
    /// when the pool is at capacity; existing connections must be evicted
         166  +
    /// or closed before another can be created.
         167  +
    ///
         168  +
    /// Should be at least [`max_connections_per_host`](Self::max_connections_per_host)
         169  +
    /// when both are set; the global limit applies on top of the per-host
         170  +
    /// limit.
         171  +
    pub fn max_connections(mut self, n: usize) -> Self {
         172  +
        self.max_connections = Some(n);
         173  +
        self
         174  +
    }
         175  +
         176  +
    /// This is the mutable version of [`max_connections`](Self::max_connections).
         177  +
    ///
         178  +
    /// `None` leaves the global limit unset (unbounded).
         179  +
    pub fn set_max_connections(&mut self, n: Option<usize>) -> &mut Self {
         180  +
        self.max_connections = n;
         181  +
        self
         182  +
    }
         183  +
         184  +
    /// Set the maximum number of concurrent connections per host.
         185  +
    ///
         186  +
    /// Each unique (scheme, authority) pair has an independent connection
         187  +
    /// budget. This limit is independent of `max_connections`; both can
         188  +
    /// be set and are enforced simultaneously.
         189  +
    pub fn max_connections_per_host(mut self, n: usize) -> Self {
         190  +
        self.max_connections_per_host = Some(n);
         191  +
        self
         192  +
    }
         193  +
         194  +
    /// This is the mutable version of
         195  +
    /// [`max_connections_per_host`](Self::max_connections_per_host).
         196  +
    ///
         197  +
    /// `None` leaves the per-host limit unset (unbounded).
         198  +
    pub fn set_max_connections_per_host(&mut self, n: Option<usize>) -> &mut Self {
         199  +
        self.max_connections_per_host = n;
         200  +
        self
         201  +
    }
         202  +
         203  +
    /// Route connections through an HTTP/HTTPS/SOCKS proxy.
         204  +
    ///
         205  +
    /// Per-host proxy resolution is stable for the lifetime of the
         206  +
    /// pool: all connections to a given authority follow the same
         207  +
    /// proxy decision. HTTPS through an HTTP proxy uses `CONNECT`
         208  +
    /// tunneling.
         209  +
    ///
         210  +
    /// Pass [`ProxyConfig::from_env`] to read configuration from the
         211  +
    /// `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables.
         212  +
    pub fn proxy_config(mut self, config: ProxyConfig) -> Self {
         213  +
        self.proxy_config = Some(config);
         214  +
        self
         215  +
    }
         216  +
         217  +
    /// This is the mutable version of [`proxy_config`](Self::proxy_config).
         218  +
    pub fn set_proxy_config(&mut self, config: Option<ProxyConfig>) -> &mut Self {
         219  +
        self.proxy_config = config;
         220  +
        self
         221  +
    }
         222  +
         223  +
    /// Set a listener for connection lifecycle events (created, reused, closed, failed).
         224  +
    pub fn connection_event_listener(mut self, listener: Arc<dyn ConnectionEventListener>) -> Self {
         225  +
        self.connection_event_listener = Some(listener);
         226  +
        self
         227  +
    }
         228  +
         229  +
    /// This is the mutable version of [`connection_event_listener`](Self::connection_event_listener).
         230  +
    pub fn set_connection_event_listener(
         231  +
        &mut self,
         232  +
        listener: Option<Arc<dyn ConnectionEventListener>>,
         233  +
    ) -> &mut Self {
         234  +
        self.connection_event_listener = listener;
         235  +
        self
         236  +
    }
         237  +
         238  +
    /// Set the policy that governs checkout when the local partition has
         239  +
    /// no idle connection and the pool is at capacity.
         240  +
    ///
         241  +
    /// Defaults to [`CrossPartitionPolicy::Never`]. Has no observable
         242  +
    /// effect with a single partition or when the pool stays under
         243  +
    /// capacity.
         244  +
    pub fn cross_partition_policy(mut self, policy: CrossPartitionPolicy) -> Self {
         245  +
        self.cross_partition_policy = policy;
         246  +
        self
         247  +
    }
         248  +
         249  +
    /// This is the mutable version of [`cross_partition_policy`](Self::cross_partition_policy).
         250  +
    pub fn set_cross_partition_policy(&mut self, policy: CrossPartitionPolicy) -> &mut Self {
         251  +
        self.cross_partition_policy = policy;
         252  +
        self
         253  +
    }
         254  +
         255  +
    /// Set a custom DNS resolver.
         256  +
    ///
         257  +
    /// Connections established by this pool resolve hostnames through the
         258  +
    /// given resolver. Defaults to the system resolver when unset.
         259  +
    pub fn dns_resolver(mut self, resolver: impl ResolveDns + 'static) -> Self {
         260  +
        self.dns_resolver = Some(resolver.into_shared());
         261  +
        self
         262  +
    }
         263  +
         264  +
    /// This is the mutable version of [`dns_resolver`](Self::dns_resolver).
         265  +
    pub fn set_dns_resolver(&mut self, resolver: Option<SharedDnsResolver>) -> &mut Self {
         266  +
        self.dns_resolver = resolver;
         267  +
        self
         268  +
    }
         269  +
         270  +
    /// Declare the pool's partition topology. Each [`Partition`] carries a
         271  +
    /// driver-spawner runtime and optional NIC binding. When omitted, the
         272  +
    /// pool uses a single anonymous partition on the current tokio runtime.
         273  +
    pub fn partitions(mut self, partitions: impl IntoIterator<Item = Partition>) -> Self {
         274  +
        self.partitions = partitions.into_iter().collect();
         275  +
        self
         276  +
    }
         277  +
         278  +
    /// This is the mutable version of [`partitions`](Self::partitions).
         279  +
    ///
         280  +
    /// `None` clears any declared topology; the pool then uses a single
         281  +
    /// anonymous partition on the current tokio runtime.
         282  +
    pub fn set_partitions(&mut self, partitions: Option<Vec<Partition>>) -> &mut Self {
         283  +
        self.partitions = partitions.unwrap_or_default();
         284  +
        self
         285  +
    }
         286  +
}
         287  +
         288  +
impl Builder<TlsUnset> {
         289  +
    /// Set the TLS implementation.
         290  +
    pub fn tls_provider(self, provider: tls::Provider) -> Builder<TlsProviderSelected> {
         291  +
        Builder {
         292  +
            pool_idle_timeout: self.pool_idle_timeout,
         293  +
            tcp_nodelay: self.tcp_nodelay,
         294  +
            tcp_keepalive: self.tcp_keepalive,
         295  +
            max_connections: self.max_connections,
         296  +
            max_connections_per_host: self.max_connections_per_host,
         297  +
            proxy_config: self.proxy_config,
         298  +
            connection_event_listener: self.connection_event_listener,
         299  +
            cross_partition_policy: self.cross_partition_policy,
         300  +
            dns_resolver: self.dns_resolver,
         301  +
            partitions: self.partitions,
         302  +
            tls: TlsProviderSelected {
         303  +
                provider,
         304  +
                context: TlsContext::default(),
         305  +
            },
         306  +
        }
         307  +
    }
         308  +
         309  +
    /// Build an HTTP client without TLS.
         310  +
    #[doc(hidden)]
         311  +
    pub fn build_http(mut self) -> super::SharedPool {
         312  +
        let dns_resolver = self.dns_resolver.take();
         313  +
        let config = PoolConfig {
         314  +
            max_connections: self.max_connections,
         315  +
            max_connections_per_host: self.max_connections_per_host,
         316  +
            pool_idle_timeout: resolve_pool_idle_timeout(self.pool_idle_timeout),
         317  +
            connection_event_listener: self.connection_event_listener.clone(),
         318  +
        };
         319  +
        let keepalive = resolve_tcp_keepalive(self.tcp_keepalive);
         320  +
        let proxy_matcher = proxy_matcher_from(&self.proxy_config);
         321  +
        let partitions = std::mem::take(&mut self.partitions);
         322  +
        let policy = self.cross_partition_policy;
         323  +
        let pool = match dns_resolver {
         324  +
            Some(resolver) => {
         325  +
                let mut tcp = HyperHttpConnector::new_with_resolver(HyperUtilResolver { resolver });
         326  +
                tcp.set_nodelay(self.tcp_nodelay);
         327  +
                tcp.set_keepalive(keepalive);
         328  +
                build_http_pool_with_proxy(tcp, &self.proxy_config, config, partitions, policy)
         329  +
            }
         330  +
            None => {
         331  +
                let mut tcp = HyperHttpConnector::new();
         332  +
                tcp.set_nodelay(self.tcp_nodelay);
         333  +
                tcp.set_keepalive(keepalive);
         334  +
                build_http_pool_with_proxy(tcp, &self.proxy_config, config, partitions, policy)
         335  +
            }
         336  +
        };
         337  +
        super::SharedPool {
         338  +
            inner: Arc::new(super::SharedPoolInner {
         339  +
                pool: Arc::new(pool),
         340  +
                proxy_matcher,
         341  +
            }),
         342  +
        }
         343  +
    }
         344  +
         345  +
    /// Build an HTTP client from a raw TCP-level connector.
         346  +
    ///
         347  +
    /// The connector must be a `tower::Service<Uri>` producing an IO type
         348  +
    /// that implements hyper's `Read`, `Write`, and `Connection` traits.
         349  +
    /// The pool's Negotiate layer uses `Connection::connected().is_negotiated_h2()`
         350  +
    /// to route connections to the H2 path.
         351  +
    ///
         352  +
    /// NIC binding is not applied to custom TCP connectors — the custom
         353  +
    /// connector owns its own socket configuration.
         354  +
    #[cfg(all(feature = "test-util", aws_sdk_unstable))]
         355  +
    #[doc(hidden)]
         356  +
    pub fn build_http_with_tcp_connector<C, IO>(self, connector: C) -> super::SharedPool
         357  +
    where
         358  +
        C: tower::Service<http_1x::Uri, Response = IO> + Clone + Send + Sync + 'static,
         359  +
        C::Error: Into<BoxError> + 'static,
         360  +
        C::Future: Unpin + Send + 'static,
         361  +
        IO: hyper::rt::Read
         362  +
            + hyper::rt::Write
         363  +
            + hyper_util::client::legacy::connect::Connection
         364  +
            + Unpin
         365  +
            + Send
         366  +
            + 'static,
         367  +
    {
         368  +
        let config = super::PoolConfig {
         369  +
            max_connections: self.max_connections,
         370  +
            max_connections_per_host: self.max_connections_per_host,
         371  +
            pool_idle_timeout: resolve_pool_idle_timeout(self.pool_idle_timeout),
         372  +
            connection_event_listener: self.connection_event_listener.clone(),
         373  +
        };
         374  +
        let policy = self.cross_partition_policy;
         375  +
        let connector_factory = move |_partition: &Partition| connector.clone();
         376  +
        let pool = super::build_pool(connector_factory, config, self.partitions, policy);
         377  +
        super::SharedPool {
         378  +
            inner: Arc::new(super::SharedPoolInner {
         379  +
                pool: Arc::new(pool),
         380  +
                proxy_matcher: None,
         381  +
            }),
         382  +
        }
         383  +
    }
         384  +
}
         385  +
         386  +
/// Build a no-TLS pool that honors `proxy_config`. Wraps the TCP connector
         387  +
/// with an HTTP proxy connector when configured. Emits a warning if an
         388  +
/// HTTPS proxy is set without a TLS provider; connections to such a
         389  +
/// proxy will fail at handshake time.
         390  +
fn build_http_pool_with_proxy<R>(
         391  +
    tcp: HyperHttpConnector<R>,
         392  +
    proxy_config: &Option<ProxyConfig>,
         393  +
    config: PoolConfig,
         394  +
    partitions: Vec<Partition>,
         395  +
    policy: CrossPartitionPolicy,
         396  +
) -> ConnectionPool
         397  +
where
         398  +
    R: Clone + Send + Sync + 'static,
         399  +
    R: tower::Service<DnsName>,
         400  +
    R::Response: Iterator<Item = std::net::SocketAddr>,
         401  +
    R::Future: Send,
         402  +
    R::Error: Into<BoxError>,
         403  +
{
         404  +
    let proxy_config = proxy_config.clone().unwrap_or_else(ProxyConfig::disabled);
         405  +
         406  +
    if proxy_config.requires_tls() {
         407  +
        tracing::warn!(
         408  +
            "HTTPS proxy configured but no TLS provider set. \
         409  +
             Connections to HTTPS proxy servers will fail. \
         410  +
             Consider configuring a TLS provider to enable TLS support."
         411  +
        );
         412  +
    }
         413  +
         414  +
    if proxy_config.is_disabled() {
         415  +
        let connector_factory =
         416  +
            move |partition: &Partition| bind_interface(&tcp, partition.nic.as_deref());
         417  +
        super::build_pool(connector_factory, config, partitions, policy)
         418  +
    } else {
         419  +
        let connector_factory = move |partition: &Partition| {
         420  +
            crate::client::connect::HttpProxyConnector::new(
         421  +
                bind_interface(&tcp, partition.nic.as_deref()),
         422  +
                proxy_config.clone(),
         423  +
            )
         424  +
        };
         425  +
        super::build_pool(connector_factory, config, partitions, policy)
         426  +
    }
         427  +
}
         428  +
         429  +
impl Builder<TlsProviderSelected> {
         430  +
    /// Set the TLS context (custom trust store, etc.).
         431  +
    pub fn tls_context(mut self, context: TlsContext) -> Self {
         432  +
        self.tls.context = context;
         433  +
        self
         434  +
    }
         435  +
         436  +
    /// This is the mutable version of [`tls_context`](Self::tls_context).
         437  +
    pub fn set_tls_context(&mut self, context: TlsContext) -> &mut Self {
         438  +
        self.tls.context = context;
         439  +
        self
         440  +
    }
         441  +
         442  +
    /// Build an HTTPS client with the selected TLS provider.
         443  +
    pub fn build_https(mut self) -> super::SharedPool {
         444  +
        let dns_resolver = self.dns_resolver.take();
         445  +
        let keepalive = resolve_tcp_keepalive(self.tcp_keepalive);
         446  +
        match dns_resolver {
         447  +
            Some(resolver) => {
         448  +
                let mut tcp = HyperHttpConnector::new_with_resolver(HyperUtilResolver { resolver });
         449  +
                tcp.set_nodelay(self.tcp_nodelay);
         450  +
                tcp.set_keepalive(keepalive);
         451  +
                tcp.enforce_http(false);
         452  +
                self.build_from_tcp(tcp)
         453  +
            }
         454  +
            None => {
         455  +
                let mut tcp = HyperHttpConnector::new();
         456  +
                tcp.set_nodelay(self.tcp_nodelay);
         457  +
                tcp.set_keepalive(keepalive);
         458  +
                tcp.enforce_http(false);
         459  +
                self.build_from_tcp(tcp)
         460  +
            }
         461  +
        }
         462  +
    }
         463  +
         464  +
    fn build_from_tcp<R>(self, tcp: HyperHttpConnector<R>) -> super::SharedPool
         465  +
    where
         466  +
        R: Clone + Send + Sync + 'static,
         467  +
        R: tower::Service<DnsName>,
         468  +
        R::Response: Iterator<Item = std::net::SocketAddr>,
         469  +
        R::Future: Send,
         470  +
        R::Error: Into<BoxError>,
         471  +
    {
         472  +
        let config = PoolConfig {
         473  +
            max_connections: self.max_connections,
         474  +
            max_connections_per_host: self.max_connections_per_host,
         475  +
            pool_idle_timeout: resolve_pool_idle_timeout(self.pool_idle_timeout),
         476  +
            connection_event_listener: self.connection_event_listener.clone(),
         477  +
        };
         478  +
         479  +
        let proxy_config = self
         480  +
            .proxy_config
         481  +
            .clone()
         482  +
            .unwrap_or_else(ProxyConfig::disabled);
         483  +
        let proxy_matcher = proxy_matcher_from(&self.proxy_config);
         484  +
        let partitions = self.partitions;
         485  +
        let policy = self.cross_partition_policy;
         486  +
         487  +
        match &self.tls.provider {
         488  +
            #[cfg(any(
         489  +
                feature = "rustls-aws-lc",
         490  +
                feature = "rustls-aws-lc-fips",
         491  +
                feature = "rustls-ring"
         492  +
            ))]
         493  +
            tls::Provider::Rustls(crypto_mode) => {
         494  +
                let crypto_mode = crypto_mode.clone();
         495  +
                let tls_context = self.tls.context.clone();
         496  +
                let connector_factory = move |partition: &Partition| {
         497  +
                    tls::rustls_provider::build_connector::wrap_connector(
         498  +
                        bind_interface(&tcp, partition.nic.as_deref()),
         499  +
                        crypto_mode.clone(),
         500  +
                        &tls_context,
         501  +
                        proxy_config.clone(),
         502  +
                    )
         503  +
                };
         504  +
                let pool = super::build_pool(connector_factory, config, partitions, policy);
         505  +
                super::SharedPool {
         506  +
                    inner: Arc::new(super::SharedPoolInner {
         507  +
                        pool: Arc::new(pool),
         508  +
                        proxy_matcher,
         509  +
                    }),
         510  +
                }
         511  +
            }
         512  +
            #[cfg(feature = "s2n-tls")]
         513  +
            tls::Provider::S2nTls => {
         514  +
                let tls_context = self.tls.context.clone();
         515  +
                let connector_factory = move |partition: &Partition| {
         516  +
                    tls::s2n_tls_provider::build_connector::wrap_connector(
         517  +
                        bind_interface(&tcp, partition.nic.as_deref()),
         518  +
                        &tls_context,
         519  +
                        proxy_config.clone(),
         520  +
                    )
         521  +
                };
         522  +
                let pool = super::build_pool(connector_factory, config, partitions, policy);
         523  +
                super::SharedPool {
         524  +
                    inner: Arc::new(super::SharedPoolInner {
         525  +
                        pool: Arc::new(pool),
         526  +
                        proxy_matcher,
         527  +
                    }),
         528  +
                }
         529  +
            }
         530  +
            // Provider is #[non_exhaustive]; this arm is unreachable when any
         531  +
            // TLS feature is enabled (which is required to construct a Provider).
         532  +
            #[allow(unreachable_patterns)]
         533  +
            _ => unreachable!("a TLS feature must be enabled to use build_https()"),
         534  +
        }
         535  +
    }
         536  +
}
         537  +
         538  +
/// Resolve the configured pool idle timeout. Outer `None` applies the
         539  +
/// default; `Some(None)` disables eviction; `Some(Some(d))` uses `d`.
         540  +
fn resolve_pool_idle_timeout(configured: Option<Option<Duration>>) -> Option<Duration> {
         541  +
    match configured {
         542  +
        None => Some(DEFAULT_POOL_IDLE_TIMEOUT),
         543  +
        Some(inner) => inner,
         544  +
    }
         545  +
}
         546  +
         547  +
/// Resolve the configured TCP keepalive idle time. Keepalive is disabled
         548  +
/// by default, so the outer `None` resolves to off. `Some(None)` is also
         549  +
/// off; `Some(Some(d))` enables keepalive with idle time `d`.
         550  +
fn resolve_tcp_keepalive(configured: Option<Option<Duration>>) -> Option<Duration> {
         551  +
    configured.flatten()
         552  +
}
         553  +
         554  +
/// Bind a clone of the base TCP connector to a network interface.
         555  +
///
         556  +
/// The single place per-partition NIC binding happens: each partition's
         557  +
/// connector factory clones the shared base connector and routes it
         558  +
/// through here with that partition's `nic`. `None` (the default,
         559  +
/// no-interface case) returns an unbound clone. `set_interface` is only
         560  +
/// available on Linux-like targets; elsewhere the `nic` is accepted and
         561  +
/// ignored, matching the v1 client.
         562  +
fn bind_interface<R>(base: &HyperHttpConnector<R>, nic: Option<&str>) -> HyperHttpConnector<R>
         563  +
where
         564  +
    R: Clone,
         565  +
{
         566  +
    #[allow(unused_mut)]
         567  +
    let mut tcp = base.clone();
         568  +
    #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
         569  +
    if let Some(interface) = nic {
         570  +
        tcp.set_interface(interface);
         571  +
    }
         572  +
    let _ = nic;
         573  +
    tcp
         574  +
}
         575  +
         576  +
/// Build the proxy URL matcher from a `ProxyConfig`, returning `None` when
         577  +
/// no proxy is configured. Wrapped in `Arc` for shared ownership: the pool
         578  +
/// stores it once and each `PooledConnector` clones the handle.
         579  +
pub(super) fn proxy_matcher_from(proxy_config: &Option<ProxyConfig>) -> Option<Arc<ProxyMatcher>> {
         580  +
    proxy_config
         581  +
        .as_ref()
         582  +
        .map(|c| Arc::new(c.clone().into_hyper_util_matcher()))
         583  +
}
         584  +
         585  +
/// Returns the port if it is not the default for the scheme.
         586  +
pub(super) fn get_non_default_port(uri: &http_1x::Uri) -> Option<http_1x::uri::Port<&str>> {
         587  +
    match (uri.port().map(|p| p.as_u16()), uri.scheme()) {
         588  +
        (Some(443), Some(s)) if *s == http_1x::uri::Scheme::HTTPS => None,
         589  +
        (Some(80), Some(s)) if *s == http_1x::uri::Scheme::HTTP => None,
         590  +
        _ => uri.port(),
         591  +
    }
         592  +
}
         593  +
         594  +
#[cfg(test)]
         595  +
mod tests {
         596  +
    use super::*;
         597  +
    use crate::client::pool::PartitionId;
         598  +
         599  +
    #[test]
         600  +
    fn builder_defaults() {
         601  +
        let b = Builder::default();
         602  +
        // Outer None = "not configured"; the default is applied at build time.
         603  +
        assert_eq!(b.pool_idle_timeout, None);
         604  +
        assert_eq!(b.tcp_keepalive, None);
         605  +
        assert!(b.tcp_nodelay, "tcp_nodelay defaults to true");
         606  +
        assert_eq!(b.max_connections, None);
         607  +
        assert_eq!(b.max_connections_per_host, None);
         608  +
        assert!(b.proxy_config.is_none());
         609  +
        assert!(b.connection_event_listener.is_none());
         610  +
        assert_eq!(b.cross_partition_policy, CrossPartitionPolicy::Never);
         611  +
        assert!(b.dns_resolver.is_none());
         612  +
        assert!(b.partitions.is_empty());
         613  +
    }
         614  +
         615  +
    #[test]
         616  +
    fn idle_timeout_resolution() {
         617  +
        // Unconfigured → default applied.
         618  +
        assert_eq!(
         619  +
            resolve_pool_idle_timeout(None),
         620  +
            Some(DEFAULT_POOL_IDLE_TIMEOUT)
         621  +
        );
         622  +
        // Some(None) → explicitly disabled.
         623  +
        assert_eq!(resolve_pool_idle_timeout(Some(None)), None);
         624  +
        // Some(Some(d)) → overridden.
         625  +
        let d = Duration::from_secs(5);
         626  +
        assert_eq!(resolve_pool_idle_timeout(Some(Some(d))), Some(d));
         627  +
    }
         628  +
         629  +
    #[test]
         630  +
    fn keepalive_resolution() {
         631  +
        // Unset → off (keepalive is disabled by default).
         632  +
        assert_eq!(resolve_tcp_keepalive(None), None);
         633  +
        assert_eq!(resolve_tcp_keepalive(Some(None)), None);
         634  +
        let d = Duration::from_secs(45);
         635  +
        assert_eq!(resolve_tcp_keepalive(Some(Some(d))), Some(d));
         636  +
    }
         637  +
         638  +
    #[test]
         639  +
    fn idle_timeout_setters_set_three_states() {
         640  +
        // chaining setter: Duration → Some(Some(d))
         641  +
        let b = Builder::default().pool_idle_timeout(Duration::from_secs(7));
         642  +
        assert_eq!(b.pool_idle_timeout, Some(Some(Duration::from_secs(7))));
         643  +
        // chaining setter: None → Some(None) (disable)
         644  +
        let b = Builder::default().pool_idle_timeout(None);
         645  +
        assert_eq!(b.pool_idle_timeout, Some(None));
         646  +
        // mutable setter passes through verbatim
         647  +
        let mut b = Builder::default();
         648  +
        b.set_pool_idle_timeout(Some(None));
         649  +
        assert_eq!(b.pool_idle_timeout, Some(None));
         650  +
    }
         651  +
         652  +
    #[test]
         653  +
    fn keepalive_setters_set_three_states() {
         654  +
        let b = Builder::default().tcp_keepalive(Duration::from_secs(15));
         655  +
        assert_eq!(b.tcp_keepalive, Some(Some(Duration::from_secs(15))));
         656  +
        let b = Builder::default().tcp_keepalive(None);
         657  +
        assert_eq!(b.tcp_keepalive, Some(None));
         658  +
        let mut b = Builder::default();
         659  +
        b.set_tcp_keepalive(Some(None));
         660  +
        assert_eq!(b.tcp_keepalive, Some(None));
         661  +
    }
         662  +
         663  +
    /// `tls_provider` transitions the type-state while preserving every
         664  +
    /// configured field. Guards the hand-written field-by-field move in
         665  +
    /// `tls_provider` against a dropped field on a future edit.
         666  +
    #[test]
         667  +
    fn tls_provider_preserves_all_config() {
         668  +
        let b = Builder::default()
         669  +
            .pool_idle_timeout(Duration::from_secs(30))
         670  +
            .tcp_nodelay(false)
         671  +
            .tcp_keepalive(Duration::from_secs(45))
         672  +
            .max_connections(100)
         673  +
            .max_connections_per_host(10)
         674  +
            .cross_partition_policy(CrossPartitionPolicy::PreferLocal)
         675  +
            .partitions([Partition::new(
         676  +
                PartitionId::from_index(0),
         677  +
                crate::client::pool::TokioDriverSpawner::from_handle(
         678  +
                    // a handle is only needed to construct the spawner; no
         679  +
                    // runtime work happens here.
         680  +
                    tokio::runtime::Builder::new_current_thread()
         681  +
                        .build()
         682  +
                        .unwrap()
         683  +
                        .handle()
         684  +
                        .clone(),
         685  +
                ),
         686  +
            )]);
         687  +
         688  +
        let provider = tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc);
         689  +
        let b = b.tls_provider(provider);
         690  +
         691  +
        assert_eq!(b.pool_idle_timeout, Some(Some(Duration::from_secs(30))));
         692  +
        assert!(!b.tcp_nodelay);
         693  +
        assert_eq!(b.tcp_keepalive, Some(Some(Duration::from_secs(45))));
         694  +
        assert_eq!(b.max_connections, Some(100));
         695  +
        assert_eq!(b.max_connections_per_host, Some(10));
         696  +
        assert_eq!(b.cross_partition_policy, CrossPartitionPolicy::PreferLocal);
         697  +
        assert_eq!(b.partitions.len(), 1);
         698  +
    }
         699  +
         700  +
    #[test]
         701  +
    fn non_default_port_elided_per_scheme() {
         702  +
        let cases = [
         703  +
            ("https://example.com/", None), // 443 elided
         704  +
            ("http://example.com/", None),  // 80 elided
         705  +
            ("https://example.com:8443/", Some(8443)),
         706  +
            ("http://example.com:8080/", Some(8080)),
         707  +
            ("https://example.com:80/", Some(80)), // 80 is non-default for https
         708  +
            ("http://example.com:443/", Some(443)), // 443 is non-default for http
         709  +
        ];
         710  +
        for (uri, expected) in cases {
         711  +
            let uri: http_1x::Uri = uri.parse().unwrap();
         712  +
            let port = get_non_default_port(&uri).map(|p| p.as_u16());
         713  +
            assert_eq!(port, expected, "uri = {uri}");
         714  +
        }
         715  +
    }
         716  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/client.rs

@@ -0,1 +0,231 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
//! Per-partition client handle.
           7  +
           8  +
use std::borrow::Cow;
           9  +
use std::sync::Arc;
          10  +
use std::time::Duration;
          11  +
          12  +
use aws_smithy_async::rt::sleep::SharedAsyncSleep;
          13  +
use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
          14  +
use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
          15  +
use aws_smithy_runtime_api::client::http::{
          16  +
    HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
          17  +
};
          18  +
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
          19  +
use aws_smithy_runtime_api::client::result::ConnectorError;
          20  +
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
          21  +
use hyper_util::client::proxy::matcher::Matcher as ProxyMatcher;
          22  +
          23  +
use super::partition::{PartitionId, PartitionState};
          24  +
use super::{ConnectionPool, SharedPool};
          25  +
use crate::client::downcast_error;
          26  +
use crate::client::proxy::add_proxy_auth_header;
          27  +
          28  +
/// Per-partition view of a [`SharedPool`].
          29  +
///
          30  +
/// Implements [`HttpClient`] by routing requests through the shared
          31  +
/// connection pool. Multiple `Client` instances can reference the same
          32  +
/// pool, each targeting a distinct declared partition.
          33  +
///
          34  +
/// Construct via [`Client::new`] (default partition) or
          35  +
/// [`Client::from_partition`] for a specific declared partition.
          36  +
///
          37  +
/// Cloning is cheap: all fields are `Arc`-backed.
          38  +
#[derive(Clone)]
          39  +
pub struct Client {
          40  +
    pool: SharedPool,
          41  +
    partition: Arc<PartitionState>,
          42  +
}
          43  +
          44  +
impl std::fmt::Debug for Client {
          45  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
          46  +
        f.debug_struct("Client")
          47  +
            .field("partition_id", &self.partition.id)
          48  +
            .field("nic", &self.partition.nic)
          49  +
            .finish_non_exhaustive()
          50  +
    }
          51  +
}
          52  +
          53  +
impl Client {
          54  +
    /// Construct a `Client` targeting the pool's default partition (the
          55  +
    /// first declared, or the anonymous partition when none were declared).
          56  +
    pub fn new(pool: &SharedPool) -> Self {
          57  +
        let partition = pool.inner.pool.registry().default_partition();
          58  +
        Self {
          59  +
            pool: pool.clone(),
          60  +
            partition,
          61  +
        }
          62  +
    }
          63  +
          64  +
    /// Construct a `Client` targeting a specific declared partition.
          65  +
    /// Panics if `id` was not declared on the pool builder (programming
          66  +
    /// error: the caller declared the topology).
          67  +
    pub fn from_partition(pool: &SharedPool, id: PartitionId) -> Self {
          68  +
        let partition = pool.inner.pool.registry().partition(id);
          69  +
        Self {
          70  +
            pool: pool.clone(),
          71  +
            partition,
          72  +
        }
          73  +
    }
          74  +
          75  +
    /// The partition id this client targets.
          76  +
    #[cfg(test)]
          77  +
    fn partition_id(&self) -> PartitionId {
          78  +
        self.partition.id
          79  +
    }
          80  +
}
          81  +
          82  +
impl HttpClient for Client {
          83  +
    fn http_connector(
          84  +
        &self,
          85  +
        settings: &HttpConnectorSettings,
          86  +
        components: &RuntimeComponents,
          87  +
    ) -> SharedHttpConnector {
          88  +
        let connect_timeout = settings.connect_timeout();
          89  +
        let read_timeout = settings.read_timeout();
          90  +
        let sleep_impl = components.sleep_impl();
          91  +
          92  +
        if (connect_timeout.is_some() || read_timeout.is_some()) && sleep_impl.is_none() {
          93  +
            panic!(
          94  +
                "an async sleep impl is required to use connect/read timeouts with \
          95  +
                 the v2 HTTP client; provide one via `RuntimeComponents::sleep_impl`"
          96  +
            );
          97  +
        }
          98  +
          99  +
        SharedHttpConnector::new(PooledConnector {
         100  +
            pool: self.pool.inner.pool.clone(),
         101  +
            partition: self.partition.clone(),
         102  +
            connect_timeout,
         103  +
            read_timeout,
         104  +
            sleep_impl,
         105  +
            proxy_matcher: self.pool.inner.proxy_matcher.clone(),
         106  +
        })
         107  +
    }
         108  +
         109  +
    fn connector_metadata(&self) -> Option<ConnectorMetadata> {
         110  +
        Some(ConnectorMetadata::new("hyper", Some(Cow::Borrowed("1.x"))))
         111  +
    }
         112  +
}
         113  +
         114  +
// ---------------------------------------------------------------------------
         115  +
// PooledConnector (HttpConnector adapter)
         116  +
// ---------------------------------------------------------------------------
         117  +
         118  +
/// Smithy [`HttpConnector`] backed by the v2 connection pool.
         119  +
///
         120  +
/// Constructed fresh per [`HttpClient::http_connector`] call so it can
         121  +
/// capture the per-operation [`HttpConnectorSettings`] (connect/read
         122  +
/// timeouts). The pool itself is shared across all operations via `Arc`.
         123  +
struct PooledConnector {
         124  +
    pool: Arc<ConnectionPool>,
         125  +
    partition: Arc<PartitionState>,
         126  +
    connect_timeout: Option<Duration>,
         127  +
    read_timeout: Option<Duration>,
         128  +
    sleep_impl: Option<SharedAsyncSleep>,
         129  +
    proxy_matcher: Option<Arc<ProxyMatcher>>,
         130  +
}
         131  +
         132  +
impl std::fmt::Debug for PooledConnector {
         133  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         134  +
        f.debug_struct("PooledConnector").finish()
         135  +
    }
         136  +
}
         137  +
         138  +
impl HttpConnector for PooledConnector {
         139  +
    fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
         140  +
        let pool = self.pool.clone();
         141  +
        let partition = self.partition.clone();
         142  +
        let connect_timeout = self.connect_timeout;
         143  +
        let read_timeout = self.read_timeout;
         144  +
        let sleep_impl = self.sleep_impl.clone();
         145  +
        let proxy_matcher = self.proxy_matcher.clone();
         146  +
        HttpConnectorFuture::new(async move {
         147  +
            let mut request = request
         148  +
                .try_into_http1x()
         149  +
                .map_err(|err| ConnectorError::user(err.into()))?;
         150  +
         151  +
            let full_uri = request.uri().clone();
         152  +
         153  +
            if let Some(matcher) = proxy_matcher.as_ref() {
         154  +
                add_proxy_auth_header(&mut request, matcher);
         155  +
            }
         156  +
         157  +
            if let Some(capture_smithy) = request.extensions().get::<CaptureSmithyConnection>() {
         158  +
                let capture = super::ConnectionMetadataCapture::new();
         159  +
                let for_retriever = capture.clone();
         160  +
                capture_smithy.set_connection_retriever(move || for_retriever.get());
         161  +
                request.extensions_mut().insert(capture);
         162  +
            }
         163  +
         164  +
            if let Some((duration, sleep)) = read_timeout.zip(sleep_impl.clone()) {
         165  +
                request.extensions_mut().insert(super::ReadTimeoutHint(
         166  +
                    super::TimeoutContext::new(duration, sleep),
         167  +
                ));
         168  +
            }
         169  +
         170  +
            if !request.headers().contains_key(http_1x::header::HOST) {
         171  +
                if let Some(authority) = full_uri.authority() {
         172  +
                    let host = match super::builder::get_non_default_port(&full_uri) {
         173  +
                        Some(port) => format!("{}:{}", authority.host(), port),
         174  +
                        None => authority.host().to_string(),
         175  +
                    };
         176  +
                    request.headers_mut().insert(
         177  +
                        http_1x::header::HOST,
         178  +
                        http_1x::HeaderValue::from_str(&host)
         179  +
                            .expect("authority is valid header value"),
         180  +
                    );
         181  +
                }
         182  +
            }
         183  +
         184  +
            let connect_ctx = super::ConnectCtx::new(
         185  +
                full_uri,
         186  +
                connect_timeout
         187  +
                    .zip(sleep_impl)
         188  +
                    .map(|(d, s)| super::TimeoutContext::new(d, s)),
         189  +
            );
         190  +
         191  +
            let response = pool
         192  +
                .send_request(&partition, connect_ctx, request)
         193  +
                .await
         194  +
                .map_err(downcast_error)?;
         195  +
         196  +
            HttpResponse::try_from(response).map_err(|err| ConnectorError::other(err.into(), None))
         197  +
        })
         198  +
    }
         199  +
}
         200  +
         201  +
#[cfg(test)]
         202  +
mod tests {
         203  +
    use super::*;
         204  +
    use crate::client::pool::partition::{Partition, TokioDriverSpawner};
         205  +
         206  +
    #[tokio::test]
         207  +
    async fn client_new_uses_default_partition() {
         208  +
        let pool = SharedPool::builder().build_http();
         209  +
        let client = Client::new(&pool);
         210  +
        assert_eq!(client.partition_id(), PartitionId::default());
         211  +
    }
         212  +
         213  +
    #[tokio::test]
         214  +
    async fn from_partition_resolves_declared() {
         215  +
        let pool = SharedPool::builder()
         216  +
            .partitions([Partition::new(
         217  +
                PartitionId::from_index(3),
         218  +
                TokioDriverSpawner::current(),
         219  +
            )])
         220  +
            .build_http();
         221  +
        let client = Client::from_partition(&pool, PartitionId::from_index(3));
         222  +
        assert_eq!(client.partition_id(), PartitionId::from_index(3));
         223  +
    }
         224  +
         225  +
    #[tokio::test]
         226  +
    #[should_panic(expected = "partition not declared")]
         227  +
    async fn from_partition_unknown_panics() {
         228  +
        let pool = SharedPool::builder().build_http();
         229  +
        Client::from_partition(&pool, PartitionId::from_index(99));
         230  +
    }
         231  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/connection.rs

@@ -0,1 +0,1400 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
//! Connection state tracking for pooled connections.
           7  +
           8  +
use std::net::SocketAddr;
           9  +
use std::sync::atomic::{AtomicBool, Ordering};
          10  +
use std::sync::{Arc, Mutex};
          11  +
use std::task::{Context, Poll};
          12  +
use std::time::{Duration, Instant};
          13  +
          14  +
use aws_smithy_async::rt::sleep::SharedAsyncSleep;
          15  +
use aws_smithy_runtime_api::box_error::BoxError;
          16  +
use aws_smithy_runtime_api::client::connection::{ConnectionId, ConnectionMetadata};
          17  +
use aws_smithy_types::body::SdkBody;
          18  +
use pin_project_lite::pin_project;
          19  +
use tokio::sync::OwnedSemaphorePermit;
          20  +
use tower::Service;
          21  +
          22  +
use super::cache;
          23  +
use super::handshake::H1SendRequest;
          24  +
          25  +
/// Authority of a pooled connection (host with optional port).
          26  +
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
          27  +
pub struct Authority(Arc<str>);
          28  +
          29  +
impl Authority {
          30  +
    pub(crate) fn new(s: impl Into<Arc<str>>) -> Self {
          31  +
        Self(s.into())
          32  +
    }
          33  +
          34  +
    /// Construct an authority key for a host, as `host` or `host:port`.
          35  +
    ///
          36  +
    /// Used to query connection counts via [`SharedPool::stats`]. The
          37  +
    /// pool keys connection state by the authority component of each
          38  +
    /// request's URI, compared as an exact byte string. The value passed
          39  +
    /// here must match that form for a lookup to hit: notably, a port is
          40  +
    /// present only when the URI carried a non-default port (an HTTPS URI
          41  +
    /// to `example.com` keys as `example.com`, not `example.com:443`), and
          42  +
    /// the host is matched case-sensitively. A value that does not match
          43  +
    /// any keyed authority yields empty stats rather than an error.
          44  +
    ///
          45  +
    /// [`SharedPool::stats`]: crate::client::pool::SharedPool::stats
          46  +
    pub fn from_host(host: impl Into<Arc<str>>) -> Self {
          47  +
        Self(host.into())
          48  +
    }
          49  +
          50  +
    /// The authority as a string slice.
          51  +
    pub fn as_str(&self) -> &str {
          52  +
        &self.0
          53  +
    }
          54  +
}
          55  +
          56  +
impl AsRef<str> for Authority {
          57  +
    fn as_ref(&self) -> &str {
          58  +
        &self.0
          59  +
    }
          60  +
}
          61  +
          62  +
impl std::fmt::Display for Authority {
          63  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
          64  +
        self.0.fmt(f)
          65  +
    }
          66  +
}
          67  +
          68  +
/// Protocol negotiated for a connection.
          69  +
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
          70  +
#[non_exhaustive]
          71  +
pub enum NegotiatedProtocol {
          72  +
    /// HTTP/1.1
          73  +
    Http1,
          74  +
    /// HTTP/2
          75  +
    Http2,
          76  +
}
          77  +
          78  +
/// Why a connection was removed from the pool.
          79  +
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
          80  +
#[non_exhaustive]
          81  +
pub enum CloseReason {
          82  +
    /// Idle longer than the configured pool idle timeout.
          83  +
    IdleTimeout,
          84  +
    /// Marked as poisoned (unhealthy) by the SDK or pool.
          85  +
    Poisoned,
          86  +
    /// Connection found dead at checkout (e.g., server closed the
          87  +
    /// connection while it was idle in the pool). The associated error
          88  +
    /// on the event carries specifics.
          89  +
    Unusable,
          90  +
    /// The pool itself was dropped.
          91  +
    PoolDropped,
          92  +
    /// Dropped to free a connection permit for another partition under
          93  +
    /// cap pressure (cross-partition active reclaim). Distinct from
          94  +
    /// `IdleTimeout`: the connection was still within its idle window but
          95  +
    /// was reclaimed because a starved partition needed the capacity.
          96  +
    Reclaimed,
          97  +
}
          98  +
          99  +
/// Timing breakdown for connection establishment.
         100  +
#[derive(Clone, Copy, Debug)]
         101  +
#[non_exhaustive]
         102  +
pub struct ConnectionTiming {
         103  +
    /// TCP connect + TLS handshake combined. Measured from connector call
         104  +
    /// start to connected IO stream returned.
         105  +
    connect_duration: Duration,
         106  +
}
         107  +
         108  +
impl ConnectionTiming {
         109  +
    pub(crate) fn new(connect_duration: Duration) -> Self {
         110  +
        Self { connect_duration }
         111  +
    }
         112  +
         113  +
    /// Total time to establish the transport (TCP + TLS).
         114  +
    pub fn connect_duration(&self) -> Duration {
         115  +
        self.connect_duration
         116  +
    }
         117  +
}
         118  +
         119  +
/// Emitted when a new connection is established (TCP + TLS + HTTP handshake).
         120  +
#[derive(Debug)]
         121  +
#[non_exhaustive]
         122  +
pub struct ConnectionCreatedEvent {
         123  +
    conn_id: ConnectionId,
         124  +
    authority: Authority,
         125  +
    remote_addr: Option<SocketAddr>,
         126  +
    protocol: NegotiatedProtocol,
         127  +
    timing: ConnectionTiming,
         128  +
}
         129  +
         130  +
impl ConnectionCreatedEvent {
         131  +
    pub(crate) fn new(
         132  +
        conn_id: ConnectionId,
         133  +
        authority: Authority,
         134  +
        remote_addr: Option<SocketAddr>,
         135  +
        protocol: NegotiatedProtocol,
         136  +
        timing: ConnectionTiming,
         137  +
    ) -> Self {
         138  +
        Self {
         139  +
            conn_id,
         140  +
            authority,
         141  +
            remote_addr,
         142  +
            protocol,
         143  +
            timing,
         144  +
        }
         145  +
    }
         146  +
         147  +
    /// The pool-assigned connection identifier.
         148  +
    pub fn conn_id(&self) -> ConnectionId {
         149  +
        self.conn_id
         150  +
    }
         151  +
         152  +
    /// The authority (host:port) this connection is for.
         153  +
    pub fn authority(&self) -> &Authority {
         154  +
        &self.authority
         155  +
    }
         156  +
         157  +
    /// Remote address of the peer, if known.
         158  +
    pub fn remote_addr(&self) -> Option<SocketAddr> {
         159  +
        self.remote_addr
         160  +
    }
         161  +
         162  +
    /// Negotiated protocol.
         163  +
    pub fn protocol(&self) -> NegotiatedProtocol {
         164  +
        self.protocol
         165  +
    }
         166  +
         167  +
    /// Timing breakdown for connection establishment.
         168  +
    pub fn timing(&self) -> &ConnectionTiming {
         169  +
        &self.timing
         170  +
    }
         171  +
}
         172  +
         173  +
/// Emitted when an existing idle connection is checked out from the pool.
         174  +
#[derive(Debug)]
         175  +
#[non_exhaustive]
         176  +
pub struct ConnectionReusedEvent {
         177  +
    conn_id: ConnectionId,
         178  +
    authority: Authority,
         179  +
}
         180  +
         181  +
impl ConnectionReusedEvent {
         182  +
    pub(crate) fn new(conn_id: ConnectionId, authority: Authority) -> Self {
         183  +
        Self { conn_id, authority }
         184  +
    }
         185  +
         186  +
    /// The pool-assigned connection identifier.
         187  +
    pub fn conn_id(&self) -> ConnectionId {
         188  +
        self.conn_id
         189  +
    }
         190  +
         191  +
    /// The authority (host:port) this connection is for.
         192  +
    pub fn authority(&self) -> &Authority {
         193  +
        &self.authority
         194  +
    }
         195  +
}
         196  +
         197  +
/// Emitted when a connection is removed from the pool.
         198  +
#[non_exhaustive]
         199  +
pub struct ConnectionClosedEvent {
         200  +
    conn_id: ConnectionId,
         201  +
    authority: Authority,
         202  +
    remote_addr: Option<SocketAddr>,
         203  +
    reason: CloseReason,
         204  +
    error: Option<BoxError>,
         205  +
}
         206  +
         207  +
impl std::fmt::Debug for ConnectionClosedEvent {
         208  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         209  +
        let mut s = f.debug_struct("ConnectionClosedEvent");
         210  +
        s.field("conn_id", &self.conn_id)
         211  +
            .field("authority", &self.authority)
         212  +
            .field("remote_addr", &self.remote_addr)
         213  +
            .field("reason", &self.reason);
         214  +
        if let Some(ref e) = self.error {
         215  +
            s.field("error", &format_args!("{e}"));
         216  +
        }
         217  +
        s.finish()
         218  +
    }
         219  +
}
         220  +
         221  +
impl ConnectionClosedEvent {
         222  +
    pub(crate) fn new(
         223  +
        conn_id: ConnectionId,
         224  +
        authority: Authority,
         225  +
        remote_addr: Option<SocketAddr>,
         226  +
        reason: CloseReason,
         227  +
        error: Option<BoxError>,
         228  +
    ) -> Self {
         229  +
        Self {
         230  +
            conn_id,
         231  +
            authority,
         232  +
            remote_addr,
         233  +
            reason,
         234  +
            error,
         235  +
        }
         236  +
    }
         237  +
         238  +
    /// The pool-assigned connection identifier.
         239  +
    pub fn conn_id(&self) -> ConnectionId {
         240  +
        self.conn_id
         241  +
    }
         242  +
         243  +
    /// The authority (host:port) this connection was for.
         244  +
    pub fn authority(&self) -> &Authority {
         245  +
        &self.authority
         246  +
    }
         247  +
         248  +
    /// Remote address of the peer, if known.
         249  +
    pub fn remote_addr(&self) -> Option<SocketAddr> {
         250  +
        self.remote_addr
         251  +
    }
         252  +
         253  +
    /// Why the connection was closed.
         254  +
    pub fn reason(&self) -> CloseReason {
         255  +
        self.reason
         256  +
    }
         257  +
         258  +
    /// The error associated with this close, if any. Present for
         259  +
    /// server-initiated closes; `None` for policy-driven closes
         260  +
    /// (idle timeout, poisoning).
         261  +
    pub fn error(&self) -> Option<&(dyn std::error::Error + Send + Sync)> {
         262  +
        self.error.as_ref().map(|e| e.as_ref())
         263  +
    }
         264  +
}
         265  +
         266  +
/// Emitted when a connection attempt fails before completing the handshake.
         267  +
#[non_exhaustive]
         268  +
pub struct ConnectionFailedEvent {
         269  +
    authority: Authority,
         270  +
    remote_addr: Option<SocketAddr>,
         271  +
    error: BoxError,
         272  +
}
         273  +
         274  +
impl std::fmt::Debug for ConnectionFailedEvent {
         275  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         276  +
        f.debug_struct("ConnectionFailedEvent")
         277  +
            .field("authority", &self.authority)
         278  +
            .field("remote_addr", &self.remote_addr)
         279  +
            .field("error", &format_args!("{}", self.error))
         280  +
            .finish()
         281  +
    }
         282  +
}
         283  +
         284  +
impl ConnectionFailedEvent {
         285  +
    pub(crate) fn new(
         286  +
        authority: Authority,
         287  +
        remote_addr: Option<SocketAddr>,
         288  +
        error: BoxError,
         289  +
    ) -> Self {
         290  +
        Self {
         291  +
            authority,
         292  +
            remote_addr,
         293  +
            error,
         294  +
        }
         295  +
    }
         296  +
         297  +
    /// The authority (host:port) the connection was attempting to reach.
         298  +
    pub fn authority(&self) -> &Authority {
         299  +
        &self.authority
         300  +
    }
         301  +
         302  +
    /// Remote address of the peer, if known. `None` when the failure occurred
         303  +
    /// before a peer address was established (e.g., DNS resolution failure or
         304  +
    /// connection refused before address binding).
         305  +
    pub fn remote_addr(&self) -> Option<SocketAddr> {
         306  +
        self.remote_addr
         307  +
    }
         308  +
         309  +
    /// The error that caused the connection attempt to fail.
         310  +
    pub fn error(&self) -> &(dyn std::error::Error + Send + Sync) {
         311  +
        self.error.as_ref()
         312  +
    }
         313  +
}
         314  +
         315  +
/// Callback for connection lifecycle events within the pool.
         316  +
///
         317  +
/// Implementations receive notifications when connections are created,
         318  +
/// reused from the pool, closed, or fail to establish.
         319  +
///
         320  +
/// Implementations must be non-blocking. Defer expensive work to a
         321  +
/// background task.
         322  +
pub trait ConnectionEventListener: Send + Sync + 'static {
         323  +
    /// A new connection was established.
         324  +
    fn on_created(&self, _event: &ConnectionCreatedEvent) {}
         325  +
    /// An existing idle connection was checked out from the pool.
         326  +
    fn on_reused(&self, _event: &ConnectionReusedEvent) {}
         327  +
    /// A connection was removed from the pool.
         328  +
    fn on_closed(&self, _event: &ConnectionClosedEvent) {}
         329  +
    /// A connection attempt failed before completing the handshake.
         330  +
    fn on_connection_failed(&self, _event: &ConnectionFailedEvent) {}
         331  +
}
         332  +
         333  +
/// A duration paired with the sleep implementation used to realize it.
         334  +
///
         335  +
/// This type makes "timeout without sleep impl" unrepresentable: you cannot
         336  +
/// construct one without committing to a way to actually wait. Created at the
         337  +
/// adapter layer from `HttpConnectorSettings` + `RuntimeComponents::sleep_impl()`
         338  +
/// and passed down the pool stack where timeouts are applied.
         339  +
#[derive(Clone, Debug)]
         340  +
pub(crate) struct TimeoutContext {
         341  +
    pub(crate) duration: Duration,
         342  +
    pub(crate) sleep_impl: SharedAsyncSleep,
         343  +
}
         344  +
         345  +
impl TimeoutContext {
         346  +
    pub(crate) fn new(duration: Duration, sleep_impl: SharedAsyncSleep) -> Self {
         347  +
        Self {
         348  +
            duration,
         349  +
            sleep_impl,
         350  +
        }
         351  +
    }
         352  +
}
         353  +
         354  +
/// Target type for the connect portion of the pool stack.
         355  +
///
         356  +
/// Replaces bare `Uri` so per-operation connect metadata flows through the
         357  +
/// composable pool types (Map → Negotiate → Cache → handshake → ConnectionLimit
         358  +
/// → TCP connector) to the layers that need them.
         359  +
#[derive(Clone, Debug)]
         360  +
pub(crate) struct ConnectCtx {
         361  +
    /// Target URI: its authority is the pool key, and the full URI is
         362  +
    /// passed to the TCP connector.
         363  +
    pub(crate) uri: http_1x::Uri,
         364  +
    /// Bounds new-connection establishment (TCP + TLS handshake). `None`
         365  +
    /// means no connect timeout; cached connections skip the connector
         366  +
    /// entirely so this is automatically a no-op on cache hit.
         367  +
    pub(crate) connect_timeout: Option<TimeoutContext>,
         368  +
    /// How the connect path behaves when a permit cannot be acquired.
         369  +
    pub(crate) mode: AcquireMode,
         370  +
}
         371  +
         372  +
/// Behavior of the connect path when the connection cap is reached.
         373  +
///
         374  +
/// Selected per request (carried on [`ConnectCtx`]) because the same
         375  +
/// partition stack is exercised twice under `PreferLocal`: once
         376  +
/// `NonBlocking` to probe for local capacity, then `Blocking` as the
         377  +
/// fallback after a peer-borrow miss.
         378  +
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
         379  +
pub(crate) enum AcquireMode {
         380  +
    /// Block on the semaphore until a permit is free (active reclaim, then
         381  +
    /// FIFO wait). The authoritative take.
         382  +
    #[default]
         383  +
    Blocking,
         384  +
    /// Return [`CapBound`] immediately on `NoPermits` instead of blocking,
         385  +
    /// so the caller can try borrowing a peer's connection first.
         386  +
    NonBlocking,
         387  +
}
         388  +
         389  +
/// Sentinel error from the connect path under [`AcquireMode::NonBlocking`]:
         390  +
/// the connection cap is reached and no permit is available. Distinct from
         391  +
/// any real connect failure, and distinct from negotiate's internal
         392  +
/// `UseOther` sentinel, so it propagates verbatim up the stack.
         393  +
#[derive(Debug)]
         394  +
pub(crate) struct CapBound;
         395  +
         396  +
impl std::fmt::Display for CapBound {
         397  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         398  +
        f.write_str("connection cap reached (non-blocking acquire)")
         399  +
    }
         400  +
}
         401  +
         402  +
impl std::error::Error for CapBound {}
         403  +
         404  +
impl CapBound {
         405  +
    /// Whether `err` is a `CapBound` sentinel anywhere in its chain.
         406  +
    pub(crate) fn is(err: &(dyn std::error::Error + 'static)) -> bool {
         407  +
        let mut e: Option<&(dyn std::error::Error + 'static)> = Some(err);
         408  +
        while let Some(cur) = e {
         409  +
            if cur.is::<CapBound>() {
         410  +
                return true;
         411  +
            }
         412  +
            e = cur.source();
         413  +
        }
         414  +
        false
         415  +
    }
         416  +
}
         417  +
         418  +
impl ConnectCtx {
         419  +
    pub(crate) fn new(uri: http_1x::Uri, connect_timeout: Option<TimeoutContext>) -> Self {
         420  +
        Self {
         421  +
            uri,
         422  +
            connect_timeout,
         423  +
            mode: AcquireMode::Blocking,
         424  +
        }
         425  +
    }
         426  +
         427  +
    /// Set the acquire mode (defaults to [`AcquireMode::Blocking`]).
         428  +
    pub(crate) fn with_mode(mut self, mode: AcquireMode) -> Self {
         429  +
        self.mode = mode;
         430  +
        self
         431  +
    }
         432  +
}
         433  +
         434  +
/// Request extension set by the adapter to hint a read timeout to the
         435  +
/// checkout services (`H{1,2}Checkout`).
         436  +
///
         437  +
/// `Some(...)` means: once the connection is selected (cache hit or fresh
         438  +
/// handshake), wrap `conn.call(req)` with this timeout. Bounds request-write
         439  +
/// + response-headers-wait only. Does NOT include pool acquire or connect
         440  +
/// establishment (those have their own timeouts).
         441  +
#[derive(Clone, Debug)]
         442  +
pub(crate) struct ReadTimeoutHint(pub(crate) TimeoutContext);
         443  +
         444  +
/// Permits acquired from connection limit semaphores.
         445  +
/// Held for the lifetime of the connection; dropped when the connection is dropped.
         446  +
pub(crate) struct ConnectionPermit {
         447  +
    _global: Option<OwnedSemaphorePermit>,
         448  +
    _per_host: Option<OwnedSemaphorePermit>,
         449  +
}
         450  +
         451  +
impl ConnectionPermit {
         452  +
    pub(crate) fn new(
         453  +
        global: Option<OwnedSemaphorePermit>,
         454  +
        per_host: Option<OwnedSemaphorePermit>,
         455  +
    ) -> Self {
         456  +
        Self {
         457  +
            _global: global,
         458  +
            _per_host: per_host,
         459  +
        }
         460  +
    }
         461  +
}
         462  +
         463  +
/// Outcome of establishing a new transport connection (TCP + TLS).
         464  +
///
         465  +
/// Carries the IO handle and the connection permit acquired during
         466  +
/// connection establishment. The IO handle drives the protocol
         467  +
/// handshake; the permit holds the connection's slot in the pool's
         468  +
/// limit semaphores until the connection is dropped.
         469  +
pub(crate) struct EstablishedConnection<IO> {
         470  +
    pub(crate) io: IO,
         471  +
    pub(crate) permit: Arc<ConnectionPermit>,
         472  +
    pub(crate) establishing: super::stats::EstablishingGuard,
         473  +
}
         474  +
         475  +
/// Error returned by `ManagedConnection::poll_ready` when the connection
         476  +
/// has been marked poisoned and should not be reused.
         477  +
#[derive(Debug)]
         478  +
pub(crate) struct PoisonedError;
         479  +
         480  +
impl std::fmt::Display for PoisonedError {
         481  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         482  +
        f.write_str("connection poisoned")
         483  +
    }
         484  +
}
         485  +
         486  +
impl std::error::Error for PoisonedError {}
         487  +
         488  +
/// Metadata about a connection captured at establishment time.
         489  +
///
         490  +
/// Captured between TLS connector output and protocol handshake (the last point
         491  +
/// where the raw transport stream is accessible).
         492  +
#[derive(Debug, Clone)]
         493  +
pub(crate) struct ConnectionInfo {
         494  +
    /// Remote address of the peer. `None` when the underlying connector
         495  +
    /// did not attach `HttpInfo` to its `Connected` extras.
         496  +
    pub(crate) remote_addr: Option<SocketAddr>,
         497  +
    /// Local address of this end of the connection.
         498  +
    pub(crate) local_addr: Option<SocketAddr>,
         499  +
    /// `true` when this connection is to an HTTP proxy server (rather than
         500  +
    /// directly to the origin). Drives request-target form selection: H1
         501  +
    /// requests dispatched on a proxied connection use absolute-form URIs;
         502  +
    /// direct connections use origin-form. Populated from
         503  +
    /// [`hyper_util::client::legacy::connect::Connected::is_proxied`] at
         504  +
    /// handshake.
         505  +
    pub(crate) is_proxied: bool,
         506  +
    /// The authority (host:port) this connection is for. Populated from the
         507  +
    /// URI at handshake time.
         508  +
    pub(crate) authority: Authority,
         509  +
}
         510  +
         511  +
/// A one-shot "this connection is dead, don't reuse it" flag.
         512  +
///
         513  +
/// Shared via `Arc`; all clones observe and control the same flag.
         514  +
/// `ManagedConnection` holds one and hands out clones (via `metadata()`)
         515  +
/// that let the adapter layer mark the connection poisoned through
         516  +
/// smithy's `ConnectionMetadata::poison_fn`. On next checkout or return,
         517  +
/// the pool sees the flag set and drops the connection instead of
         518  +
/// reusing it.
         519  +
#[derive(Debug, Clone, Default)]
         520  +
pub(crate) struct PoisonPill {
         521  +
    flag: Arc<AtomicBool>,
         522  +
}
         523  +
         524  +
impl PoisonPill {
         525  +
    /// Create a fresh, non-poisoned pill.
         526  +
    pub(crate) fn healthy() -> Self {
         527  +
        Self::default()
         528  +
    }
         529  +
         530  +
    /// Mark the connection as poisoned.
         531  +
    pub(crate) fn poison(&self) {
         532  +
        self.flag.store(true, Ordering::Release);
         533  +
    }
         534  +
         535  +
    /// Whether the connection has been poisoned.
         536  +
    pub(crate) fn is_poisoned(&self) -> bool {
         537  +
        self.flag.load(Ordering::Acquire)
         538  +
    }
         539  +
}
         540  +
         541  +
/// A connection with SDK-owned lifecycle metadata.
         542  +
///
         543  +
/// Wraps the inner service (typically `SendRequest<SdkBody>`) with state needed
         544  +
/// for pool management: poisoning, connection identity, and permit lifetime.
         545  +
///
         546  +
/// Clone is supported when the inner service is Clone (e.g., HTTP/2 multiplexed
         547  +
/// connections). Clones share the same poison pill and connection info, so
         548  +
/// poisoning one clone poisons all of them.
         549  +
pub(crate) struct ManagedConnection<S> {
         550  +
    inner: S,
         551  +
    pub(crate) info: ConnectionInfo,
         552  +
    /// Stable identifier for this physical connection, unique within the
         553  +
    /// owning pool. Shared across `Clone`s (an H2 connection's multiplexed
         554  +
    /// request handles all carry the same `conn_id`). Used in tracing and
         555  +
    /// surfaced through `ConnectionMetadata` for cross-layer correlation.
         556  +
    conn_id: ConnectionId,
         557  +
    created_at: Instant,
         558  +
    /// Timestamp the connection last became idle (or its creation time, if
         559  +
    /// it has never been returned to the pool).
         560  +
    ///
         561  +
    /// - **H1**: stamped by [`CachedConnection::drop`] on the unpoisoned
         562  +
    ///   return-to-pool path. Presence in the cache implies idle.
         563  +
    /// - **H2**: stamped by [`SingletonConnection::drop`] on the
         564  +
    ///   `active_streams` 1 → 0 transition. Combined with
         565  +
    ///   `active_streams > 0` in the H2 retain predicate, this prevents
         566  +
    ///   eviction of a multiplexed connection that's still serving streams.
         567  +
    ///
         568  +
    /// `Arc<Mutex<_>>` because all `Clone`s of a `ManagedConnection`
         569  +
    /// observe the same timestamp (last-write-wins). `Mutex` over an
         570  +
    /// atomic-encoded `u64` because writes happen at most once per
         571  +
    /// request and reads at most once per eviction tick.
         572  +
    idle_at: Arc<Mutex<Instant>>,
         573  +
    /// In-flight request count, used by the H2 retain predicate to keep
         574  +
    /// actively-multiplexed connections alive regardless of `idle_at`.
         575  +
    ///
         576  +
    /// - **H1**: never incremented (always 0). H1 is serial; the cache's
         577  +
    ///   idle set is the authoritative idleness signal.
         578  +
    /// - **H2**: incremented by [`SingletonConnection::call`] on dispatch,
         579  +
    ///   decremented by [`SingletonConnection::drop`] when the body guard
         580  +
    ///   releases.
         581  +
    ///
         582  +
    /// Arc-shared so all `Clone`s of an H2 `ManagedConnection` mutate the
         583  +
    /// same counter.
         584  +
    active_streams: Arc<std::sync::atomic::AtomicUsize>,
         585  +
    poison: PoisonPill,
         586  +
    _permit: Arc<ConnectionPermit>,
         587  +
    /// Fires `established--` when the last clone drops. Arc-shared so H2
         588  +
    /// clones share one guard that drops once.
         589  +
    _established: Arc<super::stats::EstablishedGuard>,
         590  +
}
         591  +
         592  +
impl<S> ManagedConnection<S> {
         593  +
    /// Create a new managed connection wrapping the given service.
         594  +
    pub(crate) fn new(
         595  +
        inner: S,
         596  +
        info: ConnectionInfo,
         597  +
        conn_id: ConnectionId,
         598  +
        permit: Arc<ConnectionPermit>,
         599  +
        established: super::stats::EstablishedGuard,
         600  +
    ) -> Self {
         601  +
        let now = Instant::now();
         602  +
        Self {
         603  +
            inner,
         604  +
            info,
         605  +
            conn_id,
         606  +
            created_at: now,
         607  +
            idle_at: Arc::new(Mutex::new(now)),
         608  +
            active_streams: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
         609  +
            poison: PoisonPill::healthy(),
         610  +
            _permit: permit,
         611  +
            _established: Arc::new(established),
         612  +
        }
         613  +
    }
         614  +
         615  +
    /// Stable identifier for this physical connection. Shared across
         616  +
    /// clones (H2 multiplexing).
         617  +
    pub(crate) fn conn_id(&self) -> ConnectionId {
         618  +
        self.conn_id
         619  +
    }
         620  +
         621  +
    /// Whether this connection has been poisoned.
         622  +
    pub(crate) fn is_poisoned(&self) -> bool {
         623  +
        self.poison.is_poisoned()
         624  +
    }
         625  +
         626  +
    /// Connection info (remote/local addresses).
         627  +
    #[allow(dead_code)] // accessor for telemetry/debugging consumers
         628  +
    pub(crate) fn info(&self) -> &ConnectionInfo {
         629  +
        &self.info
         630  +
    }
         631  +
         632  +
    /// When this connection was established.
         633  +
    #[allow(dead_code)] // accessor for connection age (telemetry/debugging)
         634  +
    pub(crate) fn created_at(&self) -> Instant {
         635  +
        self.created_at
         636  +
    }
         637  +
         638  +
    /// Timestamp of the last return-to-idle (or creation).
         639  +
    ///
         640  +
    /// Guaranteed meaningful for any connection sitting in the pool: the
         641  +
    /// initial value is `created_at`, and every return-to-pool overwrites
         642  +
    /// it via [`Self::mark_idle`].
         643  +
    pub(crate) fn idle_at(&self) -> Instant {
         644  +
        *self.idle_at.lock().expect("idle_at lock poisoned")
         645  +
    }
         646  +
         647  +
    /// Stamp the return-to-idle moment. Called by [`CachedConnection::drop`]
         648  +
    /// on the unpoisoned path.
         649  +
    pub(crate) fn mark_idle(&self) {
         650  +
        *self.idle_at.lock().expect("idle_at lock poisoned") = Instant::now();
         651  +
    }
         652  +
         653  +
    /// Current in-flight stream count. See [`Self::active_streams`]
         654  +
    /// (field doc) for semantics.
         655  +
    pub(crate) fn active_streams_count(&self) -> usize {
         656  +
        self.active_streams
         657  +
            .load(std::sync::atomic::Ordering::Acquire)
         658  +
    }
         659  +
         660  +
    /// Clone of the Arc'd stream counter, for publication through the H2
         661  +
    /// side-channel so `SingletonConnection` can increment/decrement it
         662  +
    /// without reaching through the opaque `Singled<…>`.
         663  +
    pub(crate) fn active_streams_ref(&self) -> Arc<std::sync::atomic::AtomicUsize> {
         664  +
        self.active_streams.clone()
         665  +
    }
         666  +
         667  +
    /// Clone of the Arc'd idle timestamp, for publication through the H2
         668  +
    /// side-channel so `SingletonConnection::drop` can stamp it on the
         669  +
    /// `active_streams` 1 → 0 transition.
         670  +
    pub(crate) fn idle_at_ref(&self) -> Arc<Mutex<Instant>> {
         671  +
        self.idle_at.clone()
         672  +
    }
         673  +
         674  +
    /// Mutable access to the inner service.
         675  +
    pub(crate) fn inner_mut(&mut self) -> &mut S {
         676  +
        &mut self.inner
         677  +
    }
         678  +
         679  +
    /// Build a smithy `ConnectionMetadata` for this connection.
         680  +
    ///
         681  +
    /// The returned metadata captures a clone of the `PoisonPill`, so
         682  +
    /// calling `ConnectionMetadata::poison()` flips this connection's
         683  +
    /// poison flag (the same flag the pool checks on checkout/return).
         684  +
    /// Address fields are copied.
         685  +
    pub(crate) fn metadata(&self) -> ConnectionMetadata {
         686  +
        let poison = self.poison.clone();
         687  +
        let conn_id = self.conn_id;
         688  +
        let remote = self.info.remote_addr;
         689  +
        let mut builder = ConnectionMetadata::builder()
         690  +
            .proxied(self.info.is_proxied)
         691  +
            .connection_id(self.conn_id)
         692  +
            .poison_fn(move || {
         693  +
                tracing::debug!(conn_id = %conn_id, ?remote, "pool: connection poisoned");
         694  +
                poison.poison();
         695  +
            });
         696  +
        builder
         697  +
            .set_remote_addr(self.info.remote_addr)
         698  +
            .set_local_addr(self.info.local_addr);
         699  +
        builder.build()
         700  +
    }
         701  +
         702  +
    /// `true` when the underlying connection is to an HTTP proxy. Used by
         703  +
    /// H1 dispatch to choose absolute-form URIs over origin-form.
         704  +
    pub(crate) fn is_proxied(&self) -> bool {
         705  +
        self.info.is_proxied
         706  +
    }
         707  +
}
         708  +
         709  +
impl<S: Clone> Clone for ManagedConnection<S> {
         710  +
    fn clone(&self) -> Self {
         711  +
        Self {
         712  +
            inner: self.inner.clone(),
         713  +
            info: self.info.clone(),
         714  +
            conn_id: self.conn_id,
         715  +
            created_at: self.created_at,
         716  +
            idle_at: self.idle_at.clone(),
         717  +
            active_streams: self.active_streams.clone(),
         718  +
            poison: self.poison.clone(),
         719  +
            _permit: self._permit.clone(),
         720  +
            _established: self._established.clone(),
         721  +
        }
         722  +
    }
         723  +
}
         724  +
         725  +
impl<S> Service<http_1x::Request<SdkBody>> for ManagedConnection<S>
         726  +
where
         727  +
    S: Service<http_1x::Request<SdkBody>>,
         728  +
    S::Error: Into<BoxError>,
         729  +
    S::Future: Send + 'static,
         730  +
    S::Response: 'static,
         731  +
{
         732  +
    type Response = S::Response;
         733  +
    type Error = BoxError;
         734  +
    type Future = std::pin::Pin<
         735  +
        Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
         736  +
    >;
         737  +
         738  +
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
         739  +
        if self.is_poisoned() {
         740  +
            return Poll::Ready(Err(PoisonedError.into()));
         741  +
        }
         742  +
        self.inner_mut().poll_ready(cx).map_err(Into::into)
         743  +
    }
         744  +
         745  +
    fn call(&mut self, req: http_1x::Request<SdkBody>) -> Self::Future {
         746  +
        let fut = self.inner_mut().call(req);
         747  +
        Box::pin(async move { fut.await.map_err(Into::into) })
         748  +
    }
         749  +
}
         750  +
         751  +
/// A connection checked out from the H1 cache.
         752  +
///
         753  +
/// Wraps `cache::Cached<ManagedConnection<S>>` so that poisoned connections
         754  +
/// are dropped from the pool (via `Cached::discard`) instead of being
         755  +
/// returned for reuse when dropped. Healthy connections return to the pool
         756  +
/// normally through `Cached::Drop`.
         757  +
pub(crate) struct CachedConnection<S> {
         758  +
    inner: Option<cache::Cached<ManagedConnection<S>>>,
         759  +
    listener: Option<Arc<dyn ConnectionEventListener>>,
         760  +
    counters: Arc<super::stats::ConnectionCounters>,
         761  +
}
         762  +
         763  +
impl<S> CachedConnection<S> {
         764  +
    pub(crate) fn new(
         765  +
        cached: cache::Cached<ManagedConnection<S>>,
         766  +
        listener: Option<Arc<dyn ConnectionEventListener>>,
         767  +
        counters: Arc<super::stats::ConnectionCounters>,
         768  +
    ) -> Self {
         769  +
        counters.incr_active();
         770  +
        let is_reuse = *cached.inner().idle_at.lock().unwrap() > cached.inner().created_at;
         771  +
        if is_reuse {
         772  +
            let conn_id = cached.inner().conn_id();
         773  +
            let authority = cached.inner().info.authority.clone();
         774  +
            tracing::trace!(conn_id = %conn_id, "pool: connection reused");
         775  +
            if let Some(ref l) = listener {
         776  +
                l.on_reused(&ConnectionReusedEvent::new(conn_id, authority));
         777  +
            }
         778  +
        }
         779  +
        Self {
         780  +
            inner: Some(cached),
         781  +
            listener,
         782  +
            counters,
         783  +
        }
         784  +
    }
         785  +
         786  +
    /// Build a smithy `ConnectionMetadata` for the underlying H1 connection.
         787  +
    ///
         788  +
    /// See [`ManagedConnection::metadata`].
         789  +
    ///
         790  +
    /// Panics if called after the inner cached handle has been consumed.
         791  +
    pub(crate) fn metadata(&self) -> ConnectionMetadata {
         792  +
        self.inner
         793  +
            .as_ref()
         794  +
            .expect("CachedConnection metadata after drop")
         795  +
            .inner()
         796  +
            .metadata()
         797  +
    }
         798  +
         799  +
    /// Forwards [`ManagedConnection::is_proxied`].
         800  +
    pub(crate) fn is_proxied(&self) -> bool {
         801  +
        self.inner
         802  +
            .as_ref()
         803  +
            .expect("CachedConnection is_proxied after drop")
         804  +
            .inner()
         805  +
            .is_proxied()
         806  +
    }
         807  +
         808  +
    /// Forwards [`ManagedConnection::conn_id`].
         809  +
    pub(crate) fn conn_id(&self) -> ConnectionId {
         810  +
        self.inner
         811  +
            .as_ref()
         812  +
            .expect("CachedConnection conn_id after drop")
         813  +
            .inner()
         814  +
            .conn_id()
         815  +
    }
         816  +
}
         817  +
         818  +
impl<S, Req> Service<Req> for CachedConnection<S>
         819  +
where
         820  +
    cache::Cached<ManagedConnection<S>>: Service<Req>,
         821  +
{
         822  +
    type Response = <cache::Cached<ManagedConnection<S>> as Service<Req>>::Response;
         823  +
    type Error = <cache::Cached<ManagedConnection<S>> as Service<Req>>::Error;
         824  +
    type Future = <cache::Cached<ManagedConnection<S>> as Service<Req>>::Future;
         825  +
         826  +
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
         827  +
        match self.inner.as_mut().unwrap().poll_ready(cx) {
         828  +
            Poll::Ready(Err(e)) => {
         829  +
                // Connection is dead. If poisoned, Drop handles the event.
         830  +
                // If not poisoned, this is an unusable connection (server
         831  +
                // closed it while idle, driver died, etc.).
         832  +
                let managed = self.inner.as_ref().unwrap().inner();
         833  +
                if !managed.is_poisoned() {
         834  +
                    if let Some(ref l) = self.listener {
         835  +
                        l.on_closed(&ConnectionClosedEvent::new(
         836  +
                            managed.conn_id,
         837  +
                            managed.info.authority.clone(),
         838  +
                            managed.info.remote_addr,
         839  +
                            CloseReason::Unusable,
         840  +
                            None,
         841  +
                        ));
         842  +
                    }
         843  +
                }
         844  +
                Poll::Ready(Err(e))
         845  +
            }
         846  +
            other => other,
         847  +
        }
         848  +
    }
         849  +
         850  +
    fn call(&mut self, req: Req) -> Self::Future {
         851  +
        self.inner.as_mut().unwrap().call(req)
         852  +
    }
         853  +
}
         854  +
         855  +
impl<S> Drop for CachedConnection<S> {
         856  +
    fn drop(&mut self) {
         857  +
        self.counters.decr_active();
         858  +
        if let Some(cached) = self.inner.take() {
         859  +
            let managed = cached.inner();
         860  +
            let conn_id = managed.conn_id;
         861  +
            if managed.is_poisoned() {
         862  +
                tracing::debug!(conn_id = %conn_id, "pool: connection discarded (poisoned)");
         863  +
                if let Some(ref listener) = self.listener {
         864  +
                    listener.on_closed(&ConnectionClosedEvent::new(
         865  +
                        managed.conn_id(),
         866  +
                        managed.info.authority.clone(),
         867  +
                        managed.info.remote_addr,
         868  +
                        CloseReason::Poisoned,
         869  +
                        None,
         870  +
                    ));
         871  +
                }
         872  +
                cached.discard();
         873  +
            } else {
         874  +
                managed.mark_idle();
         875  +
                tracing::trace!(conn_id = %conn_id, "pool: connection returned to idle");
         876  +
            }
         877  +
        }
         878  +
    }
         879  +
}
         880  +
         881  +
/// State an H2 checkout (`SingletonConnection`) needs but cannot reach
         882  +
/// through the opaque `Singled<…>` to read from the underlying
         883  +
/// `ManagedConnection`. Published by the H2 handshake on each new
         884  +
/// connection; consumed by `SingletonConnection::new` as a snapshot.
         885  +
///
         886  +
/// `active_streams` and `idle_at` are clones of the Arcs held by the
         887  +
/// `ManagedConnection`, so updates through this state are visible to the
         888  +
/// retain predicate operating on the connection directly.
         889  +
#[derive(Clone)]
         890  +
pub(crate) struct H2ConnectionState {
         891  +
    pub(crate) metadata: ConnectionMetadata,
         892  +
    pub(crate) active_streams: Arc<std::sync::atomic::AtomicUsize>,
         893  +
    pub(crate) idle_at: Arc<Mutex<Instant>>,
         894  +
}
         895  +
         896  +
/// Per-host side-channel between H2 handshake (writer) and H2 checkout
         897  +
/// (reader).
         898  +
///
         899  +
/// `hyper_util::client::pool::singleton::Singled<…>` is opaque, so the
         900  +
/// checkout side cannot reach through it to the underlying
         901  +
/// `ManagedConnection`. This ref carries everything the checkout side
         902  +
/// needs (stamped fresh on each handshake): the user-facing
         903  +
/// `ConnectionMetadata` for poison and address surfacing, plus the
         904  +
/// Arc-shared `active_streams` counter and `idle_at` timestamp that the
         905  +
/// retain predicate consults and `SingletonConnection` mutates.
         906  +
///
         907  +
/// On re-handshake (poisoning, GOAWAY, etc.), the entire state is
         908  +
/// replaced last-writer-wins. A `SingletonConnection` constructed against
         909  +
/// the previous handshake holds its own snapshot, so its
         910  +
/// increments/decrements still target the right counter.
         911  +
#[derive(Clone, Default)]
         912  +
pub(crate) struct H2ConnectionRef {
         913  +
    inner: Arc<std::sync::Mutex<Option<H2ConnectionState>>>,
         914  +
}
         915  +
         916  +
impl H2ConnectionRef {
         917  +
    pub(crate) fn new() -> Self {
         918  +
        Self::default()
         919  +
    }
         920  +
         921  +
    /// Replace the current state with a freshly-handshaked connection's
         922  +
    /// state (last-writer-wins).
         923  +
    pub(crate) fn publish(&self, state: H2ConnectionState) {
         924  +
        *self.inner.lock().unwrap() = Some(state);
         925  +
    }
         926  +
         927  +
    /// Snapshot of the published state. `None` until the first
         928  +
    /// successful handshake for this host.
         929  +
    pub(crate) fn current(&self) -> Option<H2ConnectionState> {
         930  +
        self.inner.lock().unwrap().clone()
         931  +
    }
         932  +
}
         933  +
         934  +
/// RAII guard for one in-flight dispatch against an H2 connection.
         935  +
///
         936  +
/// Existence reflects "this checkout has dispatched a request; the
         937  +
/// connection is busy on its behalf." Constructed by
         938  +
/// [`SingletonConnection::call`] when a request is dispatched; dropped
         939  +
/// when the response body's guard releases.
         940  +
///
         941  +
/// Drop decrements `active_streams` and, on the 1 → 0 transition, stamps
         942  +
/// `idle_at` (the moment the connection becomes truly idle).
         943  +
struct DispatchGuard {
         944  +
    active_streams: Arc<std::sync::atomic::AtomicUsize>,
         945  +
    idle_at: Arc<Mutex<Instant>>,
         946  +
    counters: Arc<super::stats::ConnectionCounters>,
         947  +
}
         948  +
         949  +
impl DispatchGuard {
         950  +
    /// Start a dispatch against the connection described by `state`.
         951  +
    /// Increments `active_streams`; the returned guard releases the
         952  +
    /// increment on drop.
         953  +
    fn start(state: &H2ConnectionState, counters: Arc<super::stats::ConnectionCounters>) -> Self {
         954  +
        state
         955  +
            .active_streams
         956  +
            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
         957  +
        counters.incr_active();
         958  +
        Self {
         959  +
            active_streams: state.active_streams.clone(),
         960  +
            idle_at: state.idle_at.clone(),
         961  +
            counters,
         962  +
        }
         963  +
    }
         964  +
}
         965  +
         966  +
impl Drop for DispatchGuard {
         967  +
    fn drop(&mut self) {
         968  +
        self.counters.decr_active();
         969  +
        let prev = self
         970  +
            .active_streams
         971  +
            .fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
         972  +
        if prev == 1 {
         973  +
            if let Ok(mut idle_at) = self.idle_at.lock() {
         974  +
                *idle_at = Instant::now();
         975  +
            }
         976  +
        }
         977  +
    }
         978  +
}
         979  +
         980  +
/// A checked-out H2 connection ready to dispatch requests.
         981  +
///
         982  +
/// H2 is multiplexed: the underlying `ManagedConnection` stays resident in
         983  +
/// `Singleton` for the duration of every concurrent request, so "presence
         984  +
/// in a cache" (the idleness signal we use for H1) doesn't apply.
         985  +
/// Instead, `SingletonConnection` mints a [`DispatchGuard`] on each
         986  +
/// `call`; the guard's lifetime tracks one in-flight request and its
         987  +
/// drop releases the corresponding `active_streams` slot, stamping
         988  +
/// `idle_at` on the final release. The H2 retain predicate keeps the
         989  +
/// connection alive while `active_streams > 0` regardless of `idle_at`.
         990  +
///
         991  +
/// Lifecycle:
         992  +
/// - Constructed in the H2 upgrade `map_response` after `Singleton::call`
         993  +
///   completes. Snapshots the current [`H2ConnectionState`] and holds it
         994  +
///   for the checkout's lifetime.
         995  +
/// - [`Self::call`] starts a [`DispatchGuard`] before delegating to
         996  +
///   `Singled::call`. The guard is held in `dispatch` for the rest of
         997  +
///   this `SingletonConnection`'s lifetime.
         998  +
/// - On drop, the guard's drop releases the active-stream count and may
         999  +
///   stamp `idle_at`. If `call` was never invoked (e.g. the surrounding
        1000  +
///   checkout was abandoned by the post-checkout `poll_ready` retry
        1001  +
///   loop), `dispatch` is `None` and drop is a no-op.
        1002  +
pub(crate) struct SingletonConnection<T> {
        1003  +
    inner: T,
        1004  +
    /// Snapshot of the H2 side-channel state taken at construction.
        1005  +
    ///
        1006  +
    /// Held for the lifetime of this checkout so dispatch guards always
        1007  +
    /// target the connection this checkout was issued against, even if a
        1008  +
    /// re-handshake replaces the underlying `H2ConnectionRef`'s published
        1009  +
    /// state in the meantime.
        1010  +
    state: Option<H2ConnectionState>,
        1011  +
    /// Active-stream guard. `Some` between [`Self::call`] and drop.
        1012  +
    /// `None` if `call` was never invoked or before `call` runs.
        1013  +
    dispatch: Option<DispatchGuard>,
        1014  +
    /// Pool-level warmth counters for this (partition, authority) cell.
        1015  +
    counters: Arc<super::stats::ConnectionCounters>,
        1016  +
}
        1017  +
        1018  +
impl<T> SingletonConnection<T> {
        1019  +
    pub(crate) fn new(
        1020  +
        inner: T,
        1021  +
        h2_ref: H2ConnectionRef,
        1022  +
        counters: Arc<super::stats::ConnectionCounters>,
        1023  +
    ) -> Self {
        1024  +
        let state = h2_ref.current();
        1025  +
        Self {
        1026  +
            inner,
        1027  +
            state,
        1028  +
            dispatch: None,
        1029  +
            counters,
        1030  +
        }
        1031  +
    }
        1032  +
        1033  +
    /// Metadata for the H2 connection this checkout was issued against.
        1034  +
    ///
        1035  +
    /// Returns `None` only if `SingletonConnection` was constructed before
        1036  +
    /// any handshake had published (which doesn't happen in normal flow,
        1037  +
    /// since `Singleton::call` always completes a handshake before
        1038  +
    /// yielding the `Singled` we wrap).
        1039  +
    pub(crate) fn metadata(&self) -> Option<ConnectionMetadata> {
        1040  +
        self.state.as_ref().map(|s| s.metadata.clone())
        1041  +
    }
        1042  +
}
        1043  +
        1044  +
impl<T, Req> Service<Req> for SingletonConnection<T>
        1045  +
where
        1046  +
    T: Service<Req>,
        1047  +
{
        1048  +
    type Response = T::Response;
        1049  +
    type Error = T::Error;
        1050  +
    type Future = T::Future;
        1051  +
        1052  +
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        1053  +
        self.inner.poll_ready(cx)
        1054  +
    }
        1055  +
        1056  +
    fn call(&mut self, req: Req) -> Self::Future {
        1057  +
        if let Some(state) = self.state.as_ref() {
        1058  +
            self.dispatch = Some(DispatchGuard::start(state, self.counters.clone()));
        1059  +
        }
        1060  +
        self.inner.call(req)
        1061  +
    }
        1062  +
}
        1063  +
        1064  +
pin_project! {
        1065  +
    /// A response body that keeps the originating pool checkout alive until
        1066  +
    /// the body is fully consumed.
        1067  +
    ///
        1068  +
    /// # Why this exists
        1069  +
    ///
        1070  +
    /// When a checked-out pool connection's `Service::call` returns, the
        1071  +
    /// response head is available but the body may still be streaming over
        1072  +
    /// the same underlying HTTP connection. For H1 this is critical: if the
        1073  +
    /// `CachedConnection` drops at that point, the connection returns to the
        1074  +
    /// pool mid-body-stream and the next checkout would be handed a still-busy
        1075  +
    /// connection.
        1076  +
    ///
        1077  +
    /// `GuardedBody` holds the checkout (`CachedConnection` for H1,
        1078  +
    /// `SingletonConnection` for H2) in its guard field until the body is
        1079  +
    /// fully dropped. Body streaming continues through the held inner
        1080  +
    /// `Incoming`; when the `GuardedBody` is dropped the guard drops, which
        1081  +
    /// for H1 triggers `CachedConnection::Drop` (return-to-pool or `discard`
        1082  +
    /// if poisoned), and for H2 releases the stream's `DispatchGuard`
        1083  +
    /// (decrementing `active_streams`, and stamping `idle_at` on the final
        1084  +
    /// release).
        1085  +
    ///
        1086  +
    /// The H2 variant carries a generic type parameter because
        1087  +
    /// `SingletonConnection<T>`'s inner `T` is `hyper_util::client::pool::
        1088  +
    /// singleton::Singled<...>`, which is unnameable outside hyper-util.
        1089  +
    /// The H1 variant is fully concrete because `cache::Cached<...>` is
        1090  +
    /// nameable.
        1091  +
    pub(crate) struct GuardedBody<H2Unnameable> {
        1092  +
        #[pin]
        1093  +
        inner: hyper::body::Incoming,
        1094  +
        _guard: ConnectionGuard<H2Unnameable>,
        1095  +
    }
        1096  +
}
        1097  +
        1098  +
/// What a `GuardedBody` holds alive while the body streams.
        1099  +
///
        1100  +
/// Explicit per-leg variants so H1 vs H2 bifurcation is visible at the
        1101  +
/// type level and in debugger output.
        1102  +
pub(crate) enum ConnectionGuard<H2Unnameable> {
        1103  +
    H1(CachedConnection<H1SendRequest>),
        1104  +
    H2(SingletonConnection<H2Unnameable>),
        1105  +
}
        1106  +
        1107  +
impl<H2Unnameable> GuardedBody<H2Unnameable> {
        1108  +
    pub(crate) fn new(inner: hyper::body::Incoming, guard: ConnectionGuard<H2Unnameable>) -> Self {
        1109  +
        Self {
        1110  +
            inner,
        1111  +
            _guard: guard,
        1112  +
        }
        1113  +
    }
        1114  +
}
        1115  +
        1116  +
impl<H2Unnameable> hyper::body::Body for GuardedBody<H2Unnameable> {
        1117  +
    type Data = <hyper::body::Incoming as hyper::body::Body>::Data;
        1118  +
    type Error = <hyper::body::Incoming as hyper::body::Body>::Error;
        1119  +
        1120  +
    fn poll_frame(
        1121  +
        self: std::pin::Pin<&mut Self>,
        1122  +
        cx: &mut Context<'_>,
        1123  +
    ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
        1124  +
        self.project().inner.poll_frame(cx)
        1125  +
    }
        1126  +
        1127  +
    fn is_end_stream(&self) -> bool {
        1128  +
        self.inner.is_end_stream()
        1129  +
    }
        1130  +
        1131  +
    fn size_hint(&self) -> hyper::body::SizeHint {
        1132  +
        self.inner.size_hint()
        1133  +
    }
        1134  +
}
        1135  +
        1136  +
/// The response type both H1 and H2 pool checkouts produce.
        1137  +
///
        1138  +
/// Carries `GuardedBody<PoolUnnameable>` so the H2 leg can hold its
        1139  +
/// checkout guard (`Singled<…>`, type-erased through `PoolUnnameable`)
        1140  +
/// for the response body's lifetime. `Negotiate` requires uniform
        1141  +
/// response types across its legs; this alias is the uniform type that
        1142  +
/// consumers above the Negotiate composition point work with.
        1143  +
pub(crate) type CheckoutResponse<PoolUnnameable> = http_1x::Response<GuardedBody<PoolUnnameable>>;
        1144  +
        1145  +
/// Wire-level IO wrapper sitting below TLS in the connector stack. Pure
        1146  +
/// passthrough; never modifies data or buffers.
        1147  +
///
        1148  +
/// TODO(pool): instrument TCP-connect vs. TLS-handshake timing separately
        1149  +
/// here (this wrapper is the seam below TLS where the TCP-only duration is
        1150  +
/// observable); surface the split on `ConnectionTiming`.
        1151  +
pub(crate) struct TransportIo<IO> {
        1152  +
    inner: IO,
        1153  +
}
        1154  +
        1155  +
impl<IO> TransportIo<IO> {
        1156  +
    fn new(inner: IO) -> Self {
        1157  +
        Self { inner }
        1158  +
    }
        1159  +
}
        1160  +
        1161  +
impl<IO: Unpin> Unpin for TransportIo<IO> {}
        1162  +
        1163  +
impl<IO: hyper::rt::Read + Unpin> hyper::rt::Read for TransportIo<IO> {
        1164  +
    fn poll_read(
        1165  +
        mut self: std::pin::Pin<&mut Self>,
        1166  +
        cx: &mut std::task::Context<'_>,
        1167  +
        buf: hyper::rt::ReadBufCursor<'_>,
        1168  +
    ) -> Poll<std::io::Result<()>> {
        1169  +
        std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
        1170  +
    }
        1171  +
}
        1172  +
        1173  +
impl<IO: hyper::rt::Write + Unpin> hyper::rt::Write for TransportIo<IO> {
        1174  +
    fn poll_write(
        1175  +
        mut self: std::pin::Pin<&mut Self>,
        1176  +
        cx: &mut std::task::Context<'_>,
        1177  +
        buf: &[u8],
        1178  +
    ) -> Poll<std::io::Result<usize>> {
        1179  +
        std::pin::Pin::new(&mut self.inner).poll_write(cx, buf)
        1180  +
    }
        1181  +
        1182  +
    fn poll_flush(
        1183  +
        mut self: std::pin::Pin<&mut Self>,
        1184  +
        cx: &mut std::task::Context<'_>,
        1185  +
    ) -> Poll<std::io::Result<()>> {
        1186  +
        std::pin::Pin::new(&mut self.inner).poll_flush(cx)
        1187  +
    }
        1188  +
        1189  +
    fn poll_shutdown(
        1190  +
        mut self: std::pin::Pin<&mut Self>,
        1191  +
        cx: &mut std::task::Context<'_>,
        1192  +
    ) -> Poll<std::io::Result<()>> {
        1193  +
        std::pin::Pin::new(&mut self.inner).poll_shutdown(cx)
        1194  +
    }
        1195  +
}
        1196  +
        1197  +
impl<IO: hyper_util::client::legacy::connect::Connection>
        1198  +
    hyper_util::client::legacy::connect::Connection for TransportIo<IO>
        1199  +
{
        1200  +
    fn connected(&self) -> hyper_util::client::legacy::connect::Connected {
        1201  +
        self.inner.connected()
        1202  +
    }
        1203  +
}
        1204  +
        1205  +
/// Wraps a TCP connector with the [`TransportIo`] seam below the TLS layer.
        1206  +
///
        1207  +
/// The wrapper is the point at which transport-level (TCP) timing and byte
        1208  +
/// accounting can be observed independently of the TLS handshake above it.
        1209  +
pub(crate) struct TimingConnector<C> {
        1210  +
    inner: C,
        1211  +
}
        1212  +
        1213  +
impl<C> TimingConnector<C> {
        1214  +
    pub(crate) fn new(inner: C) -> Self {
        1215  +
        Self { inner }
        1216  +
    }
        1217  +
}
        1218  +
        1219  +
impl<C: Clone> Clone for TimingConnector<C> {
        1220  +
    fn clone(&self) -> Self {
        1221  +
        Self {
        1222  +
            inner: self.inner.clone(),
        1223  +
        }
        1224  +
    }
        1225  +
}
        1226  +
        1227  +
impl<C, IO> tower::Service<http_1x::Uri> for TimingConnector<C>
        1228  +
where
        1229  +
    C: tower::Service<http_1x::Uri, Response = IO>,
        1230  +
    C::Error: Into<BoxError>,
        1231  +
    C::Future: Send + 'static,
        1232  +
    IO: Send + 'static,
        1233  +
{
        1234  +
    type Response = TransportIo<IO>;
        1235  +
    type Error = BoxError;
        1236  +
    type Future = std::pin::Pin<
        1237  +
        Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
        1238  +
    >;
        1239  +
        1240  +
    fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
        1241  +
        self.inner.poll_ready(cx).map_err(Into::into)
        1242  +
    }
        1243  +
        1244  +
    fn call(&mut self, uri: http_1x::Uri) -> Self::Future {
        1245  +
        let fut = self.inner.call(uri);
        1246  +
        Box::pin(async move {
        1247  +
            let io = fut.await.map_err(Into::into)?;
        1248  +
            Ok(TransportIo::new(io))
        1249  +
        })
        1250  +
    }
        1251  +
}
        1252  +
        1253  +
#[cfg(test)]
        1254  +
mod tests {
        1255  +
    //! Unit tests for H2 active-stream tracking.
        1256  +
    //!
        1257  +
    //! The test harness (`ConnectionTestHarness`) is plain HTTP only, without
        1258  +
    //! ALPN, so the full H2 pipeline is not exercised end-to-end here. These
        1259  +
    //! tests verify the atomic transitions directly, which is the
        1260  +
    //! architectural correctness property we need: an in-flight H2 connection
        1261  +
    //! must not be evicted by the retain predicate, and the connection's
        1262  +
    //! `idle_at` must be stamped only on the `active_streams` 1 → 0
        1263  +
    //! transition.
        1264  +
    use super::*;
        1265  +
    use std::future::Future;
        1266  +
    use std::sync::atomic::{AtomicUsize, Ordering};
        1267  +
    use tower::Service as _;
        1268  +
        1269  +
    /// A minimal `Service<()>` that returns `Ok(())` (stand-in for
        1270  +
    /// `Singled::call`). We only care about the wrapper's stream-count
        1271  +
    /// side effects.
        1272  +
    #[derive(Clone, Default)]
        1273  +
    struct OkService;
        1274  +
    impl Service<()> for OkService {
        1275  +
        type Response = ();
        1276  +
        type Error = BoxError;
        1277  +
        type Future = std::pin::Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>;
        1278  +
        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        1279  +
            Poll::Ready(Ok(()))
        1280  +
        }
        1281  +
        fn call(&mut self, _: ()) -> Self::Future {
        1282  +
            Box::pin(async { Ok(()) })
        1283  +
        }
        1284  +
    }
        1285  +
        1286  +
    fn publish_state(h2_ref: &H2ConnectionRef) -> (Arc<AtomicUsize>, Arc<Mutex<Instant>>) {
        1287  +
        use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
        1288  +
        let active = Arc::new(AtomicUsize::new(0));
        1289  +
        let idle_at = Arc::new(Mutex::new(Instant::now()));
        1290  +
        h2_ref.publish(H2ConnectionState {
        1291  +
            metadata: ConnectionMetadata::builder()
        1292  +
                .proxied(false)
        1293  +
                .poison_fn(|| {})
        1294  +
                .build(),
        1295  +
            active_streams: active.clone(),
        1296  +
            idle_at: idle_at.clone(),
        1297  +
        });
        1298  +
        (active, idle_at)
        1299  +
    }
        1300  +
        1301  +
    /// A round-trip through `SingletonConnection::call` mints a
        1302  +
    /// `DispatchGuard` that releases on drop. Counter goes 0 → 1 → 0.
        1303  +
    #[tokio::test]
        1304  +
    async fn singleton_dispatch_round_trip_is_net_zero() {
        1305  +
        let h2_ref = H2ConnectionRef::new();
        1306  +
        let (active, _) = publish_state(&h2_ref);
        1307  +
        assert_eq!(active.load(Ordering::Acquire), 0);
        1308  +
        1309  +
        let counters = Arc::new(super::super::stats::ConnectionCounters::default());
        1310  +
        let mut sc = SingletonConnection::new(OkService, h2_ref, counters);
        1311  +
        let _ = sc.call(()).await;
        1312  +
        assert_eq!(
        1313  +
            active.load(Ordering::Acquire),
        1314  +
            1,
        1315  +
            "call should mint a DispatchGuard, incrementing active_streams"
        1316  +
        );
        1317  +
        drop(sc);
        1318  +
        assert_eq!(
        1319  +
            active.load(Ordering::Acquire),
        1320  +
            0,
        1321  +
            "DispatchGuard's Drop should release the active_streams slot"
        1322  +
        );
        1323  +
    }
        1324  +
        1325  +
    /// Constructing without dispatching leaves no `DispatchGuard`, so
        1326  +
    /// dropping is a no-op. The post-checkout `poll_ready` retry loop
        1327  +
    /// relies on this: a checkout discarded before `call` must not
        1328  +
    /// underflow the counter.
        1329  +
    #[tokio::test]
        1330  +
    async fn singleton_drop_without_dispatch_is_noop() {
        1331  +
        let h2_ref = H2ConnectionRef::new();
        1332  +
        let (active, _) = publish_state(&h2_ref);
        1333  +
        active.store(5, Ordering::Release);
        1334  +
        1335  +
        let counters = Arc::new(super::super::stats::ConnectionCounters::default());
        1336  +
        let sc = SingletonConnection::new(OkService, h2_ref, counters);
        1337  +
        drop(sc);
        1338  +
        assert_eq!(
        1339  +
            active.load(Ordering::Acquire),
        1340  +
            5,
        1341  +
            "uncalled SingletonConnection must not release a DispatchGuard"
        1342  +
        );
        1343  +
    }
        1344  +
        1345  +
    /// Concurrent dispatches against the same H2 connection (simulating
        1346  +
    /// multiplexed requests) all land on the same counter. `idle_at` is
        1347  +
    /// stamped only on the LAST `DispatchGuard` drop (the 1 → 0
        1348  +
    /// transition).
        1349  +
    #[tokio::test]
        1350  +
    async fn idle_at_stamped_only_on_last_dispatch_release() {
        1351  +
        let h2_ref = H2ConnectionRef::new();
        1352  +
        let (active, idle_at) = publish_state(&h2_ref);
        1353  +
        1354  +
        // Capture the stamp from construction to detect updates.
        1355  +
        let original_idle = *idle_at.lock().unwrap();
        1356  +
        1357  +
        let mut a = SingletonConnection::new(
        1358  +
            OkService,
        1359  +
            h2_ref.clone(),
        1360  +
            Arc::new(super::super::stats::ConnectionCounters::default()),
        1361  +
        );
        1362  +
        let mut b = SingletonConnection::new(
        1363  +
            OkService,
        1364  +
            h2_ref.clone(),
        1365  +
            Arc::new(super::super::stats::ConnectionCounters::default()),
        1366  +
        );
        1367  +
        let mut c = SingletonConnection::new(
        1368  +
            OkService,
        1369  +
            h2_ref,
        1370  +
            Arc::new(super::super::stats::ConnectionCounters::default()),
        1371  +
        );
        1372  +
        1373  +
        let _ = a.call(()).await;
        1374  +
        let _ = b.call(()).await;
        1375  +
        let _ = c.call(()).await;
        1376  +
        assert_eq!(active.load(Ordering::Acquire), 3);
        1377  +
        1378  +
        // Sleep so any stamp is distinguishable from construction time.
        1379  +
        tokio::time::sleep(std::time::Duration::from_millis(5)).await;
        1380  +
        1381  +
        // Release two; idle_at must NOT yet be stamped (counter > 0).
        1382  +
        drop(a);
        1383  +
        drop(b);
        1384  +
        assert_eq!(active.load(Ordering::Acquire), 1);
        1385  +
        assert_eq!(
        1386  +
            *idle_at.lock().unwrap(),
        1387  +
            original_idle,
        1388  +
            "idle_at must not be stamped while dispatches are in flight"
        1389  +
        );
        1390  +
        1391  +
        // Last release: 1 → 0 transition. idle_at must be stamped.
        1392  +
        drop(c);
        1393  +
        assert_eq!(active.load(Ordering::Acquire), 0);
        1394  +
        let final_idle = *idle_at.lock().unwrap();
        1395  +
        assert!(
        1396  +
            final_idle > original_idle,
        1397  +
            "idle_at should be stamped when the last DispatchGuard releases"
        1398  +
        );
        1399  +
    }
        1400  +
}