AWS SDK

AWS SDK

rev. 214b1cfffb5029555908aba78574468110a9ad23 (ignoring whitespace)

Files changed:

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

@@ -581,581 +640,652 @@
  601    601   
            ConnectionId::new(id),
  602    602   
            OriginKey::from_parts(Scheme::HTTP, "example.com", None).unwrap(),
  603    603   
            PartitionId::from_index(1),
  604    604   
            NegotiatedProtocol::Http1,
  605    605   
            hyper_util::client::legacy::connect::Connected::new(),
  606    606   
        );
  607    607   
        let (connection, _physical) = ConnectionState::unbounded(info);
  608    608   
        connection
  609    609   
    }
  610    610   
         611  +
    fn h2_connection(id: u64) -> Arc<ConnectionState> {
         612  +
        let info = ConnectionInfo::new(
         613  +
            ConnectionId::new(id),
         614  +
            OriginKey::from_parts(Scheme::HTTP, "example.com", None).unwrap(),
         615  +
            PartitionId::from_index(1),
         616  +
            NegotiatedProtocol::Http2,
         617  +
            hyper_util::client::legacy::connect::Connected::new(),
         618  +
        );
         619  +
        let (connection, _physical) = ConnectionState::unbounded(info);
         620  +
        connection
         621  +
    }
         622  +
  611    623   
    #[derive(Clone, Debug)]
  612    624   
    struct TrackingSpawner {
  613    625   
        submitted: Arc<AtomicU64>,
  614    626   
        active: Arc<AtomicU64>,
  615    627   
    }
  616    628   
  617    629   
    impl TrackingSpawner {
  618    630   
        fn new() -> Self {
  619    631   
            Self {
  620    632   
                submitted: Arc::new(AtomicU64::new(0)),
@@ -703,715 +762,827 @@
  723    735   
            if connection.snapshot().close_reason == Some(super::super::CloseReason::IdleTimeout) {
  724    736   
                break;
  725    737   
            }
  726    738   
            tokio::task::yield_now().await;
  727    739   
        }
  728    740   
        assert_eq!(
  729    741   
            Some(super::super::CloseReason::IdleTimeout),
  730    742   
            connection.snapshot().close_reason
  731    743   
        );
  732    744   
    }
         745  +
    #[tokio::test]
         746  +
    async fn h2_generation_closes_at_its_fake_time_deadline() {
         747  +
        let timeout = Duration::from_secs(10);
         748  +
        let (maintenance, cell, mut gate) = managed_cell(timeout);
         749  +
        let connection = h2_connection(1);
         750  +
        OriginCell::install_h2_for_test(&cell, connection.clone(), 1, maintenance.idle_deadline());
         751  +
        PartitionMaintenance::start(&maintenance, &TokioDriverSpawner::current());
         752  +
         753  +
        let sleep = gate.expect_sleep().await;
         754  +
        assert_eq!(timeout, sleep.duration());
         755  +
        assert_eq!(None, connection.snapshot().close_reason);
         756  +
        sleep.allow_progress();
         757  +
         758  +
        for _ in 0..10 {
         759  +
            if connection.snapshot().close_reason == Some(super::super::CloseReason::IdleTimeout) {
         760  +
                break;
         761  +
            }
         762  +
            tokio::task::yield_now().await;
         763  +
        }
         764  +
        assert_eq!(
         765  +
            Some(super::super::CloseReason::IdleTimeout),
         766  +
            connection.snapshot().close_reason
         767  +
        );
         768  +
        assert_eq!(None, cell.accepting_h2_generation());
         769  +
    }
         770  +
         771  +
    #[test]
         772  +
    fn accepted_h2_dispatch_resets_the_idle_deadline() {
         773  +
        let timeout = Duration::from_secs(10);
         774  +
        let time = RewindableTimeSource::new(0);
         775  +
        let (_unused_time, sleep, _gate) = controlled_time_and_sleep(UNIX_EPOCH);
         776  +
        let (maintenance, cell) = cell_with_maintenance(
         777  +
            timeout,
         778  +
            SharedTimeSource::new(time.clone()),
         779  +
            SharedAsyncSleep::new(sleep),
         780  +
        );
         781  +
        let connection = h2_connection(1);
         782  +
        OriginCell::install_h2_for_test(&cell, connection.clone(), 1, maintenance.idle_deadline());
         783  +
        assert_eq!(Some(UNIX_EPOCH + timeout), cell.nearest_idle_deadline());
         784  +
         785  +
        time.set(20);
         786  +
        let mut activation =
         787  +
            OriginCell::select_h2(&cell).expect("HTTP/2 generation was not selectable");
         788  +
        let _dispatch_parts = activation.take_dispatch_parts();
         789  +
        let dispatch = ConnectionState::try_commit_dispatch(&connection)
         790  +
            .expect("HTTP/2 connection rejected dispatch");
         791  +
        activation.accept(dispatch);
         792  +
         793  +
        assert_eq!(
         794  +
            Some(UNIX_EPOCH + Duration::from_secs(20) + timeout),
         795  +
            cell.nearest_idle_deadline()
         796  +
        );
         797  +
    }
  733    798   
  734    799   
    #[tokio::test]
  735    800   
    async fn selected_record_has_no_idle_deadline() {
  736    801   
        let timeout = Duration::from_secs(10);
  737    802   
        let (maintenance, cell, mut gate) = managed_cell(timeout);
  738    803   
        let connection = connection(1);
  739    804   
        OriginCell::install_idle_h1(&cell, connection.clone(), H1Sender::test(1));
  740    805   
        PartitionMaintenance::start(&maintenance, &TokioDriverSpawner::current());
  741    806   
  742    807   
        let sleep = gate.expect_sleep().await;

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

@@ -78,78 +138,138 @@
   98     98   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
   99     99   
        f.debug_struct("Partition")
  100    100   
            .field("id", &self.id)
  101    101   
            .field("spawner", &self.spawner)
  102    102   
            .field("interface", &self.interface)
  103    103   
            .finish()
  104    104   
    }
  105    105   
}
  106    106   
  107    107   
/// Identifies one declared connection partition.
  108         -
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
         108  +
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
  109    109   
pub struct PartitionId(usize);
  110    110   
  111    111   
impl PartitionId {
  112    112   
    /// Reserved identity for the implicit default partition.
  113    113   
    pub const ANONYMOUS: Self = Self(usize::MAX);
  114    114   
  115    115   
    /// Creates a partition identity from a caller-owned index.
  116    116   
    ///
  117    117   
    /// `usize::MAX` is reserved for [`Self::ANONYMOUS`] and is rejected when
  118    118   
    /// used in an explicit partition declaration.
@@ -155,155 +216,221 @@
  175    175   
pub enum ConnectionReuseScope {
  176    176   
    /// A connection serves only requests from its owning partition.
  177    177   
    Partition,
  178    178   
    /// Partitions with the same interface binding may share connections.
  179    179   
    #[default]
  180    180   
    NetworkInterface,
  181    181   
    /// Every partition in the pool may share connections.
  182    182   
    Pool,
  183    183   
}
  184    184   
  185         -
/// Identifies the exact set of partitions eligible to reuse a connection.
  186         -
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
         185  +
/// Identifies the exact set of partitions allowed to share a connection.
         186  +
///
         187  +
/// The configured [`ConnectionReuseScope`] maps each partition to one group:
         188  +
/// partition-local scope creates a group for one partition, network-interface
         189  +
/// scope groups equal interface bindings, and pool scope creates one group for
         190  +
/// the whole pool.
         191  +
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
  187    192   
pub(crate) enum EligibilityGroup {
  188    193   
    /// Only the partition with this identity is eligible.
  189    194   
    Partition(PartitionId),
  190    195   
    /// Partitions with this exact interface binding are eligible.
  191    196   
    NetworkInterface(Option<Arc<str>>),
  192    197   
    /// Every partition in the pool is eligible.
  193    198   
    Pool,
  194    199   
}
  195    200   
  196    201   
#[cfg(test)]

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

@@ -10,10 +196,203 @@
   30     30   
   31     31   
/// Fixed partition set and the origin-wide admission states it shares.
   32     32   
#[derive(Debug)]
   33     33   
pub(crate) struct PartitionRegistry {
   34     34   
    /// Immutable partition identities resolved when a client is built.
   35     35   
    partitions: HashMap<PartitionId, Arc<PartitionState>>,
   36     36   
    /// Policy used to derive a new cell's eligibility group.
   37     37   
    reuse_scope: ConnectionReuseScope,
   38     38   
    /// Optional origin-wide bound used when admission is first created.
   39     39   
    max_connections_per_host: Option<NonZeroUsize>,
          40  +
    /// Whether admission may close idle H2 capacity for H1-required demand.
          41  +
    allow_h2_reclaim_for_h1: bool,
   40     42   
    /// Admission authorities retained by canonical origin.
   41     43   
    bounded_origins: Mutex<HashMap<OriginKey, Arc<OriginAdmission>>>,
   42     44   
}
   43     45   
   44     46   
impl PartitionRegistry {
   45     47   
    /// Creates the anonymous partition or validates and retains explicit partitions.
   46     48   
    pub(crate) fn new(
   47     49   
        partitions: Option<Vec<Partition>>,
   48     50   
        reuse_scope: ConnectionReuseScope,
   49     51   
        max_connections_per_host: Option<NonZeroUsize>,
          52  +
        allow_h2_reclaim_for_h1: bool,
   50     53   
        maintenance: MaintenanceConfig,
   51     54   
    ) -> Result<Self, PartitionRegistryError> {
   52     55   
        let Some(partitions) = partitions else {
   53     56   
            let partition = Arc::new(PartitionState::anonymous(maintenance));
   54     57   
            let mut partitions = HashMap::new();
   55     58   
            partitions.insert(PartitionId::ANONYMOUS, partition);
   56     59   
            return Ok(Self {
   57     60   
                partitions,
   58     61   
                reuse_scope,
   59     62   
                max_connections_per_host,
          63  +
                allow_h2_reclaim_for_h1,
   60     64   
                bounded_origins: Mutex::new(HashMap::new()),
   61     65   
            });
   62     66   
        };
   63     67   
   64     68   
        let mut by_id = HashMap::new();
   65     69   
        for partition in partitions {
   66     70   
            let id = partition.id();
   67     71   
            if id.is_anonymous() {
   68     72   
                return Err(PartitionRegistryError::ReservedAnonymousPartition);
   69     73   
            }
   70     74   
            if by_id
   71     75   
                .insert(
   72     76   
                    id,
   73     77   
                    Arc::new(PartitionState::explicit(partition, maintenance.clone())),
   74     78   
                )
   75     79   
                .is_some()
   76     80   
            {
   77     81   
                return Err(PartitionRegistryError::DuplicatePartition(id));
   78     82   
            }
   79     83   
        }
   80     84   
   81     85   
        if by_id.is_empty() {
   82     86   
            return Err(PartitionRegistryError::EmptyExplicitPartitionSet);
   83     87   
        }
   84     88   
   85     89   
        Ok(Self {
   86     90   
            partitions: by_id,
   87     91   
            reuse_scope,
   88     92   
            max_connections_per_host,
          93  +
            allow_h2_reclaim_for_h1,
   89     94   
            bounded_origins: Mutex::new(HashMap::new()),
   90     95   
        })
   91     96   
    }
   92     97   
   93     98   
    /// Resolves a retained partition once during client construction.
   94     99   
    pub(crate) fn partition(&self, id: PartitionId) -> Option<Arc<PartitionState>> {
   95    100   
        self.partitions.get(&id).cloned()
   96    101   
    }
   97    102   
   98    103   
    /// Resolves the stable cell for an already-resolved partition and URI.
   99    104   
    pub(crate) fn resolve_cell(
  100    105   
        &self,
  101    106   
        partition: &PartitionState,
  102    107   
        uri: &Uri,
  103    108   
    ) -> Result<Arc<OriginCell>, InvalidOrigin> {
  104    109   
        let lookup = OriginLookup::from_uri(uri)?;
  105    110   
        if let Some(cell) = partition.find_cell(&lookup) {
  106    111   
            return Ok(cell);
  107    112   
        }
  108    113   
  109    114   
        let eligibility_group = self.eligibility_group(partition);
  110    115   
        let origin = lookup.into_owned();
  111    116   
        let admission = self.origin_admission(&origin);
  112    117   
        let candidate = Arc::new(OriginCell::new(
  113    118   
            partition.id(),
  114    119   
            origin,
  115    120   
            eligibility_group,
  116    121   
            admission.clone(),
  117    122   
            Some(partition.maintenance.clone()),
  118    123   
        ));
  119    124   
        let cell = match admission {
  120    125   
            Some(admission) => OriginAdmission::register_cell(&admission, candidate),
  121    126   
            None => candidate,
  122    127   
        };
  123    128   
        let cell = partition.publish_cell(cell);
  124    129   
        partition.maintenance.register(&cell);
  125    130   
        Ok(cell)
  126    131   
    }
  127    132   
  128    133   
    /// Returns the shared admission authority for a bounded origin.
  129    134   
    ///
  130    135   
    /// Unbounded registries return `None`. Concurrent first use of a bounded
  131    136   
    /// origin converges on one retained authority.
  132    137   
    fn origin_admission(&self, origin: &OriginKey) -> Option<Arc<OriginAdmission>> {
  133    138   
        let limit = self.max_connections_per_host?;
  134    139   
        let admission = {
  135    140   
            let mut origins = self.bounded_origins.lock();
  136    141   
            origins
  137    142   
                .entry(origin.clone())
  138         -
                .or_insert_with(|| OriginAdmission::new(origin.clone(), limit))
         143  +
                .or_insert_with(|| {
         144  +
                    OriginAdmission::new(origin.clone(), limit, self.allow_h2_reclaim_for_h1)
         145  +
                })
  139    146   
                .clone()
  140    147   
        };
  141    148   
        Some(admission)
  142    149   
    }
  143    150   
  144    151   
    /// Derives which partitions may relieve a new cell's bounded demand.
  145    152   
    fn eligibility_group(&self, partition: &PartitionState) -> EligibilityGroup {
  146    153   
        match self.reuse_scope {
  147    154   
            ConnectionReuseScope::Partition => EligibilityGroup::Partition(partition.id()),
  148    155   
            ConnectionReuseScope::NetworkInterface => {
  149    156   
                EligibilityGroup::NetworkInterface(partition.interface().cloned())
  150    157   
            }
  151    158   
            ConnectionReuseScope::Pool => EligibilityGroup::Pool,
  152    159   
        }
  153    160   
    }
  154    161   
  155    162   
    /// Stops partition maintenance and logically closes every connection.
  156    163   
    pub(crate) fn close_all(&self, reason: CloseReason) {
  157    164   
        for partition in self.partitions.values() {
  158    165   
            partition.shutdown_maintenance();
  159    166   
        }
  160    167   
        let cells = self
  161    168   
            .partitions
  162    169   
            .values()
  163    170   
            .flat_map(|partition| partition.cells())
  164    171   
            .collect::<Vec<_>>();
  165    172   
        for cell in cells {
  166         -
            OriginCell::close_all_h1(&cell, reason);
         173  +
            OriginCell::close_all(&cell, reason);
  167    174   
        }
  168    175   
    }
  169    176   
}
  170    177   
  171    178   
/// Runtime placement and retained origin cells for one partition.
  172    179   
#[derive(Debug)]
  173    180   
pub(crate) struct PartitionState {
  174    181   
    /// Stable identity copied into every cell and owned connection.
  175    182   
    id: PartitionId,
  176    183   
    /// Configured spawner, or the first spawner published for the anonymous
@@ -398,405 +471,480 @@
  418    425   
  419    426   
    #[cfg(feature = "rt-tokio")]
  420    427   
    fn anonymous_registry(
  421    428   
        reuse_scope: ConnectionReuseScope,
  422    429   
        max_connections_per_host: Option<NonZeroUsize>,
  423    430   
    ) -> PartitionRegistry {
  424    431   
        PartitionRegistry::new(
  425    432   
            None,
  426    433   
            reuse_scope,
  427    434   
            max_connections_per_host,
         435  +
            true,
  428    436   
            MaintenanceConfig::default(),
  429    437   
        )
  430    438   
        .unwrap()
  431    439   
    }
  432    440   
  433    441   
    fn explicit_registry(
  434    442   
        partitions: impl IntoIterator<Item = Partition>,
  435    443   
        reuse_scope: ConnectionReuseScope,
  436    444   
        max_connections_per_host: Option<NonZeroUsize>,
  437    445   
    ) -> Result<PartitionRegistry, PartitionRegistryError> {
  438    446   
        PartitionRegistry::new(
  439    447   
            Some(partitions.into_iter().collect()),
  440    448   
            reuse_scope,
  441    449   
            max_connections_per_host,
         450  +
            true,
  442    451   
            MaintenanceConfig::default(),
  443    452   
        )
  444    453   
    }
  445    454   
  446    455   
    fn explicit_partition(registry: &PartitionRegistry, index: usize) -> Arc<PartitionState> {
  447    456   
        registry
  448    457   
            .partition(PartitionId::from_index(index))
  449    458   
            .expect("test partition was not registered")
  450    459   
    }
  451    460   
@@ -695,704 +785,795 @@
  715    724   
        let cells: Vec<_> = threads
  716    725   
            .into_iter()
  717    726   
            .map(|thread| thread.join().unwrap())
  718    727   
            .collect();
  719    728   
        assert!(cells
  720    729   
            .iter()
  721    730   
            .skip(1)
  722    731   
            .all(|cell| Arc::ptr_eq(&cells[0], cell)));
  723    732   
        assert_eq!(1, partition.cell_count());
  724    733   
  725         -
        let waiter = cells[0].register_waiter(ProtocolRequirement::H1Compatible);
         734  +
        let waiter = OriginCell::register_waiter(&cells[0], ProtocolRequirement::H1Compatible);
  726    735   
        let lease = OriginCell::take_ready_lease(&cells[0], waiter)
  727    736   
            .expect("admission targeted a different cell than the registry retained");
  728    737   
        drop(lease);
  729    738   
    }
  730    739   
}
  731    740   
  732    741   
#[cfg(all(test, smithy_http_client_loom))]
  733    742   
mod loom_tests {
  734    743   
    use super::*;
  735    744   
    use std::future::Future;
  736    745   
    use std::pin::Pin;
  737    746   
  738    747   
    #[derive(Debug)]
  739    748   
    struct TestSpawner;
  740    749   
  741    750   
    impl DriverSpawner for TestSpawner {
  742    751   
        fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
  743    752   
            drop(driver);
  744    753   
        }
  745    754   
    }
  746    755   
  747    756   
    fn explicit_registry(
  748    757   
        partitions: impl IntoIterator<Item = Partition>,
  749    758   
        reuse_scope: ConnectionReuseScope,
  750    759   
        max_connections_per_host: Option<NonZeroUsize>,
  751    760   
    ) -> Result<PartitionRegistry, PartitionRegistryError> {
  752    761   
        PartitionRegistry::new(
  753    762   
            Some(partitions.into_iter().collect()),
  754    763   
            reuse_scope,
  755    764   
            max_connections_per_host,
         765  +
            true,
  756    766   
            MaintenanceConfig::default(),
  757    767   
        )
  758    768   
    }
  759    769   
  760    770   
    #[test]
  761    771   
    fn first_cell_publication_is_stable_under_contention() {
  762    772   
        loom::model(|| {
  763    773   
            let registry = Arc::new(
  764    774   
                explicit_registry(
  765    775   
                    [Partition::new(PartitionId::from_index(1), TestSpawner)],

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

@@ -282,282 +354,371 @@
  302    302   
            self.web_pki_server_verifier
  303    303   
                .verify_tls13_signature(message, cert, dss)
  304    304   
        }
  305    305   
  306    306   
        fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
  307    307   
            self.web_pki_server_verifier.supported_verify_schemes()
  308    308   
        }
  309    309   
    }
  310    310   
  311    311   
    pub(crate) fn wrap_connector<R>(
         312  +
        conn: HttpConnector<R>,
         313  +
        crypto_mode: CryptoMode,
         314  +
        tls_context: &TlsContext,
         315  +
        proxy_config: crate::client::proxy::ProxyConfig,
         316  +
    ) -> super::connect::RustTlsConnector<R> {
         317  +
        wrap_connector_with_alpn(
         318  +
            conn,
         319  +
            crypto_mode,
         320  +
            tls_context,
         321  +
            proxy_config,
         322  +
            &[b"h2", b"http/1.1"],
         323  +
        )
         324  +
    }
         325  +
         326  +
    /// Wraps an HTTP connector with the ALPN offer selected for one pool attempt.
         327  +
    pub(crate) fn wrap_connector_with_alpn<R>(
  312    328   
        mut conn: HttpConnector<R>,
  313    329   
        crypto_mode: CryptoMode,
  314    330   
        tls_context: &TlsContext,
  315    331   
        proxy_config: crate::client::proxy::ProxyConfig,
         332  +
        alpn_protocols: &[&[u8]],
  316    333   
    ) -> super::connect::RustTlsConnector<R> {
  317         -
        let client_config = create_rustls_client_config(crypto_mode, tls_context);
         334  +
        let mut client_config = create_rustls_client_config(crypto_mode, tls_context);
         335  +
        client_config.alpn_protocols = alpn_protocols
         336  +
            .iter()
         337  +
            .map(|protocol| protocol.to_vec())
         338  +
            .collect();
  318    339   
        conn.enforce_http(false);
  319         -
        let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
  320         -
            .with_tls_config(client_config.clone())
  321         -
            .https_or_http()
  322         -
            .enable_http1()
  323         -
            .enable_http2()
  324         -
            .wrap_connector(conn);
         340  +
        let https_connector =
         341  +
            hyper_rustls::HttpsConnector::from((conn, client_config.clone()));
  325    342   
  326    343   
        super::connect::RustTlsConnector::new(https_connector, client_config, proxy_config)
  327    344   
    }
  328    345   
}
  329    346   
  330    347   
pub(crate) mod connect {
  331    348   
    use crate::client::connect::{Conn, Connecting};
  332    349   
    use crate::client::proxy::ProxyConfig;
  333    350   
    use aws_smithy_runtime_api::box_error::BoxError;
  334    351   
    use http_1x::uri::Scheme;

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/tests/common/h2.rs

@@ -1,1 +47,48 @@
    8      8   
//! This fixture is intentionally test-local. The project keeps its TLS setup surface
    9      9   
//! out of the public API while still testing HTTP/2 connection-pool behavior against
   10     10   
//! a real TLS+h2 server.
   11     11   
   12     12   
use super::client::WAIT;
   13     13   
use super::tls;
   14     14   
use aws_smithy_http_client::test_util::wire::connection::{ConnectionCloseReason, GateWaiter};
   15     15   
use bytes::Bytes;
   16     16   
use h2::server::SendResponse;
   17     17   
use h2::Reason;
          18  +
use h2::RecvStream;
   18     19   
use http_1x::{Method, Response, StatusCode};
   19     20   
use std::collections::{HashMap, VecDeque};
   20     21   
use std::error::Error;
   21     22   
use std::fmt;
   22     23   
use std::future::poll_fn;
   23     24   
use std::io;
   24     25   
use std::net::SocketAddr;
   25     26   
use std::pin::Pin;
   26     27   
use std::sync::atomic::{AtomicU64, Ordering};
   27     28   
use std::sync::{Arc, Mutex};
@@ -169,170 +272,292 @@
  189    190   
  190    191   
    pub(crate) fn body(mut self, body: H2BodyPlan) -> Self {
  191    192   
        self.body = body;
  192    193   
        self
  193    194   
    }
  194    195   
}
  195    196   
  196    197   
#[derive(Clone, Debug)]
  197    198   
pub(crate) enum H2StreamScript {
  198    199   
    Respond(H2Response),
         200  +
    /// Sends the response before a gate permits the request body to be drained.
         201  +
    RespondBeforeReceivingRequestBody {
         202  +
        response: H2Response,
         203  +
        receive: GateWaiter,
         204  +
    },
  199    205   
    Reset(Reason),
  200    206   
}
  201    207   
  202    208   
impl H2StreamScript {
  203    209   
    pub(crate) fn respond(response: H2Response) -> Self {
  204    210   
        Self::Respond(response)
  205    211   
    }
  206    212   
         213  +
    /// Delays request-body reads until after the response and gate release.
         214  +
    pub(crate) fn respond_before_receiving_request_body(
         215  +
        response: H2Response,
         216  +
        receive: GateWaiter,
         217  +
    ) -> Self {
         218  +
        Self::RespondBeforeReceivingRequestBody { response, receive }
         219  +
    }
         220  +
  207    221   
    pub(crate) fn reset(reason: Reason) -> Self {
  208    222   
        Self::Reset(reason)
  209    223   
    }
  210    224   
}
  211    225   
  212    226   
#[derive(Clone, Debug, Default)]
  213    227   
pub(crate) struct H2ConnectionScript {
  214    228   
    routes: HashMap<String, H2StreamScript>,
  215    229   
    fallback: Option<H2StreamScript>,
  216    230   
    allow_handshake_abandonment: bool,
         231  +
    goaway_on_ready: bool,
  217    232   
}
  218    233   
  219    234   
impl H2ConnectionScript {
  220    235   
    pub(crate) fn new() -> Self {
  221    236   
        Self::default()
  222    237   
    }
  223    238   
  224    239   
    pub(crate) fn route(mut self, path: impl Into<String>, script: H2StreamScript) -> Self {
  225    240   
        let path = path.into();
  226    241   
        assert!(
  227    242   
            self.routes.insert(path.clone(), script).is_none(),
  228    243   
            "duplicate H2 route {path:?}"
  229    244   
        );
  230    245   
        self
  231    246   
    }
  232    247   
  233    248   
    pub(crate) fn fallback(mut self, script: H2StreamScript) -> Self {
  234    249   
        self.fallback = Some(script);
  235    250   
        self
  236    251   
    }
  237    252   
  238    253   
    pub(crate) fn allow_handshake_abandonment(mut self) -> Self {
  239    254   
        self.allow_handshake_abandonment = true;
  240    255   
        self
  241    256   
    }
  242    257   
         258  +
    pub(crate) fn goaway_on_ready(mut self) -> Self {
         259  +
        self.goaway_on_ready = true;
         260  +
        self
         261  +
    }
         262  +
  243    263   
    fn script_for(&self, path: &str) -> Option<H2StreamScript> {
  244    264   
        self.routes.get(path).or(self.fallback.as_ref()).cloned()
  245    265   
    }
  246    266   
}
  247    267   
  248    268   
#[derive(Clone, Debug)]
  249    269   
pub(crate) enum H2ConnectionPlan {
  250    270   
    Queue(VecDeque<H2ConnectionScript>),
  251    271   
    Unbounded(H2ConnectionScript),
  252    272   
}
@@ -683,703 +806,836 @@
  703    723   
            Ok(Err(err)) if script.allow_handshake_abandonment => {
  704    724   
                state.record_event(H2Event::H2HandshakeAbandoned {
  705    725   
                    connection_id,
  706    726   
                    error: err.to_string(),
  707    727   
                });
  708    728   
                return Ok(ConnectionCloseReason::ClientClosed);
  709    729   
            }
  710    730   
            Ok(Err(err)) => return Err(H2HarnessError::new(format!("H2 handshake failed: {err}"))),
  711    731   
        };
  712    732   
    state.record_event(H2Event::H2Ready { connection_id });
         733  +
    if script.goaway_on_ready {
         734  +
        connection.abrupt_shutdown(Reason::NO_ERROR);
         735  +
    }
  713    736   
  714    737   
    let mut stream_tasks = JoinSet::new();
  715    738   
    let mut shutting_down = false;
  716         -
    let mut graceful_shutdown = false;
         739  +
    let mut graceful_shutdown = script.goaway_on_ready;
  717    740   
    let mut control_open = true;
  718    741   
    // Tracks whether the connection ended because the client closed it (accept returned None)
  719    742   
    // vs. a scripted GOAWAY completing.
  720    743   
    let mut client_initiated_close = false;
  721    744   
  722    745   
    loop {
  723    746   
        tokio::select! {
  724    747   
            changed = shutdown.changed(), if !shutting_down => {
  725    748   
                if changed.is_err() || *shutdown.borrow() {
  726    749   
                    shutting_down = true;
  727    750   
                    connection.abrupt_shutdown(Reason::NO_ERROR);
  728    751   
                }
  729    752   
            }
  730    753   
            command = control.recv(), if control_open => {
  731    754   
                match command {
  732    755   
                    Some(ConnectionCommand::GracefulShutdown(acknowledged)) => {
  733    756   
                        graceful_shutdown = true;
  734    757   
                        connection.graceful_shutdown();
  735    758   
                        let _ = acknowledged.send(());
  736    759   
                    }
  737    760   
                    None => control_open = false,
  738    761   
                }
  739    762   
            }
  740    763   
            accepted = connection.accept() => {
  741    764   
                match accepted {
  742    765   
                    Some(Ok((request, mut respond))) => {
  743    766   
                        let stream_id = respond.stream_id().as_u32();
  744    767   
                        let method = request.method().clone();
  745    768   
                        let path = request.uri().path().to_string();
  746    769   
                        state.record_event(H2Event::StreamAccepted {
  747    770   
                            connection_id,
  748    771   
                            stream_id,
  749    772   
                            method,
  750    773   
                            path: path.clone(),
  751    774   
                        });
  752    775   
                        let Some(stream_script) = script.script_for(&path) else {
  753    776   
                            respond.send_reset(Reason::PROTOCOL_ERROR);
  754    777   
                            return Err(H2HarnessError::new(format!(
  755    778   
                                "connection {connection_id:?} received unscripted H2 path {path:?}"
  756    779   
                            )));
  757    780   
                        };
  758         -
                        // Scripts cover the response side only, so the request body is
  759         -
                        // never read. Dropping the `RecvStream` marks the stream as no
  760         -
                        // longer receiving: h2 then discards incoming DATA without
  761         -
                        // charging the stream window and releases the connection
  762         -
                        // capacity, so a client streaming a request body runs to
  763         -
                        // completion. Holding it instead would charge the window on
  764         -
                        // every DATA frame and never refund it, stalling the client
  765         -
                        // once the send window is exhausted.
  766         -
                        //
  767         -
                        // TODO(test-utils): to script request bodies, pass
  768         -
                        // `request.into_body()` into `run_stream` and drive it there,
  769         -
                        // calling `release_capacity()` as chunks are consumed.
         781  +
                        let held_request = match &stream_script {
         782  +
                            H2StreamScript::RespondBeforeReceivingRequestBody { receive, .. } => {
         783  +
                                Some((request.into_body(), receive.clone()))
         784  +
                            }
         785  +
                            _ => {
  770    786   
                                drop(request);
         787  +
                                None
         788  +
                            }
         789  +
                        };
         790  +
                        // Most scripts cover the response side only. Dropping their
         791  +
                        // `RecvStream` marks the stream as no longer receiving: h2
         792  +
                        // then discards incoming DATA without charging the stream
         793  +
                        // window and releases connection capacity, so a client
         794  +
                        // streaming a request body runs to completion.
         795  +
                        //
         796  +
                        // A lifetime contract may retain a pending request body
         797  +
                        // behind a gate without reading DATA. General request-body
         798  +
                        // scripts must drive `RecvStream` and call
         799  +
                        // `release_capacity()` as chunks are consumed.
  771    800   
                        stream_tasks.spawn(run_stream(
  772    801   
                            connection_id,
  773    802   
                            stream_id,
  774    803   
                            stream_script,
  775    804   
                            respond,
  776    805   
                            state.clone(),
         806  +
                            held_request,
  777    807   
                        ));
  778    808   
                    }
  779    809   
                    Some(Err(err)) => {
  780    810   
                        if shutting_down
  781    811   
                            || (graceful_shutdown && err.reason() == Some(Reason::NO_ERROR))
  782    812   
                        {
  783    813   
                            break;
  784    814   
                        }
  785    815   
                        return Err(H2HarnessError::new(format!(
  786    816   
                            "H2 connection {connection_id:?} failed while accepting a stream: {err}"
@@ -815,845 +901,937 @@
  835    865   
        }
  836    866   
    }
  837    867   
}
  838    868   
  839    869   
async fn run_stream(
  840    870   
    connection_id: H2ConnectionId,
  841    871   
    stream_id: u32,
  842    872   
    script: H2StreamScript,
  843    873   
    mut respond: SendResponse<Bytes>,
  844    874   
    state: Arc<SharedState>,
         875  +
    held_request: Option<(RecvStream, GateWaiter)>,
  845    876   
) -> Result<(), H2HarnessError> {
  846    877   
    match script {
  847    878   
        H2StreamScript::Reset(reason) => {
  848    879   
            respond.send_reset(reason);
  849    880   
            state.record_event(H2Event::ResetSent {
  850    881   
                connection_id,
  851    882   
                stream_id,
  852    883   
                reason,
  853    884   
            });
  854    885   
        }
  855         -
        H2StreamScript::Respond(response) => {
         886  +
        H2StreamScript::Respond(response)
         887  +
        | H2StreamScript::RespondBeforeReceivingRequestBody {
         888  +
            response,
         889  +
            receive: _,
         890  +
        } => {
  856    891   
            let end_stream =
  857    892   
                matches!(&response.body, H2BodyPlan::Complete(body) if body.is_empty());
  858    893   
            let response_head = Response::builder()
  859    894   
                .status(response.status)
  860    895   
                .body(())
  861    896   
                .map_err(|err| {
  862    897   
                    H2HarnessError::new(format!("failed to build H2 response: {err}"))
  863    898   
                })?;
  864    899   
            let mut send = respond
  865    900   
                .send_response(response_head, end_stream)
  866    901   
                .map_err(|err| H2HarnessError::new(format!("failed to send H2 response: {err}")))?;
  867    902   
            if end_stream {
  868    903   
                state.record_event(H2Event::ResponseCompleted {
  869    904   
                    connection_id,
  870    905   
                    stream_id,
  871    906   
                });
         907  +
                receive_request_body(held_request).await?;
  872    908   
                return Ok(());
  873    909   
            }
  874    910   
  875    911   
            match response.body {
  876    912   
                H2BodyPlan::Complete(body) => {
  877    913   
                    send.send_data(body, true).map_err(|err| {
  878    914   
                        H2HarnessError::new(format!("failed to send H2 response body: {err}"))
  879    915   
                    })?;
  880    916   
                    state.record_event(H2Event::ResponseCompleted {
  881    917   
                        connection_id,
@@ -930,966 +989,1051 @@
  950    986   
                    }
  951    987   
                    state.record_event(H2Event::ClientResetObserved {
  952    988   
                        connection_id,
  953    989   
                        stream_id,
  954    990   
                        reason,
  955    991   
                    });
  956    992   
                }
  957    993   
            }
  958    994   
        }
  959    995   
    }
         996  +
    receive_request_body(held_request).await?;
         997  +
    Ok(())
         998  +
}
         999  +
        1000  +
/// Waits for the test, then consumes a retained request body through end-of-stream.
        1001  +
async fn receive_request_body(
        1002  +
    held_request: Option<(RecvStream, GateWaiter)>,
        1003  +
) -> Result<(), H2HarnessError> {
        1004  +
    let Some((mut request_body, receive)) = held_request else {
        1005  +
        return Ok(());
        1006  +
    };
        1007  +
    receive
        1008  +
        .wait()
        1009  +
        .await
        1010  +
        .map_err(|err| H2HarnessError::new(format!("H2 request-body gate failed: {err}")))?;
        1011  +
    while let Some(chunk) = request_body.data().await {
        1012  +
        let chunk = chunk.map_err(|err| {
        1013  +
            H2HarnessError::new(format!("failed while receiving H2 request body: {err}"))
        1014  +
        })?;
        1015  +
        request_body
        1016  +
            .flow_control()
        1017  +
            .release_capacity(chunk.len())
        1018  +
            .map_err(|err| {
        1019  +
                H2HarnessError::new(format!("failed to release H2 request capacity: {err}"))
        1020  +
            })?;
        1021  +
    }
  960   1022   
    Ok(())
  961   1023   
}
  962   1024   
  963   1025   
struct ObservedIo<T> {
  964   1026   
    inner: T,
  965   1027   
    observer: FrameObserver,
  966   1028   
}
  967   1029   
  968   1030   
impl<T> ObservedIo<T> {
  969   1031   
    fn new(inner: T, connection_id: H2ConnectionId, state: Arc<SharedState>) -> Self {

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/tests/h2_connection_behavior_test.rs

@@ -1,1 +91,118 @@
    5      5   
    6      6   
//! HTTP/2 connection behavior contracts.
    7      7   
//!
    8      8   
//! Each contract is implementation-neutral and has an explicit runner for every
    9      9   
//! HTTP client backend expected to preserve that behavior.
   10     10   
   11     11   
#![cfg(all(feature = "wire-mock", feature = "rustls-aws-lc"))]
   12     12   
   13     13   
mod common;
   14     14   
          15  +
use aws_smithy_http_client::pool::{
          16  +
    Client as PoolClient, ConnectionPool, ConnectionReuseScope, Partition, PartitionId,
          17  +
    TokioDriverSpawner,
          18  +
};
   15     19   
use aws_smithy_http_client::test_util::wire::connection::{ConnectionCloseReason, ManualGate};
   16     20   
use aws_smithy_http_client::tls;
   17     21   
use aws_smithy_http_client::Builder;
   18     22   
use aws_smithy_runtime_api::client::connection::{
   19     23   
    CaptureSmithyConnection, ConnectionMetadata as SmithyConnectionMetadata,
   20     24   
};
   21     25   
use aws_smithy_runtime_api::client::http::{SharedHttpClient, SharedHttpConnector};
   22     26   
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
   23     27   
use bytes::Bytes;
   24     28   
use common::client as test_client;
   25     29   
use common::client::{BackendConfig, HyperUtilLegacyPool};
   26     30   
use common::h2::{
   27     31   
    H2BodyPlan, H2ConnectionId, H2ConnectionPlan, H2ConnectionScript, H2Event, H2Response,
   28     32   
    H2StreamScript, H2TestServer,
   29     33   
};
   30     34   
use common::tls as test_tls;
   31     35   
use h2::Reason;
   32     36   
use http_body_util::BodyExt;
   33     37   
use std::error::Error;
   34     38   
   35     39   
trait HttpsClientBackend {
   36     40   
    fn build_https(
   37     41   
        &self,
   38     42   
        config: BackendConfig,
   39     43   
        provider: tls::Provider,
   40     44   
        tls_context: tls::TlsContext,
   41     45   
    ) -> SharedHttpClient;
   42     46   
}
   43     47   
   44     48   
impl HttpsClientBackend for HyperUtilLegacyPool {
   45     49   
    fn build_https(
   46     50   
        &self,
   47     51   
        config: BackendConfig,
   48     52   
        provider: tls::Provider,
   49     53   
        tls_context: tls::TlsContext,
   50     54   
    ) -> SharedHttpClient {
   51     55   
        let mut builder = Builder::new();
   52     56   
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
   53     57   
            builder = builder.pool_idle_timeout(pool_idle_timeout);
   54     58   
        }
   55     59   
        builder
   56     60   
            .tls_provider(provider)
   57     61   
            .tls_context(tls_context)
   58     62   
            .build_https()
   59     63   
    }
   60     64   
}
   61     65   
          66  +
#[derive(Clone, Copy, Debug)]
          67  +
struct PartitionedConnectionPool;
          68  +
          69  +
impl HttpsClientBackend for PartitionedConnectionPool {
          70  +
    fn build_https(
          71  +
        &self,
          72  +
        config: BackendConfig,
          73  +
        provider: tls::Provider,
          74  +
        tls_context: tls::TlsContext,
          75  +
    ) -> SharedHttpClient {
          76  +
        let mut builder = ConnectionPool::builder().tls_provider(provider);
          77  +
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
          78  +
            builder = builder.idle_timeout(pool_idle_timeout);
          79  +
        }
          80  +
        let pool = builder
          81  +
            .tls_context(tls_context)
          82  +
            .build_https()
          83  +
            .expect("valid connection-pool config");
          84  +
        let client = PoolClient::new(&pool).expect("anonymous partition exists");
          85  +
        SharedHttpClient::new(client)
          86  +
    }
          87  +
}
          88  +
   62     89   
fn rustls_aws_lc() -> tls::Provider {
   63     90   
    tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc)
   64     91   
}
   65     92   
   66     93   
fn h2_client_with_provider(
   67     94   
    backend: &dyn HttpsClientBackend,
   68     95   
    provider: tls::Provider,
   69     96   
) -> SharedHttpClient {
   70     97   
    backend.build_https(
   71     98   
        BackendConfig::default(),
@@ -130,157 +241,278 @@
  150    177   
        drop(connector);
  151    178   
        drop(client);
  152    179   
        server.shutdown().await.expect("clean H2 server shutdown");
  153    180   
    }
  154    181   
  155    182   
    #[tokio::test]
  156    183   
    async fn test_sequential_requests_reuse_connection_with_hyper_util_legacy_pool() {
  157    184   
        sequential_requests_reuse_connection(&HyperUtilLegacyPool).await;
  158    185   
    }
  159    186   
         187  +
    #[tokio::test]
         188  +
    async fn test_sequential_requests_reuse_connection_with_partitioned_pool() {
         189  +
        sequential_requests_reuse_connection(&PartitionedConnectionPool).await;
         190  +
    }
         191  +
  160    192   
    /// Concurrent requests multiplex as independent streams on a warmed H2 connection.
  161    193   
    async fn concurrent_requests_multiplex_on_warmed_connection(backend: &dyn HttpsClientBackend) {
  162    194   
        let body_gate = ManualGate::new();
  163    195   
        let script = H2ConnectionScript::new()
  164    196   
            .route("/warm", H2StreamScript::respond(H2Response::ok("warm")))
  165    197   
            .route(
  166    198   
                "/concurrent",
  167    199   
                H2StreamScript::respond(H2Response::new(http_1x::StatusCode::OK).body(
  168    200   
                    H2BodyPlan::gated(Bytes::new(), body_gate.waiter(), "multiplexed"),
  169    201   
                )),
  170    202   
            );
  171    203   
        let server = H2TestServer::builder()
  172    204   
            .connections(H2ConnectionPlan::queue([script]))
  173    205   
            .start()
  174    206   
            .await
  175    207   
            .expect("H2 server should start");
  176    208   
        let client = h2_client(backend);
  177    209   
        let connector = test_client::connector(&client);
  178    210   
  179    211   
        let (status, body) = test_client::get_and_collect(&connector, &server.url("/warm")).await;
  180    212   
        assert_eq!((status, body.as_slice()), (200, b"warm".as_slice()));
  181    213   
  182    214   
        let mut requests = tokio::task::JoinSet::new();
  183    215   
        for _ in 0..4 {
  184    216   
            let connector = connector.clone();
  185    217   
            let url = server.url("/concurrent");
  186    218   
            requests.spawn(async move { test_client::get_and_collect(&connector, &url).await });
  187    219   
        }
  188    220   
  189    221   
        body_gate
  190    222   
            .wait_for_arrivals(4, test_client::WAIT)
  191    223   
            .await
  192    224   
            .expect("all concurrent H2 streams should reach their body gate");
  193    225   
        assert_eq!(server.connection_count(), 1);
  194    226   
        assert_eq!(server.stream_count(), 5);
  195    227   
        body_gate.release();
  196    228   
  197    229   
        while let Some(result) = requests.join_next().await {
  198    230   
            let (status, body) = result.expect("request task should not panic");
  199    231   
            assert_eq!((status, body.as_slice()), (200, b"multiplexed".as_slice()));
  200    232   
        }
  201    233   
  202    234   
        drop(connector);
  203    235   
        drop(client);
  204    236   
        server.shutdown().await.expect("clean H2 server shutdown");
  205    237   
    }
  206    238   
  207    239   
    #[tokio::test]
  208    240   
    async fn test_concurrent_requests_multiplex_on_warmed_connection_with_hyper_util_legacy_pool() {
  209    241   
        concurrent_requests_multiplex_on_warmed_connection(&HyperUtilLegacyPool).await;
  210    242   
    }
  211    243   
         244  +
    #[tokio::test]
         245  +
    async fn test_concurrent_requests_multiplex_on_warmed_connection_with_partitioned_pool() {
         246  +
        concurrent_requests_multiplex_on_warmed_connection(&PartitionedConnectionPool).await;
         247  +
    }
         248  +
  212    249   
    /// Concurrent cold-start requests converge on one established H2 connection even when
  213    250   
    /// connection attempts race.
  214    251   
    async fn concurrent_cold_start_converges_on_one_h2_connection(
  215    252   
        backend: &dyn HttpsClientBackend,
  216    253   
    ) {
  217    254   
        let body_gate = ManualGate::new();
  218    255   
        let script = H2ConnectionScript::new()
  219    256   
            .allow_handshake_abandonment()
  220    257   
            .fallback(H2StreamScript::respond(
  221    258   
                H2Response::new(http_1x::StatusCode::OK).body(H2BodyPlan::gated(
@@ -251,288 +350,397 @@
  271    308   
        drop(connector);
  272    309   
        drop(client);
  273    310   
        server.shutdown().await.expect("clean H2 server shutdown");
  274    311   
    }
  275    312   
  276    313   
    #[tokio::test]
  277    314   
    async fn test_concurrent_cold_start_converges_on_one_h2_connection_with_hyper_util_legacy_pool()
  278    315   
    {
  279    316   
        concurrent_cold_start_converges_on_one_h2_connection(&HyperUtilLegacyPool).await;
  280    317   
    }
         318  +
         319  +
    #[tokio::test]
         320  +
    async fn test_concurrent_cold_start_converges_on_one_h2_connection_with_partitioned_pool() {
         321  +
        concurrent_cold_start_converges_on_one_h2_connection(&PartitionedConnectionPool).await;
         322  +
    }
  281    323   
}
  282    324   
  283    325   
mod connection_metadata {
  284    326   
    use super::*;
  285    327   
  286    328   
    /// Poisoning captured H2 connection metadata moves later streams to a new connection.
  287    329   
    async fn poisoned_connection_is_not_reused(backend: &dyn HttpsClientBackend) {
  288    330   
        let script =
  289    331   
            H2ConnectionScript::new().fallback(H2StreamScript::respond(H2Response::ok("ok")));
  290    332   
        let server = H2TestServer::builder()
  291    333   
            .connections(H2ConnectionPlan::queue([script.clone(), script]))
  292    334   
            .start()
  293    335   
            .await
  294    336   
            .expect("H2 server should start");
  295    337   
        let client = h2_client(backend);
  296    338   
        let connector = test_client::connector(&client);
  297    339   
  298    340   
        let (status, body, metadata) =
  299    341   
            get_and_collect_with_capture(&connector, &server.url("/first")).await;
  300    342   
        assert_eq!((status, body.as_slice()), (200, b"ok".as_slice()));
  301    343   
        metadata.poison();
  302    344   
  303    345   
        let (status, body) = test_client::get_and_collect(&connector, &server.url("/second")).await;
  304    346   
        assert_eq!((status, body.as_slice()), (200, b"ok".as_slice()));
  305    347   
  306    348   
        let first_connection = single_stream_connection(&server, "/first");
  307    349   
        let second_connection = single_stream_connection(&server, "/second");
  308    350   
        assert_ne!(first_connection, second_connection);
  309    351   
        assert_eq!(server.connection_count(), 2);
  310    352   
  311    353   
        drop(metadata);
  312    354   
        drop(connector);
  313    355   
        drop(client);
  314    356   
        server.shutdown().await.expect("clean H2 server shutdown");
  315    357   
    }
  316    358   
  317    359   
    #[tokio::test]
  318    360   
    async fn test_poisoned_connection_is_not_reused_with_hyper_util_legacy_pool() {
  319    361   
        poisoned_connection_is_not_reused(&HyperUtilLegacyPool).await;
  320    362   
    }
         363  +
         364  +
    #[tokio::test]
         365  +
    async fn test_poisoned_connection_is_not_reused_with_partitioned_pool() {
         366  +
        poisoned_connection_is_not_reused(&PartitionedConnectionPool).await;
         367  +
    }
  321    368   
}
  322    369   
  323    370   
mod stream_failures {
  324    371   
    use super::*;
  325    372   
  326    373   
    /// A peer reset fails only its H2 stream and leaves the connection available for reuse.
  327    374   
    async fn stream_reset_does_not_retire_connection(backend: &dyn HttpsClientBackend) {
  328    375   
        let reset_gate = ManualGate::new();
  329    376   
        let script = H2ConnectionScript::new()
  330    377   
            .route(
@@ -371,418 +430,482 @@
  391    438   
        drop(connector);
  392    439   
        drop(client);
  393    440   
        server.shutdown().await.expect("clean H2 server shutdown");
  394    441   
    }
  395    442   
  396    443   
    #[tokio::test]
  397    444   
    async fn test_stream_reset_does_not_retire_connection_with_hyper_util_legacy_pool() {
  398    445   
        stream_reset_does_not_retire_connection(&HyperUtilLegacyPool).await;
  399    446   
    }
  400    447   
         448  +
    #[tokio::test]
         449  +
    async fn test_stream_reset_does_not_retire_connection_with_partitioned_pool() {
         450  +
        stream_reset_does_not_retire_connection(&PartitionedConnectionPool).await;
         451  +
    }
         452  +
  401    453   
    /// Dropping an incomplete response body cancels only that stream and permits reuse.
  402    454   
    async fn dropping_response_body_cancels_only_stream(backend: &dyn HttpsClientBackend) {
  403    455   
        let script = H2ConnectionScript::new()
  404    456   
            .route(
  405    457   
                "/drop",
  406    458   
                H2StreamScript::respond(
  407    459   
                    H2Response::new(http_1x::StatusCode::OK)
  408    460   
                        .body(H2BodyPlan::await_client_reset("partial", Reason::CANCEL)),
  409    461   
                ),
  410    462   
            )
@@ -439,491 +498,555 @@
  459    511   
  460    512   
        drop(connector);
  461    513   
        drop(client);
  462    514   
        server.shutdown().await.expect("clean H2 server shutdown");
  463    515   
    }
  464    516   
  465    517   
    #[tokio::test]
  466    518   
    async fn test_dropping_response_body_cancels_only_stream_with_hyper_util_legacy_pool() {
  467    519   
        dropping_response_body_cancels_only_stream(&HyperUtilLegacyPool).await;
  468    520   
    }
         521  +
         522  +
    #[tokio::test]
         523  +
    async fn test_dropping_response_body_cancels_only_stream_with_partitioned_pool() {
         524  +
        dropping_response_body_cancels_only_stream(&PartitionedConnectionPool).await;
         525  +
    }
  469    526   
}
  470    527   
  471    528   
mod goaway_and_replacement {
  472    529   
    use super::*;
  473    530   
  474    531   
    /// Graceful GOAWAY drains an eligible in-flight stream while later streams use a
  475    532   
    /// replacement connection.
  476    533   
    async fn graceful_goaway_preserves_in_flight_stream_and_replaces_connection(
  477    534   
        backend: &dyn HttpsClientBackend,
  478    535   
    ) {
@@ -556,613 +720,884 @@
  576    633   
        drop(client);
  577    634   
        server.shutdown().await.expect("clean H2 server shutdown");
  578    635   
    }
  579    636   
  580    637   
    #[tokio::test]
  581    638   
    async fn test_graceful_goaway_preserves_in_flight_stream_and_replaces_connection_with_hyper_util_legacy_pool(
  582    639   
    ) {
  583    640   
        graceful_goaway_preserves_in_flight_stream_and_replaces_connection(&HyperUtilLegacyPool)
  584    641   
            .await;
  585    642   
    }
         643  +
         644  +
    #[tokio::test]
         645  +
    async fn test_graceful_goaway_preserves_in_flight_stream_and_replaces_connection_with_partitioned_pool(
         646  +
    ) {
         647  +
        graceful_goaway_preserves_in_flight_stream_and_replaces_connection(
         648  +
            &PartitionedConnectionPool,
         649  +
        )
         650  +
        .await;
         651  +
    }
         652  +
         653  +
    #[tokio::test]
         654  +
    async fn goaway_before_first_stream_has_bounded_reacquisition() {
         655  +
        let script = H2ConnectionScript::new()
         656  +
            .goaway_on_ready()
         657  +
            .fallback(H2StreamScript::respond(H2Response::ok("unexpected")));
         658  +
        let server = H2TestServer::builder()
         659  +
            .connections(H2ConnectionPlan::unbounded(script))
         660  +
            .start()
         661  +
            .await
         662  +
            .expect("H2 server should start");
         663  +
        let client = h2_client(&PartitionedConnectionPool);
         664  +
        let connector = test_client::connector(&client);
         665  +
         666  +
        let outcome = tokio::time::timeout(
         667  +
            test_client::WAIT,
         668  +
            test_client::send_request(
         669  +
                &connector,
         670  +
                HttpRequest::get(server.url("/before-first-stream")).expect("valid HTTP request"),
         671  +
            ),
         672  +
        )
         673  +
        .await
         674  +
        .expect("GOAWAY before dispatch did not terminate");
         675  +
        assert!(
         676  +
            outcome.is_err(),
         677  +
            "a request succeeded after GOAWAY excluded its stream"
         678  +
        );
         679  +
        assert!(
         680  +
            server.connection_count() <= 3,
         681  +
            "GOAWAY before dispatch created {} connections",
         682  +
            server.connection_count()
         683  +
        );
         684  +
         685  +
        drop(connector);
         686  +
        drop(client);
         687  +
        server.shutdown().await.expect("clean H2 server shutdown");
         688  +
    }
  586    689   
}
  587    690   
  588    691   
#[cfg(feature = "s2n-tls")]
  589    692   
mod protocol_negotiation {
  590    693   
    use super::*;
  591    694   
  592    695   
    /// The s2n-tls provider negotiates `h2` with ALPN and reuses that connection.
  593    696   
    async fn s2n_negotiates_h2_and_reuses_connection(backend: &dyn HttpsClientBackend) {
  594    697   
        let server = H2TestServer::builder()
  595    698   
            .connections(H2ConnectionPlan::queue([H2ConnectionScript::new()
  596    699   
                .fallback(H2StreamScript::respond(H2Response::ok("s2n-h2")))]))
  597    700   
            .start()
  598    701   
            .await
  599    702   
            .expect("H2 server should start");
  600    703   
        let client = h2_client_with_provider(backend, tls::Provider::S2nTls);
  601    704   
        let connector = test_client::connector(&client);
  602    705   
  603    706   
        for request_number in 1..=3 {
  604    707   
            let (status, body) = test_client::get_and_collect(&connector, &server.url("/")).await;
  605    708   
            assert_eq!(status, 200, "request {request_number}");
  606    709   
            assert_eq!(body, b"s2n-h2", "request {request_number}");
  607    710   
        }
  608    711   
  609    712   
        let negotiated = server
  610    713   
            .events()
  611    714   
            .into_iter()
  612    715   
            .filter_map(|event| match event {
  613    716   
                H2Event::TlsNegotiated { alpn, .. } => Some(alpn),
  614    717   
                _ => None,
  615    718   
            })
  616    719   
            .collect::<Vec<_>>();
  617    720   
        assert_eq!(negotiated, vec![Some(b"h2".to_vec())]);
  618    721   
        assert_eq!(server.connection_count(), 1);
  619    722   
        assert_eq!(server.stream_count(), 3);
  620    723   
  621    724   
        drop(connector);
  622    725   
        drop(client);
  623    726   
        server.shutdown().await.expect("clean H2 server shutdown");
  624    727   
    }
  625    728   
  626    729   
    #[tokio::test]
  627    730   
    async fn test_s2n_negotiates_h2_and_reuses_connection_with_hyper_util_legacy_pool() {
  628    731   
        s2n_negotiates_h2_and_reuses_connection(&HyperUtilLegacyPool).await;
  629    732   
    }
         733  +
         734  +
    #[tokio::test]
         735  +
    async fn test_s2n_negotiates_h2_and_reuses_connection_with_partitioned_pool() {
         736  +
        s2n_negotiates_h2_and_reuses_connection(&PartitionedConnectionPool).await;
         737  +
    }
  630    738   
}
  631    739   
  632    740   
mod idle_timeout {
  633    741   
    use super::*;
         742  +
    use aws_smithy_types::body::SdkBody;
         743  +
    use http_body_1x::{Body, Frame, SizeHint};
         744  +
    use std::convert::Infallible;
         745  +
    use std::future::Future;
         746  +
    use std::pin::Pin;
         747  +
    use std::task::{Context, Poll};
  634    748   
    use std::time::Duration;
         749  +
    use tokio::sync::oneshot;
  635    750   
  636    751   
    const IDLE_TIMEOUT: Duration = Duration::from_millis(100);
  637    752   
         753  +
    /// Streaming request body that remains open until the test releases it.
         754  +
    struct HeldUpload {
         755  +
        finish: oneshot::Receiver<()>,
         756  +
        complete: bool,
         757  +
    }
         758  +
         759  +
    impl Body for HeldUpload {
         760  +
        type Data = Bytes;
         761  +
        type Error = Infallible;
         762  +
         763  +
        fn poll_frame(
         764  +
            mut self: Pin<&mut Self>,
         765  +
            cx: &mut Context<'_>,
         766  +
        ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
         767  +
            if self.complete {
         768  +
                return Poll::Ready(None);
         769  +
            }
         770  +
            match Pin::new(&mut self.finish).poll(cx) {
         771  +
                Poll::Ready(_) => {
         772  +
                    self.complete = true;
         773  +
                    Poll::Ready(None)
         774  +
                }
         775  +
                Poll::Pending => Poll::Pending,
         776  +
            }
         777  +
        }
         778  +
         779  +
        fn is_end_stream(&self) -> bool {
         780  +
            self.complete
         781  +
        }
         782  +
         783  +
        fn size_hint(&self) -> SizeHint {
         784  +
            SizeHint::default()
         785  +
        }
         786  +
    }
         787  +
         788  +
    fn held_upload() -> (oneshot::Sender<()>, SdkBody) {
         789  +
        let (finish, finished) = oneshot::channel();
         790  +
        let body = HeldUpload {
         791  +
            finish: finished,
         792  +
            complete: false,
         793  +
        };
         794  +
        (finish, SdkBody::from_body_1_x(body))
         795  +
    }
         796  +
  638    797   
    fn client_with_idle_timeout(backend: &dyn HttpsClientBackend) -> SharedHttpClient {
  639    798   
        backend.build_https(
  640    799   
            BackendConfig {
  641    800   
                pool_idle_timeout: Some(IDLE_TIMEOUT),
  642    801   
            },
  643    802   
            rustls_aws_lc(),
  644    803   
            test_tls::server_tls_context(),
  645    804   
        )
  646    805   
    }
  647    806   
  648    807   
    /// An idle H2 connection is closed after its configured timeout and then replaced.
  649    808   
    async fn idle_connection_is_evicted_after_timeout(backend: &dyn HttpsClientBackend) {
  650    809   
        let script =
  651    810   
            H2ConnectionScript::new().fallback(H2StreamScript::respond(H2Response::ok("ok")));
  652    811   
        let server = H2TestServer::builder()
  653    812   
            .connections(H2ConnectionPlan::queue([script.clone(), script]))
  654    813   
            .start()
  655    814   
            .await
  656    815   
            .expect("H2 server should start");
  657    816   
        let client = client_with_idle_timeout(backend);
  658    817   
        let connector = test_client::connector(&client);
  659    818   
  660    819   
        let (status, body) = test_client::get_and_collect(&connector, &server.url("/first")).await;
  661    820   
        assert_eq!((status, body.as_slice()), (200, b"ok".as_slice()));
  662    821   
        let first_connection = single_stream_connection(&server, "/first");
  663    822   
        server
  664    823   
            .wait_for_event(test_client::WAIT, |event| {
  665    824   
                matches!(
  666    825   
                    event,
  667    826   
                    H2Event::ConnectionClosed { connection_id, reason: ConnectionCloseReason::ClientClosed }
  668    827   
                        if *connection_id == first_connection
  669    828   
                )
  670    829   
            })
  671    830   
            .await
  672    831   
            .expect("the idle H2 connection should close after its pool timeout");
  673    832   
  674    833   
        let (status, body) = test_client::get_and_collect(&connector, &server.url("/second")).await;
  675    834   
        assert_eq!((status, body.as_slice()), (200, b"ok".as_slice()));
  676    835   
        assert_ne!(
  677    836   
            first_connection,
  678    837   
            single_stream_connection(&server, "/second")
  679    838   
        );
  680    839   
  681    840   
        drop(connector);
  682    841   
        drop(client);
  683    842   
        server.shutdown().await.expect("clean H2 server shutdown");
  684    843   
    }
  685    844   
  686    845   
    #[tokio::test]
  687    846   
    async fn test_idle_connection_is_evicted_after_timeout_with_hyper_util_legacy_pool() {
  688    847   
        idle_connection_is_evicted_after_timeout(&HyperUtilLegacyPool).await;
  689    848   
    }
  690    849   
         850  +
    #[tokio::test]
         851  +
    async fn test_idle_connection_is_evicted_after_timeout_with_partitioned_pool() {
         852  +
        idle_connection_is_evicted_after_timeout(&PartitionedConnectionPool).await;
         853  +
    }
         854  +
  691    855   
    /// An active stream survives the idle deadline, but the connection is replaced after the
  692    856   
    /// stream completes.
  693    857   
    async fn active_stream_survives_idle_timeout_but_later_request_uses_replacement(
  694    858   
        backend: &dyn HttpsClientBackend,
  695    859   
    ) {
  696    860   
        let held_body_gate = ManualGate::new();
  697    861   
        let script = H2ConnectionScript::new()
  698    862   
            .route(
  699    863   
                "/held",
  700    864   
                H2StreamScript::respond(H2Response::new(http_1x::StatusCode::OK).body(
@@ -731,895 +761,1129 @@
  751    915   
    }
  752    916   
  753    917   
    #[tokio::test]
  754    918   
    async fn test_active_stream_survives_idle_timeout_but_later_request_uses_replacement_with_hyper_util_legacy_pool(
  755    919   
    ) {
  756    920   
        active_stream_survives_idle_timeout_but_later_request_uses_replacement(
  757    921   
            &HyperUtilLegacyPool,
  758    922   
        )
  759    923   
        .await;
  760    924   
    }
         925  +
         926  +
    #[tokio::test]
         927  +
    async fn test_active_stream_survives_idle_timeout_but_later_request_uses_replacement_with_partitioned_pool(
         928  +
    ) {
         929  +
        active_stream_survives_idle_timeout_but_later_request_uses_replacement(
         930  +
            &PartitionedConnectionPool,
         931  +
        )
         932  +
        .await;
         933  +
    }
         934  +
         935  +
    /// Idle expiration retains a physical connection while its upload remains active.
         936  +
    #[tokio::test]
         937  +
    async fn response_completion_does_not_close_active_upload_at_idle_timeout() {
         938  +
        let held_request_gate = ManualGate::new();
         939  +
        let script = H2ConnectionScript::new()
         940  +
            .route(
         941  +
                "/upload",
         942  +
                H2StreamScript::respond_before_receiving_request_body(
         943  +
                    H2Response::ok("early response"),
         944  +
                    held_request_gate.waiter(),
         945  +
                ),
         946  +
            )
         947  +
            .route("/reuse", H2StreamScript::respond(H2Response::ok("reused")));
         948  +
        let server = H2TestServer::builder()
         949  +
            .connections(H2ConnectionPlan::queue([script.clone(), script]))
         950  +
            .start()
         951  +
            .await
         952  +
            .expect("H2 server should start");
         953  +
        let client = client_with_idle_timeout(&PartitionedConnectionPool);
         954  +
        let connector = test_client::connector(&client);
         955  +
         956  +
        let (finish_upload, upload) = held_upload();
         957  +
        let mut request = HttpRequest::new(upload);
         958  +
        request.set_method("POST").expect("valid HTTP method");
         959  +
        request
         960  +
            .set_uri(server.url("/upload"))
         961  +
            .expect("valid HTTP URI");
         962  +
        let response = test_client::send_request(&connector, request)
         963  +
            .await
         964  +
            .expect("response should arrive before upload completion");
         965  +
        let (status, body) = test_client::collect_response(response).await;
         966  +
        assert_eq!(
         967  +
            (status, body.as_slice()),
         968  +
            (200, b"early response".as_slice())
         969  +
        );
         970  +
         971  +
        held_request_gate
         972  +
            .wait_until_reached(test_client::WAIT)
         973  +
            .await
         974  +
            .expect("server should retain the active request body");
         975  +
        let upload_connection = single_stream_connection(&server, "/upload");
         976  +
        tokio::time::sleep(IDLE_TIMEOUT * 2).await;
         977  +
        assert!(
         978  +
            !server.events().iter().any(|event| {
         979  +
                matches!(
         980  +
                    event,
         981  +
                    H2Event::ConnectionClosed { connection_id, .. }
         982  +
                        if *connection_id == upload_connection
         983  +
                )
         984  +
            }),
         985  +
            "idle expiration closed a connection with an active upload"
         986  +
        );
         987  +
         988  +
        let (status, body) = test_client::get_and_collect(&connector, &server.url("/reuse")).await;
         989  +
        assert_eq!((status, body.as_slice()), (200, b"reused".as_slice()));
         990  +
        assert_ne!(
         991  +
            upload_connection,
         992  +
            single_stream_connection(&server, "/reuse"),
         993  +
            "an expired HTTP/2 generation accepted a new request"
         994  +
        );
         995  +
        assert_eq!(server.connection_count(), 2);
         996  +
         997  +
        finish_upload
         998  +
            .send(())
         999  +
            .expect("request body disappeared before upload completion");
        1000  +
        held_request_gate.release();
        1001  +
        server
        1002  +
            .wait_for_event(test_client::WAIT, |event| {
        1003  +
                matches!(
        1004  +
                    event,
        1005  +
                    H2Event::ConnectionClosed { connection_id, .. }
        1006  +
                        if *connection_id == upload_connection
        1007  +
                )
        1008  +
            })
        1009  +
            .await
        1010  +
            .expect("draining connection should close after upload completion");
        1011  +
        drop(connector);
        1012  +
        drop(client);
        1013  +
        server.shutdown().await.expect("clean H2 server shutdown");
        1014  +
    }
        1015  +
}
        1016  +
        1017  +
mod partition_reuse {
        1018  +
    use super::*;
        1019  +
    use std::time::Duration;
        1020  +
        1021  +
    fn partitioned_clients(
        1022  +
        scope: ConnectionReuseScope,
        1023  +
    ) -> (ConnectionPool, SharedHttpConnector, SharedHttpConnector) {
        1024  +
        let first = PartitionId::from_index(1);
        1025  +
        let second = PartitionId::from_index(2);
        1026  +
        let pool = ConnectionPool::builder()
        1027  +
            .tls_provider(rustls_aws_lc())
        1028  +
            .tls_context(test_tls::server_tls_context())
        1029  +
            .partitions([
        1030  +
                Partition::new(first, TokioDriverSpawner::current()),
        1031  +
                Partition::new(second, TokioDriverSpawner::current()),
        1032  +
            ])
        1033  +
            .connection_reuse_scope(scope)
        1034  +
            .max_connections_per_host(1)
        1035  +
            .build_https()
        1036  +
            .expect("valid partitioned HTTPS pool");
        1037  +
        let first_client = SharedHttpClient::new(
        1038  +
            PoolClient::from_partition(&pool, first).expect("first partition should resolve"),
        1039  +
        );
        1040  +
        let second_client = SharedHttpClient::new(
        1041  +
            PoolClient::from_partition(&pool, second).expect("second partition should resolve"),
        1042  +
        );
        1043  +
        (
        1044  +
            pool,
        1045  +
            test_client::connector(&first_client),
        1046  +
            test_client::connector(&second_client),
        1047  +
        )
        1048  +
    }
        1049  +
        1050  +
    async fn eligible_partition_reuses_peer_h2(scope: ConnectionReuseScope) {
        1051  +
        let server = H2TestServer::builder()
        1052  +
            .connections(H2ConnectionPlan::queue([H2ConnectionScript::new()
        1053  +
                .fallback(H2StreamScript::respond(H2Response::ok("shared")))]))
        1054  +
            .start()
        1055  +
            .await
        1056  +
            .expect("H2 server should start");
        1057  +
        let (pool, first, second) = partitioned_clients(scope);
        1058  +
        1059  +
        let first_result = test_client::get_and_collect(&first, &server.url("/first")).await;
        1060  +
        let second_result = test_client::get_and_collect(&second, &server.url("/second")).await;
        1061  +
        1062  +
        assert_eq!(first_result, (200, b"shared".to_vec()));
        1063  +
        assert_eq!(second_result, (200, b"shared".to_vec()));
        1064  +
        assert_eq!(server.connection_count(), 1);
        1065  +
        assert_eq!(
        1066  +
            single_stream_connection(&server, "/first"),
        1067  +
            single_stream_connection(&server, "/second"),
        1068  +
            "eligible partitions should dispatch through the connection-owning generation"
        1069  +
        );
        1070  +
        1071  +
        drop(first);
        1072  +
        drop(second);
        1073  +
        drop(pool);
        1074  +
        server.shutdown().await.expect("clean H2 server shutdown");
        1075  +
    }
        1076  +
        1077  +
    #[tokio::test]
        1078  +
    async fn pool_scope_reuses_a_peer_h2_generation() {
        1079  +
        eligible_partition_reuses_peer_h2(ConnectionReuseScope::Pool).await;
        1080  +
    }
        1081  +
        1082  +
    #[tokio::test]
        1083  +
    async fn matching_network_interface_scope_reuses_a_peer_h2_generation() {
        1084  +
        eligible_partition_reuses_peer_h2(ConnectionReuseScope::NetworkInterface).await;
        1085  +
    }
        1086  +
        1087  +
    #[tokio::test]
        1088  +
    async fn partition_scope_waits_until_out_of_scope_h2_releases_capacity() {
        1089  +
        let script = H2ConnectionScript::new()
        1090  +
            .fallback(H2StreamScript::respond(H2Response::ok("partition")));
        1091  +
        let server = H2TestServer::builder()
        1092  +
            .connections(H2ConnectionPlan::queue([script.clone(), script]))
        1093  +
            .start()
        1094  +
            .await
        1095  +
            .expect("H2 server should start");
        1096  +
        let (pool, first, second) = partitioned_clients(ConnectionReuseScope::Partition);
        1097  +
        1098  +
        let (status, body, metadata) =
        1099  +
            get_and_collect_with_capture(&first, &server.url("/first")).await;
        1100  +
        assert_eq!((status, body.as_slice()), (200, b"partition".as_slice()));
        1101  +
        let first_connection = single_stream_connection(&server, "/first");
        1102  +
        1103  +
        let second_url = server.url("/second");
        1104  +
        let mut pending =
        1105  +
            tokio::spawn(async move { test_client::get_and_collect(&second, &second_url).await });
        1106  +
        assert!(
        1107  +
            tokio::time::timeout(Duration::from_millis(100), &mut pending)
        1108  +
                .await
        1109  +
                .is_err(),
        1110  +
            "an out-of-scope generation must not satisfy partition-local demand"
        1111  +
        );
        1112  +
        assert_eq!(server.connection_count(), 1);
        1113  +
        1114  +
        metadata.poison();
        1115  +
        let (status, body) = tokio::time::timeout(test_client::WAIT, pending)
        1116  +
            .await
        1117  +
            .expect("released capacity should wake partition-local demand")
        1118  +
            .expect("request task should not panic");
        1119  +
        assert_eq!((status, body.as_slice()), (200, b"partition".as_slice()));
        1120  +
        let second_connection = single_stream_connection(&server, "/second");
        1121  +
        assert_ne!(first_connection, second_connection);
        1122  +
        assert_eq!(server.connection_count(), 2);
        1123  +
        1124  +
        drop(metadata);
        1125  +
        drop(first);
        1126  +
        drop(pool);
        1127  +
        server.shutdown().await.expect("clean H2 server shutdown");
        1128  +
    }
  761   1129   
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/tests/tls.rs

@@ -1,1 +88,94 @@
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
#![cfg(any(feature = "__rustls", feature = "s2n-tls",))]
    7      7   
    8      8   
mod common {
    9      9   
    pub(crate) mod tls;
   10     10   
}
   11     11   
   12     12   
use aws_smithy_async::time::SystemTimeSource;
          13  +
#[cfg(all(feature = "rustls-aws-lc", feature = "rt-tokio"))]
          14  +
use aws_smithy_http_client::pool::{Client as PoolClient, ConnectionPool};
   13     15   
use aws_smithy_http_client::tls;
   14     16   
#[cfg(any(feature = "rustls-aws-lc", feature = "s2n-tls"))]
   15     17   
use aws_smithy_http_client::tls::{ServerName, TlsContext};
   16     18   
use aws_smithy_runtime_api::box_error::BoxError;
   17     19   
use aws_smithy_runtime_api::client::http::{HttpClient, HttpConnector, HttpConnectorSettings};
   18     20   
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
   19     21   
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
   20     22   
use aws_smithy_types::byte_stream::ByteStream;
   21     23   
use common::tls as test_tls;
   22     24   
use http_1x::{Method, Request, Response, StatusCode};
   23     25   
use http_body_util::{BodyExt, Full};
   24     26   
use hyper::body::{Bytes, Incoming};
   25     27   
use hyper::service::service_fn;
   26     28   
use hyper_util::rt::{TokioExecutor, TokioIo};
   27     29   
use hyper_util::server::conn::auto::Builder;
   28     30   
use std::net::SocketAddr;
   29     31   
use std::sync::Arc;
   30     32   
use std::time::Duration;
   31     33   
use tokio::net::TcpListener;
   32     34   
use tokio::task::JoinHandle;
   33     35   
use tracing::{debug, error};
   34     36   
   35     37   
struct TestServer {
   36     38   
    _handle: JoinHandle<()>,
   37     39   
    listen_addr: SocketAddr,
   38     40   
    conn_count: Arc<()>,
   39     41   
}
   40     42   
   41     43   
impl TestServer {
   42     44   
    /// Return the number of active connections to this server
   43     45   
    fn conn_count(&self) -> usize {
   44     46   
        // 1 reference for the struct MockProxyServer, 1 reference for the
   45     47   
        // socket task.
   46     48   
        Arc::strong_count(&self.conn_count)
   47     49   
            .checked_sub(2)
   48     50   
            .expect("de-count 2 refs")
   49     51   
    }
   50     52   
}
   51     53   
   52     54   
async fn server() -> Result<TestServer, BoxError> {
          55  +
    server_with_alpn(&[b"h2", b"http/1.1", b"http/1.0"]).await
          56  +
}
          57  +
          58  +
async fn server_with_alpn(alpn_protocols: &[&[u8]]) -> Result<TestServer, BoxError> {
   53     59   
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
   54     60   
    let addr = listener.local_addr().unwrap();
   55     61   
   56     62   
    debug!("Starting to serve on https://{}", addr);
   57     63   
   58         -
    let tls_acceptor = test_tls::server_tls_acceptor(&[b"h2", b"http/1.1", b"http/1.0"])?;
          64  +
    let tls_acceptor = test_tls::server_tls_acceptor(alpn_protocols)?;
   59     65   
    let service = service_fn(echo);
   60     66   
   61     67   
    let conn_count = Arc::new(());
   62     68   
    let server_conn_count = conn_count.clone();
   63     69   
   64     70   
    let server = async move {
   65     71   
        loop {
   66     72   
            let (tcp_stream, remote_addr) = listener.accept().await.unwrap();
   67     73   
            debug!("accepted connection from: {}", remote_addr);
   68     74   
@@ -116,122 +175,252 @@
  136    142   
    let client = aws_smithy_http_client::Builder::new()
  137    143   
        .tls_provider(tls::Provider::Rustls(
  138    144   
            tls::rustls_provider::CryptoMode::AwsLc,
  139    145   
        ))
  140    146   
        .tls_context(test_tls::server_tls_context())
  141    147   
        .build_https();
  142    148   
  143    149   
    run_tls_test(&client).await.unwrap()
  144    150   
}
  145    151   
         152  +
#[cfg(all(feature = "rustls-aws-lc", feature = "rt-tokio"))]
         153  +
#[tokio::test]
         154  +
async fn partitioned_pool_falls_back_to_h1_after_alpn() {
         155  +
    let server = server_with_alpn(&[b"http/1.1"]).await.unwrap();
         156  +
    let pool = ConnectionPool::builder()
         157  +
        .tls_provider(tls::Provider::Rustls(
         158  +
            tls::rustls_provider::CryptoMode::AwsLc,
         159  +
        ))
         160  +
        .tls_context(test_tls::server_tls_context())
         161  +
        .build_https()
         162  +
        .expect("valid HTTPS pool");
         163  +
    let client = PoolClient::new(&pool).expect("anonymous partition exists");
         164  +
    let connector_settings = HttpConnectorSettings::builder().build();
         165  +
    let runtime_components = RuntimeComponentsBuilder::for_tests()
         166  +
        .with_time_source(Some(SystemTimeSource::new()))
         167  +
        .build()
         168  +
        .unwrap();
         169  +
    let connector = client.http_connector(&connector_settings, &runtime_components);
         170  +
    let endpoint = format!("https://localhost:{}/", server.listen_addr.port());
         171  +
         172  +
    for _ in 0..2 {
         173  +
        let mut response = connector
         174  +
            .call(HttpRequest::get(&endpoint).unwrap())
         175  +
            .await
         176  +
            .expect("HTTP/1 request over TLS should succeed");
         177  +
        let body = ByteStream::new(response.take_body())
         178  +
            .collect()
         179  +
            .await
         180  +
            .expect("response body should be readable")
         181  +
            .into_bytes();
         182  +
        assert_eq!(b"Hello TLS!", &body[..]);
         183  +
    }
         184  +
    assert_eq!(1, server.conn_count());
         185  +
}
         186  +
         187  +
#[cfg(all(feature = "rustls-aws-lc", feature = "rt-tokio"))]
         188  +
#[tokio::test]
         189  +
async fn partitioned_pool_narrows_alpn_for_h1_required_request() {
         190  +
    let server = server_with_alpn(&[b"h2", b"http/1.1"]).await.unwrap();
         191  +
    let pool = ConnectionPool::builder()
         192  +
        .tls_provider(tls::Provider::Rustls(
         193  +
            tls::rustls_provider::CryptoMode::AwsLc,
         194  +
        ))
         195  +
        .tls_context(test_tls::server_tls_context())
         196  +
        .build_https()
         197  +
        .expect("valid HTTPS pool");
         198  +
    let client = PoolClient::new(&pool).expect("anonymous partition exists");
         199  +
    let connector_settings = HttpConnectorSettings::builder().build();
         200  +
    let runtime_components = RuntimeComponentsBuilder::for_tests()
         201  +
        .with_time_source(Some(SystemTimeSource::new()))
         202  +
        .build()
         203  +
        .unwrap();
         204  +
    let connector = client.http_connector(&connector_settings, &runtime_components);
         205  +
    let endpoint = format!("https://localhost:{}/", server.listen_addr.port());
         206  +
    let mut request = HttpRequest::new(ByteStream::default().into_inner());
         207  +
    request
         208  +
        .set_method("CONNECT")
         209  +
        .expect("CONNECT is a valid HTTP method");
         210  +
    request
         211  +
        .set_uri(endpoint.as_str())
         212  +
        .expect("valid server URI");
         213  +
         214  +
    let response = connector
         215  +
        .call(request)
         216  +
        .await
         217  +
        .expect("HTTP/1-required request should narrow the ALPN offer");
         218  +
         219  +
    assert_eq!(StatusCode::NOT_FOUND.as_u16(), response.status().as_u16());
         220  +
    assert_eq!(1, server.conn_count());
         221  +
}
         222  +
  146    223   
#[cfg(feature = "rustls-aws-lc")]
  147    224   
#[tokio::test(start_paused = false)]
  148    225   
// can't have paused clock due to <https://github.com/hyperium/hyper/issues/3950>
  149    226   
async fn test_rustls_aws_lc_custom_ca_with_timeout() {
  150    227   
    const TIMEOUT: Duration = Duration::from_secs(10);
  151    228   
    let client = aws_smithy_http_client::Builder::new()
  152    229   
        .pool_idle_timeout(TIMEOUT)
  153    230   
        .tls_provider(tls::Provider::Rustls(
  154    231   
            tls::rustls_provider::CryptoMode::AwsLc,
  155    232   
        ))