AWS SDK

AWS SDK

rev. 39ed07918eb1729475c85ddf93a83d0b1539e928 (ignoring whitespace)

Files changed:

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

@@ -1,1 +40,41 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Partition-bound handles for Smithy HTTP operations.
    7      7   
    8      8   
use super::partition::PartitionId;
    9      9   
use super::registry::PartitionState;
   10     10   
use super::ConnectionPool;
          11  +
use crate::client::downcast_error;
   11     12   
use crate::client::timeout::{self, TimeoutKind};
   12     13   
use crate::sync::Arc;
   13     14   
use aws_smithy_async::rt::sleep::{default_async_sleep, SharedAsyncSleep};
   14     15   
use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
   15     16   
use aws_smithy_runtime_api::client::http::{
   16     17   
    HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
   17     18   
};
   18     19   
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
   19     20   
use aws_smithy_runtime_api::client::result::ConnectorError;
   20     21   
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
@@ -171,172 +245,244 @@
  191    192   
        let read_timeout = self.read_timeout;
  192    193   
        let sleep = self.sleep.clone();
  193    194   
        HttpConnectorFuture::new(async move {
  194    195   
            if (connect_timeout.is_some() || read_timeout.is_some()) && sleep.is_none() {
  195    196   
                return Err(ConnectorError::user(MissingAsyncSleep.into()));
  196    197   
            }
  197    198   
  198    199   
            let request = request.try_into_http1x().map_err(|error| {
  199    200   
                aws_smithy_runtime_api::client::result::ConnectorError::user(error.into())
  200    201   
            })?;
  201         -
            let send = pool.send_request(
  202         -
                partition,
  203         -
                request,
  204         -
                connect_timeout.zip(sleep.clone()).map(|(duration, sleep)| {
  205         -
                    super::establish::TransportTimeout::new(duration, sleep)
  206         -
                }),
  207         -
            );
         202  +
            let options =
         203  +
                super::dispatch::RequestOptions::new(connect_timeout.zip(sleep.clone()).map(
         204  +
                    |(duration, sleep)| super::establish::TransportTimeout::new(duration, sleep),
         205  +
                ));
         206  +
            let send = pool.send_request(partition, request, options);
  208    207   
            let response = timeout::maybe_timeout_future(
  209    208   
                send,
  210    209   
                read_timeout,
  211    210   
                sleep.as_ref(),
  212    211   
                TimeoutKind::Read,
  213    212   
            )
  214    213   
            .await
  215         -
            .map_err(super::super::downcast_error)?;
         214  +
            .map_err(downcast_error)?;
  216    215   
            HttpResponse::try_from(response).map_err(|error| {
  217    216   
                aws_smithy_runtime_api::client::result::ConnectorError::other(error.into(), None)
  218    217   
            })
  219    218   
        })
  220    219   
    }
  221    220   
}
  222    221   
  223    222   
/// Operation timeouts were configured without a runtime timer.
  224    223   
#[derive(Debug)]
  225    224   
struct MissingAsyncSleep;

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

@@ -1,1 +44,43 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Protocol-independent connection lifetime.
    7      7   
//!
    8      8   
//! [`ConnectionState`] serializes dispatch commitment with logical close.
    9      9   
//! [`DispatchGuard`] accounts for accepted requests.
   10         -
//! [`PhysicalConnectionGuard`] follows root I/O until the pool no longer owns
   11         -
//! it, including transfer through a protocol upgrade. This physical transition
   12         -
//! does not assert that the peer or kernel has completed the underlying TCP
   13         -
//! close. Logical close returns bounded capacity without waiting for either
   14         -
//! lifetime to finish.
          10  +
//! [`PhysicalConnectionGuard`] follows root I/O until the client releases its
          11  +
//! physical connection handle, including transfer through a protocol upgrade.
          12  +
//! The operating system may continue TCP teardown afterward. Logical close
          13  +
//! returns bounded capacity without waiting for either lifetime to finish.
   15     14   
   16     15   
use super::admission::CapacityLease;
   17     16   
use super::origin::OriginKey;
   18     17   
use super::partition::PartitionId;
   19     18   
use crate::sync::{Arc, Mutex};
   20     19   
pub use aws_smithy_runtime_api::client::connection::ConnectionId;
   21     20   
use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
   22     21   
use http_1x::Extensions;
   23     22   
use hyper::rt::{Read, ReadBufCursor, Write};
   24     23   
use hyper_util::client::legacy::connect::{Connected, Connection, HttpInfo};
@@ -170,169 +321,320 @@
  190    189   
            NegotiatedProtocol::Http1,
  191    190   
            Connected::new(),
  192    191   
        )
  193    192   
    }
  194    193   
}
  195    194   
  196    195   
/// Shared connection ownership from negotiated transport through close.
  197    196   
pub(super) struct ConnectionState {
  198    197   
    /// Identity and transport facts shared with metadata and lifecycle events.
  199    198   
    info: Arc<ConnectionInfo>,
  200         -
    /// Dispatch, logical-close, and root-I/O completion state.
  201         -
    lifecycle: Mutex<LifecycleState>,
         199  +
    /// Dispatch, logical-close, and physical-connection completion state.
         200  +
    lifecycle: Mutex<ConnectionLifecycle>,
  202    201   
}
  203    202   
  204    203   
/// Connection lifetime state serialized with dispatch commitment and close.
  205    204   
#[derive(Debug)]
  206         -
struct LifecycleState {
         205  +
struct ConnectionLifecycle {
  207    206   
    /// Dispatch eligibility and ownership of bounded capacity.
  208    207   
    logical: LogicalState,
  209    208   
    /// Requests that committed before logical close.
  210    209   
    in_flight: usize,
  211         -
    /// Whether ownership of root transport I/O has ended.
  212         -
    physical_complete: bool,
         210  +
    /// Whether the client has released its physical connection handle.
         211  +
    physical_connection_complete: bool,
  213    212   
}
  214    213   
  215    214   
/// Whether a connection may accept dispatch and still owns bounded capacity.
  216    215   
#[derive(Debug)]
  217    216   
enum LogicalState {
  218    217   
    /// The connector returned connected I/O and selected the HTTP protocol,
  219    218   
    /// but Hyper has not produced the request handle required for dispatch.
  220    219   
    PendingOpen,
  221    220   
    /// Dispatch may commit.
  222    221   
    Open {
  223    222   
        /// Bounded-origin slot released by logical close, when configured.
  224         -
        lease: Option<CapacityLease>,
         223  +
        capacity: Option<CapacityLease>,
  225    224   
    },
  226    225   
    /// New dispatch is rejected while accepted work may still drain.
  227    226   
    Closed {
  228    227   
        /// Reason recorded by the first logical-close transition.
  229    228   
        reason: CloseReason,
  230    229   
    },
  231    230   
}
  232    231   
  233    232   
impl ConnectionState {
  234    233   
    /// Creates state after transport establishment and protocol selection.
  235    234   
    ///
  236    235   
    /// For TLS transports, the connector has completed TLS and ALPN before this
  237    236   
    /// call. Hyper protocol setup and cell installation have not occurred.
  238    237   
    ///
  239         -
    /// The returned guard is the unique physical-lifetime owner. After Hyper
  240         -
    /// returns a request handle, [`Self::open`] attaches optional bounded
  241         -
    /// capacity before cell installation makes the connection discoverable.
         238  +
    /// The returned guard uniquely tracks the client's physical connection
         239  +
    /// handle.
         240  +
    /// After Hyper returns a request handle, [`Self::open`] attaches optional
         241  +
    /// bounded capacity before cell installation makes the connection
         242  +
    /// discoverable.
  242    243   
    pub(super) fn pending_open(info: Arc<ConnectionInfo>) -> (Arc<Self>, PhysicalConnectionGuard) {
  243    244   
        let connection = Arc::new(Self {
  244    245   
            info,
  245         -
            lifecycle: Mutex::new(LifecycleState {
         246  +
            lifecycle: Mutex::new(ConnectionLifecycle {
  246    247   
                logical: LogicalState::PendingOpen,
  247    248   
                in_flight: 0,
  248         -
                physical_complete: false,
         249  +
                physical_connection_complete: false,
  249    250   
            }),
  250    251   
        });
  251    252   
        let physical = PhysicalConnectionGuard {
  252    253   
            connection: connection.clone(),
  253    254   
            active: true,
  254    255   
        };
  255    256   
        (connection, physical)
  256    257   
    }
  257    258   
  258    259   
    /// Opens dispatch commitment and transfers optional bounded capacity.
  259    260   
    ///
  260    261   
    /// The caller performs this transition after Hyper protocol setup and
  261    262   
    /// before publishing the connection through its cell. Opening first
  262    263   
    /// ensures that a newly visible request handle can commit dispatch.
  263    264   
    ///
  264    265   
    /// Returns `lease` when logical close won before opening.
  265    266   
    pub(super) fn open(&self, lease: Option<CapacityLease>) -> Result<(), Option<CapacityLease>> {
  266    267   
        let mut lifecycle = self.lifecycle.lock();
  267    268   
        if !matches!(lifecycle.logical, LogicalState::PendingOpen) {
  268    269   
            return Err(lease);
  269    270   
        }
  270         -
        lifecycle.logical = LogicalState::Open { lease };
         271  +
        lifecycle.logical = LogicalState::Open { capacity: lease };
  271    272   
        Ok(())
  272    273   
    }
  273    274   
  274    275   
    /// Creates a connection whose origin has no admission bound.
  275    276   
    ///
  276         -
    /// The returned guard is the unique physical-lifetime owner and must move
  277         -
    /// with the root I/O task.
         277  +
    /// The returned unique guard must move with the root I/O task.
  278    278   
    #[cfg(test)]
  279    279   
    pub(super) fn unbounded(info: Arc<ConnectionInfo>) -> (Arc<Self>, PhysicalConnectionGuard) {
  280    280   
        let (connection, physical) = Self::pending_open(info);
  281    281   
        connection
  282    282   
            .open(None)
  283    283   
            .expect("new unbounded connection could not open");
  284    284   
        (connection, physical)
  285    285   
    }
  286    286   
  287    287   
    /// Creates a connection that takes ownership of one bounded-origin slot.
  288    288   
    ///
  289         -
    /// The returned guard is the unique physical-lifetime owner and must move
  290         -
    /// with the root I/O task. Logical close returns `lease` independently of
  291         -
    /// that guard.
         289  +
    /// The returned unique guard must move with the root I/O task. Logical
         290  +
    /// close returns `lease` independently of that guard.
  292    291   
    #[cfg(test)]
  293    292   
    pub(super) fn bounded(
  294    293   
        info: Arc<ConnectionInfo>,
  295    294   
        lease: CapacityLease,
  296    295   
    ) -> (Arc<Self>, PhysicalConnectionGuard) {
  297    296   
        let (connection, physical) = Self::pending_open(info);
  298    297   
        connection
  299    298   
            .open(Some(lease))
  300    299   
            .expect("new bounded connection could not open");
  301    300   
        (connection, physical)
@@ -326,325 +612,611 @@
  346    345   
    /// The detached lease is dropped only after the connection lock is
  347    346   
    /// released, so admission and connection locks are never nested.
  348    347   
    pub(super) fn logical_close(&self, reason: CloseReason) -> bool {
  349    348   
        let lease = {
  350    349   
            let mut lifecycle = self.lifecycle.lock();
  351    350   
            match &mut lifecycle.logical {
  352    351   
                LogicalState::PendingOpen => {
  353    352   
                    lifecycle.logical = LogicalState::Closed { reason };
  354    353   
                    None
  355    354   
                }
  356         -
                LogicalState::Open { lease } => {
  357         -
                    let lease = lease.take();
         355  +
                LogicalState::Open { capacity } => {
         356  +
                    let lease = capacity.take();
  358    357   
                    lifecycle.logical = LogicalState::Closed { reason };
  359    358   
                    lease
  360    359   
                }
  361    360   
                LogicalState::Closed { .. } => return false,
  362    361   
            }
  363    362   
        };
  364    363   
        drop(lease);
  365    364   
        tracing::debug!(
  366    365   
            connection_id = %self.id(),
  367    366   
            connection_partition = ?self.owner_partition(),
  368    367   
            protocol = ?self.info.protocol(),
  369    368   
            origin_scheme = %self.info.origin().scheme(),
  370    369   
            origin_host = self.info.origin().host(),
  371    370   
            origin_port = ?self.info.origin().port(),
  372    371   
            close_reason = ?reason,
  373    372   
            "connection logically closed"
  374    373   
        );
  375    374   
        true
  376    375   
    }
  377    376   
  378    377   
    /// Refines a driver-observed close after Hyper confirms an upgrade.
  379    378   
    ///
  380    379   
    /// Hyper may transfer upgraded I/O and complete its HTTP/1 driver in one
  381    380   
    /// poll. The driver can therefore record [`CloseReason::ProtocolClosed`]
  382    381   
    /// before the request task observes the upgrading response. This method
  383    382   
    /// changes that stored reason to [`CloseReason::Upgraded`].
  384    383   
    ///
  385    384   
    /// Returns `true` only when that refinement was applied. It does not close
  386    385   
    /// the connection again, change I/O ownership, or release capacity a second
  387    386   
    /// time.
  388    387   
    pub(super) fn refine_protocol_close_as_upgrade(&self) -> bool {
  389    388   
        let refined = {
  390    389   
            let mut lifecycle = self.lifecycle.lock();
  391    390   
            match &mut lifecycle.logical {
  392    391   
                LogicalState::Closed { reason } if *reason == CloseReason::ProtocolClosed => {
  393    392   
                    *reason = CloseReason::Upgraded;
  394    393   
                    true
  395    394   
                }
  396    395   
                _ => false,
  397    396   
            }
  398    397   
        };
  399    398   
        if refined {
  400    399   
            tracing::debug!(
  401    400   
                connection_id = %self.id(),
  402    401   
                connection_partition = ?self.owner_partition(),
  403    402   
                origin_scheme = %self.info.origin().scheme(),
  404    403   
                origin_host = self.info.origin().host(),
  405    404   
                origin_port = ?self.info.origin().port(),
  406    405   
                previous_close_reason = ?CloseReason::ProtocolClosed,
  407    406   
                close_reason = ?CloseReason::Upgraded,
  408    407   
                "refined connection close reason after HTTP/1 upgrade"
  409    408   
            );
  410    409   
        }
  411    410   
        refined
  412    411   
    }
  413    412   
  414         -
    /// Verifies a close reason at a protocol handoff boundary in debug builds.
  415         -
    #[cfg(debug_assertions)]
         413  +
    /// Verifies a close reason at a protocol handoff boundary in debug builds and tests.
         414  +
    #[cfg(any(debug_assertions, test))]
  416    415   
    pub(super) fn debug_assert_close_reason(&self, expected: CloseReason) {
  417    416   
        let lifecycle = self.lifecycle.lock();
  418    417   
        let actual = match lifecycle.logical {
  419    418   
            LogicalState::PendingOpen => None,
  420    419   
            LogicalState::Open { .. } => None,
  421    420   
            LogicalState::Closed { reason } => Some(reason),
  422    421   
        };
  423    422   
        debug_assert_eq!(Some(expected), actual);
  424    423   
    }
  425    424   
  426    425   
    /// Removes one dispatch previously committed by [`Self::try_commit_dispatch`].
  427         -
    fn finish_dispatch(&self) {
         426  +
    fn release_dispatch(&self) {
  428    427   
        let mut lifecycle = self.lifecycle.lock();
  429    428   
        lifecycle.in_flight = lifecycle
  430    429   
            .in_flight
  431    430   
            .checked_sub(1)
  432    431   
            .expect("completed a dispatch that was not in flight");
  433    432   
    }
  434    433   
  435         -
    /// Records that the connection's root I/O is no longer live.
         434  +
    /// Records that the client released its physical connection handle.
  436    435   
    ///
  437    436   
    /// # Panics
  438    437   
    ///
  439         -
    /// Panics if physical ownership completes more than once.
  440         -
    fn finish_physical(&self) {
         438  +
    /// Panics if physical connection ownership completes more than once.
         439  +
    fn complete_physical_connection(&self) {
  441    440   
        {
  442    441   
            let mut lifecycle = self.lifecycle.lock();
  443    442   
            assert!(
  444         -
                !lifecycle.physical_complete,
         443  +
                !lifecycle.physical_connection_complete,
  445    444   
                "physical connection ownership completed more than once"
  446    445   
            );
  447         -
            lifecycle.physical_complete = true;
         446  +
            lifecycle.physical_connection_complete = true;
  448    447   
        }
  449    448   
        tracing::debug!(
  450    449   
            connection_id = %self.id(),
  451    450   
            connection_partition = ?self.owner_partition(),
  452    451   
            origin_scheme = %self.info.origin().scheme(),
  453    452   
            origin_host = self.info.origin().host(),
  454    453   
            origin_port = ?self.info.origin().port(),
  455         -
            "connection root I/O ownership ended"
         454  +
            "physical connection ownership ended"
  456    455   
        );
  457    456   
    }
  458    457   
  459    458   
    /// Returns a consistent lifecycle snapshot.
  460    459   
    #[cfg(test)]
  461         -
    pub(super) fn snapshot(&self) -> ConnectionSnapshot {
         460  +
    pub(super) fn probe(&self) -> ConnectionProbe {
  462    461   
        let lifecycle = self.lifecycle.lock();
  463         -
        ConnectionSnapshot {
         462  +
        ConnectionProbe {
  464    463   
            close_reason: match lifecycle.logical {
  465    464   
                LogicalState::PendingOpen => None,
  466    465   
                LogicalState::Open { .. } => None,
  467    466   
                LogicalState::Closed { reason } => Some(reason),
  468    467   
            },
  469    468   
            in_flight: lifecycle.in_flight,
  470         -
            physical_complete: lifecycle.physical_complete,
         469  +
            physical_connection_complete: lifecycle.physical_connection_complete,
  471    470   
        }
  472    471   
    }
  473    472   
}
  474    473   
  475    474   
impl fmt::Debug for ConnectionState {
  476    475   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  477    476   
        f.debug_struct("ConnectionState")
  478    477   
            .field("info", &self.info)
  479    478   
            .field("lifecycle", &self.lifecycle)
  480    479   
            .finish()
  481    480   
    }
  482    481   
}
  483    482   
  484    483   
#[cfg(test)]
  485    484   
/// Observable lifecycle state used by protocol coordination and focused tests.
  486    485   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
  487         -
pub(super) struct ConnectionSnapshot {
         486  +
pub(super) struct ConnectionProbe {
  488    487   
    /// Reason logical close won, or `None` while dispatch is accepted.
  489    488   
    pub(super) close_reason: Option<CloseReason>,
  490    489   
    /// Number of dispatches that have not completed.
  491    490   
    pub(super) in_flight: usize,
  492         -
    /// Whether root-I/O ownership has completed.
  493         -
    pub(super) physical_complete: bool,
         491  +
    /// Whether the client released its physical connection handle.
         492  +
    pub(super) physical_connection_complete: bool,
  494    493   
}
  495    494   
  496    495   
/// One dispatch that committed before logical close.
  497    496   
///
  498    497   
/// Dropping the guard records request completion exactly once.
  499    498   
#[derive(Debug)]
  500    499   
pub(super) struct DispatchGuard {
  501    500   
    /// Shared state whose in-flight count this guard owns.
  502    501   
    connection: Arc<ConnectionState>,
  503    502   
    /// Whether `Drop` still owes request completion.
  504    503   
    active: bool,
  505    504   
}
  506    505   
  507    506   
impl DispatchGuard {
  508    507   
    /// Returns the identity of the connection carrying this dispatch.
  509    508   
    #[cfg(test)]
  510    509   
    pub(super) fn connection_id(&self) -> ConnectionId {
  511    510   
        self.connection.id()
  512    511   
    }
  513    512   
  514    513   
    /// Consumes the guard and records request completion immediately.
  515    514   
    ///
  516    515   
    /// Dropping an uncompleted guard performs the same accounting as a
  517    516   
    /// cancellation fallback.
  518         -
    pub(super) fn complete(mut self) {
         517  +
    pub(super) fn release(mut self) {
  519    518   
        self.active = false;
  520         -
        self.connection.finish_dispatch();
         519  +
        self.connection.release_dispatch();
  521    520   
    }
  522    521   
}
  523    522   
  524    523   
impl Drop for DispatchGuard {
  525    524   
    fn drop(&mut self) {
  526    525   
        if self.active {
  527         -
            self.connection.finish_dispatch();
         526  +
            self.connection.release_dispatch();
  528    527   
        }
  529    528   
    }
  530    529   
}
  531    530   
  532         -
/// Unique root-I/O ownership whose drop records the end of pool ownership.
         531  +
/// Tracks the lifetime of the client's physical connection handle.
  533    532   
///
  534    533   
/// The guard is created with the connection and moves with root I/O through
  535         -
/// protocol drain or upgrade. Completion says only that the pool no longer
  536         -
/// owns the root transport; the operating system may continue TCP teardown.
         534  +
/// protocol drain or upgrade. Dropping it means the client no longer owns its
         535  +
/// transport handle; the operating system may continue TCP teardown.
  537    536   
#[derive(Debug)]
  538    537   
pub(super) struct PhysicalConnectionGuard {
  539         -
    /// Shared state whose physical lifetime this guard owns.
         538  +
    /// Shared state whose physical connection lifetime this guard tracks.
  540    539   
    connection: Arc<ConnectionState>,
  541         -
    /// Whether `Drop` still owes physical completion.
         540  +
    /// Whether `Drop` still owes physical connection completion.
  542    541   
    active: bool,
  543    542   
}
  544    543   
  545    544   
impl PhysicalConnectionGuard {
  546         -
    /// Consumes the guard and records root-I/O completion immediately.
         545  +
    /// Consumes the guard and records physical connection completion.
  547    546   
    ///
  548    547   
    /// Dropping an uncompleted guard performs the same transition during task
  549    548   
    /// cancellation or runtime shutdown.
  550    549   
    #[cfg(test)]
  551         -
    pub(super) fn complete(mut self) {
         550  +
    pub(super) fn release(mut self) {
  552    551   
        self.active = false;
  553         -
        self.connection.finish_physical();
         552  +
        self.connection.complete_physical_connection();
  554    553   
    }
  555    554   
}
  556    555   
  557    556   
impl Drop for PhysicalConnectionGuard {
  558    557   
    fn drop(&mut self) {
  559    558   
        if self.active {
  560         -
            self.connection.finish_physical();
         559  +
            self.connection.complete_physical_connection();
  561    560   
        }
  562    561   
    }
  563    562   
}
  564    563   
  565    564   
pin_project! {
  566         -
    /// Root transport wrapper that tracks how long the pool owns the I/O.
         565  +
    /// Root transport wrapper that ties physical connection ownership to I/O.
  567    566   
    ///
  568    567   
    /// The wrapper moves intact through Hyper's driver and H1 upgrade path.
  569         -
    /// Logical close may happen earlier. Dropping the wrapper records that pool
  570         -
    /// ownership ended after the wrapped I/O was destroyed; it does not observe
  571         -
    /// peer or kernel TCP completion.
         568  +
    /// Logical close may happen earlier. Dropping the wrapper records that the
         569  +
    /// client released its physical connection handle after the wrapped I/O
         570  +
    /// was destroyed; the operating system may continue TCP teardown.
  572    571   
    pub(super) struct ConnectionIo<T> {
  573    572   
        #[pin]
  574    573   
        inner: T,
  575    574   
        // Declared after `inner` so transport destruction precedes the
  576         -
        // physical-completion signal.
         575  +
        // physical-connection completion signal.
  577    576   
        physical: PhysicalConnectionGuard,
  578    577   
    }
  579    578   
}
  580    579   
  581    580   
impl<T> ConnectionIo<T> {
  582         -
    /// Attaches the unique physical-lifetime guard to root transport I/O.
         581  +
    /// Attaches physical connection lifetime tracking to root transport I/O.
  583    582   
    pub(super) fn new(inner: T, physical: PhysicalConnectionGuard) -> Self {
  584    583   
        Self { inner, physical }
  585    584   
    }
  586    585   
  587    586   
    /// Returns the wrapped transport.
  588    587   
    #[cfg(test)]
  589    588   
    pub(super) fn get_ref(&self) -> &T {
  590    589   
        &self.inner
  591    590   
    }
  592    591   
}
@@ -641,640 +848,841 @@
  661    660   
mod tests {
  662    661   
    use super::*;
  663    662   
    use crate::client::pool::admission::OriginAdmission;
  664    663   
    use std::num::NonZeroUsize;
  665    664   
  666    665   
    fn test_info(id: u64) -> Arc<ConnectionInfo> {
  667    666   
        ConnectionInfo::for_test(ConnectionId::new(id), PartitionId::from_index(0))
  668    667   
    }
  669    668   
  670    669   
    #[test]
  671         -
    fn logical_close_releases_capacity_before_physical_completion() {
         670  +
    fn logical_close_releases_capacity_before_physical_connection_completion() {
  672    671   
        let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
  673    672   
        let lease = OriginAdmission::lease_for_test(&origin);
  674    673   
        let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
  675    674   
  676    675   
        assert!(connection.logical_close(CloseReason::Reclaimed));
  677    676   
        assert!(!connection.logical_close(CloseReason::PoolDropped));
  678    677   
        assert_eq!(1, origin.available_capacity_for_test());
  679         -
        assert!(!connection.snapshot().physical_complete);
         678  +
        assert!(!connection.probe().physical_connection_complete);
  680    679   
        assert_eq!(
  681    680   
            Some(CloseReason::Reclaimed),
  682         -
            connection.snapshot().close_reason
         681  +
            connection.probe().close_reason
  683    682   
        );
  684    683   
  685    684   
        drop(physical);
  686         -
        assert!(connection.snapshot().physical_complete);
         685  +
        assert!(connection.probe().physical_connection_complete);
  687    686   
    }
  688    687   
  689    688   
    #[test]
  690    689   
    fn upgrade_refines_only_a_driver_observed_protocol_close() {
  691    690   
        let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
  692    691   
        let lease = OriginAdmission::lease_for_test(&origin);
  693    692   
        let (connection, _physical) = ConnectionState::bounded(test_info(1), lease);
  694    693   
  695    694   
        assert!(connection.logical_close(CloseReason::ProtocolClosed));
  696    695   
        assert!(connection.refine_protocol_close_as_upgrade());
  697         -
        assert_eq!(
  698         -
            Some(CloseReason::Upgraded),
  699         -
            connection.snapshot().close_reason
  700         -
        );
         696  +
        assert_eq!(Some(CloseReason::Upgraded), connection.probe().close_reason);
  701    697   
        assert_eq!(1, origin.available_capacity_for_test());
  702    698   
        assert!(!connection.refine_protocol_close_as_upgrade());
  703    699   
  704    700   
        let (poisoned, _physical) = ConnectionState::unbounded(test_info(2));
  705    701   
        assert!(poisoned.logical_close(CloseReason::Poisoned));
  706    702   
        assert!(!poisoned.refine_protocol_close_as_upgrade());
  707         -
        assert_eq!(
  708         -
            Some(CloseReason::Poisoned),
  709         -
            poisoned.snapshot().close_reason
  710         -
        );
         703  +
        assert_eq!(Some(CloseReason::Poisoned), poisoned.probe().close_reason);
  711    704   
    }
  712    705   
  713    706   
    #[test]
  714    707   
    fn committed_dispatch_drains_after_logical_close() {
  715    708   
        let (connection, _physical) = ConnectionState::unbounded(test_info(1));
  716    709   
        let dispatch = ConnectionState::try_commit_dispatch(&connection).unwrap();
  717    710   
        assert_eq!(ConnectionId::new(1), dispatch.connection_id());
  718    711   
  719    712   
        assert!(connection.logical_close(CloseReason::ProtocolClosed));
  720    713   
        assert!(ConnectionState::try_commit_dispatch(&connection).is_none());
  721         -
        assert_eq!(1, connection.snapshot().in_flight);
         714  +
        assert_eq!(1, connection.probe().in_flight);
  722    715   
  723         -
        dispatch.complete();
  724         -
        assert_eq!(0, connection.snapshot().in_flight);
         716  +
        dispatch.release();
         717  +
        assert_eq!(0, connection.probe().in_flight);
  725    718   
    }
  726    719   
  727    720   
    #[test]
  728         -
    fn physical_guard_is_created_once_with_the_connection() {
         721  +
    fn physical_connection_guard_is_created_once_with_the_connection() {
  729    722   
        let (connection, physical) = ConnectionState::unbounded(test_info(1));
  730    723   
        assert_eq!(ConnectionId::new(1), connection.id());
  731    724   
        assert_eq!(PartitionId::from_index(0), connection.owner_partition());
  732         -
        assert!(!connection.snapshot().physical_complete);
  733         -
        physical.complete();
  734         -
        assert!(connection.snapshot().physical_complete);
         725  +
        assert!(!connection.probe().physical_connection_complete);
         726  +
        physical.release();
         727  +
        assert!(connection.probe().physical_connection_complete);
  735    728   
    }
  736    729   
  737    730   
    #[test]
  738    731   
    fn connection_state_retains_immutable_connection_info() {
  739    732   
        #[derive(Clone, Debug, Eq, PartialEq)]
  740    733   
        struct ConnectorMarker(&'static str);
  741    734   
  742    735   
        let origin =
  743    736   
            OriginKey::from_parts(http_1x::uri::Scheme::HTTPS, "example.com", None).unwrap();
  744    737   
        let info = ConnectionInfo::new(
  745    738   
            ConnectionId::new(7),
  746    739   
            origin.clone(),
  747    740   
            PartitionId::from_index(2),
  748    741   
            NegotiatedProtocol::Http1,
  749    742   
            Connected::new()
  750    743   
                .proxy(true)
  751    744   
                .extra(ConnectorMarker("connector-extra")),
  752    745   
        );
  753    746   
        let (connection, _physical) = ConnectionState::unbounded(info);
  754    747   
  755    748   
        assert_eq!(ConnectionId::new(7), connection.info().id());
  756    749   
        assert_eq!(&origin, connection.info().origin());
  757    750   
        assert_eq!(PartitionId::from_index(2), connection.owner_partition());
  758    751   
        assert_eq!(NegotiatedProtocol::Http1, connection.info().protocol());
  759    752   
        assert_eq!(None, connection.info().local_addr());
  760    753   
        assert_eq!(None, connection.info().remote_addr());
  761    754   
        assert!(connection.info().is_proxied());
  762    755   
        let mut extensions = Extensions::new();
  763    756   
        connection.info().apply_connector_extras(&mut extensions);
  764    757   
        assert_eq!(
  765    758   
            Some(&ConnectorMarker("connector-extra")),
  766    759   
            extensions.get::<ConnectorMarker>()
  767    760   
        );
  768    761   
    }
  769    762   
  770    763   
    #[test]
  771         -
    fn root_io_drop_completes_physical_lifetime() {
         764  +
    fn dropping_connection_io_completes_physical_connection_ownership() {
  772    765   
        let (connection, physical) = ConnectionState::unbounded(test_info(1));
  773    766   
        let io = ConnectionIo::new("transport", physical);
  774    767   
        assert_eq!(&"transport", io.get_ref());
  775         -
        assert!(!connection.snapshot().physical_complete);
         768  +
        assert!(!connection.probe().physical_connection_complete);
  776    769   
  777    770   
        drop(io);
  778    771   
  779         -
        assert!(connection.snapshot().physical_complete);
         772  +
        assert!(connection.probe().physical_connection_complete);
  780    773   
    }
  781    774   
}
  782    775   
  783    776   
#[cfg(all(test, smithy_http_client_loom))]
  784    777   
mod loom_tests {
  785    778   
    use super::*;
  786    779   
    use crate::client::pool::admission::OriginAdmission;
  787    780   
    use std::num::NonZeroUsize;
  788    781   
  789    782   
    fn test_info(id: u64) -> Arc<ConnectionInfo> {
  790    783   
        ConnectionInfo::for_test(ConnectionId::new(id), PartitionId::from_index(0))
  791    784   
    }
  792    785   
  793    786   
    #[test]
  794    787   
    fn dispatch_commit_linearizes_against_close() {
  795    788   
        loom::model(|| {
  796    789   
            let (connection, _physical) = ConnectionState::unbounded(test_info(1));
  797    790   
  798    791   
            let dispatch_connection = connection.clone();
  799    792   
            let dispatch = loom::thread::spawn(move || {
  800    793   
                ConnectionState::try_commit_dispatch(&dispatch_connection)
  801    794   
            });
  802    795   
            let close_connection = connection.clone();
  803    796   
            let close = loom::thread::spawn(move || {
  804    797   
                close_connection.logical_close(CloseReason::ProtocolClosed)
  805    798   
            });
  806    799   
  807    800   
            let dispatch = dispatch.join().unwrap();
  808    801   
            assert!(close.join().unwrap());
  809         -
            let snapshot = connection.snapshot();
  810         -
            assert_eq!(Some(CloseReason::ProtocolClosed), snapshot.close_reason);
         802  +
            let probe = connection.probe();
         803  +
            assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
  811    804   
            assert!(ConnectionState::try_commit_dispatch(&connection).is_none());
  812    805   
            match dispatch {
  813    806   
                Some(dispatch) => {
  814         -
                    assert_eq!(1, snapshot.in_flight);
         807  +
                    assert_eq!(1, probe.in_flight);
  815    808   
                    drop(dispatch);
  816         -
                    assert_eq!(0, connection.snapshot().in_flight);
         809  +
                    assert_eq!(0, connection.probe().in_flight);
  817    810   
                }
  818         -
                None => assert_eq!(0, snapshot.in_flight),
         811  +
                None => assert_eq!(0, probe.in_flight),
  819    812   
            }
  820    813   
        });
  821    814   
    }
  822    815   
  823    816   
    #[test]
  824    817   
    fn concurrent_logical_close_releases_one_capacity_lease() {
  825    818   
        loom::model(|| {
  826    819   
            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
  827    820   
            let lease = OriginAdmission::lease_for_test(&origin);
  828    821   
            let (connection, _physical) = ConnectionState::bounded(test_info(1), lease);
  829    822   
            let first_connection = connection.clone();
  830    823   
            let first =
  831    824   
                loom::thread::spawn(move || first_connection.logical_close(CloseReason::Poisoned));
  832    825   
            let second_connection = connection.clone();
  833    826   
            let second = loom::thread::spawn(move || {
  834    827   
                second_connection.logical_close(CloseReason::PoolDropped)
  835    828   
            });
  836    829   
  837    830   
            let first = first.join().unwrap();
  838    831   
            let second = second.join().unwrap();
  839    832   
            assert_ne!(first, second);
  840    833   
            assert_eq!(1, origin.available_capacity_for_test());
  841         -
            let reason = connection.snapshot().close_reason.unwrap();
         834  +
            let reason = connection.probe().close_reason.unwrap();
  842    835   
            assert!(matches!(
  843    836   
                reason,
  844    837   
                CloseReason::Poisoned | CloseReason::PoolDropped
  845    838   
            ));
  846    839   
        });
  847    840   
    }
  848    841   
}

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

@@ -1,1 +384,417 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Request routing and protocol dispatch.
    7      7   
//!
    8         -
//! This module is the request entry point after a [`super::Client`] has
    9         -
//! resolved its partition. It rejects unsupported request forms, resolves the
   10         -
//! origin cell and partition runtime, and owns acquisition across protocol
   11         -
//! selection. Child modules prepare protocol wire forms and retain response
   12         -
//! ownership.
           8  +
//! A [`super::Client`] supplies its partition and one request. This module then
           9  +
//! performs the request journey in four phases:
          10  +
//!
          11  +
//! 1. validate protocol-sensitive request semantics;
          12  +
//! 2. resolve the origin cell and its owner runtime;
          13  +
//! 3. acquire the next authority that can make progress; and
          14  +
//! 4. submit through the selected HTTP protocol.
          15  +
//!
          16  +
//! Acquisition does not necessarily acquire a connection. A request may
          17  +
//! receive an exclusive HTTP/1 sender, an activation on an existing HTTP/2
          18  +
//! generation, or authority to establish a new transport. A waiter event is
          19  +
//! the lower-level transition that supplies one of those outcomes.
          20  +
//!
          21  +
//! Protocol dispatch may return the original request only when Hyper did not
          22  +
//! accept it. The same acquisition remains authoritative while the dispatcher
          23  +
//! selects a replacement; this is not an SDK request retry. Child modules own
          24  +
//! protocol wire preparation and accepted-response lifetime.
   13     25   
   14     26   
mod h1;
   15     27   
mod h2;
   16     28   
   17         -
use self::h1::H1DispatchResult;
   18         -
use self::h2::H2DispatchResult;
          29  +
use self::h1::H1DispatchOutcome;
          30  +
use self::h2::H2DispatchOutcome;
   19     31   
use super::admission::ProtocolRequirement;
   20         -
use super::cell::{AcquisitionEvent, AcquisitionResult, OriginCell, WaiterId};
          32  +
use super::cell::h1::H1Selection;
          33  +
use super::cell::h2::H2Activation;
          34  +
use super::cell::{AcquisitionOutcome, AcquisitionStep, OriginCell, WaiterId};
   21     35   
use super::establish::{self, TransportTimeout};
   22     36   
use super::partition::DriverSpawner;
   23     37   
use super::registry::PartitionState;
   24     38   
use super::{ConnectionPool, PoolInner};
   25     39   
use crate::sync::Arc;
   26     40   
use aws_smithy_runtime_api::client::result::ConnectorError;
   27     41   
use aws_smithy_types::body::SdkBody;
   28     42   
use http_1x::{header, Method, Request, Response, Uri, Version};
   29     43   
use std::future::poll_fn;
   30     44   
use std::sync::Arc as StdArc;
   31     45   
   32     46   
/// Replacement selections allowed after the initial HTTP/2 dispatch attempt.
   33         -
const MAX_H2_REACQUISITIONS: usize = 2;
          47  +
const MAX_H2_REPLACEMENTS: usize = 2;
          48  +
          49  +
/// Operation settings known before the request's origin cell is resolved.
          50  +
///
          51  +
/// This value is separate from [`AcquisitionContext`], which also contains
          52  +
/// cell and runtime state discovered by dispatch.
          53  +
#[derive(Clone, Debug, Default)]
          54  +
pub(super) struct RequestOptions {
          55  +
    /// Transport connection timeout configured for this operation.
          56  +
    connect_timeout: Option<TransportTimeout>,
          57  +
}
          58  +
          59  +
impl RequestOptions {
          60  +
    /// Creates operation settings for one pool request.
          61  +
    pub(super) fn new(connect_timeout: Option<TransportTimeout>) -> Self {
          62  +
        Self { connect_timeout }
          63  +
    }
          64  +
}
   34     65   
   35     66   
/// Stable state shared by acquisition and protocol dispatch for one request.
   36     67   
///
   37     68   
/// This context owns ordinary shared configuration and identities. Linear
   38     69   
/// acquisition values, such as an establishment permit or an exclusive
   39     70   
/// protocol handle, remain separate arguments so their transfer is explicit.
   40     71   
#[derive(Clone)]
   41     72   
pub(super) struct AcquisitionContext {
   42     73   
    /// Shared pool policy and transport construction.
   43     74   
    pub(super) pool: Arc<PoolInner>,
   44     75   
    /// Partition from which the request was issued.
   45     76   
    pub(super) partition: Arc<PartitionState>,
   46     77   
    /// Partition-local state for the request's canonical origin.
   47     78   
    pub(super) cell: Arc<OriginCell>,
   48     79   
    /// Absolute request URI retained across protocol wire-form changes.
   49     80   
    pub(super) absolute_uri: Uri,
   50     81   
    /// Runtime that owns establishment, drivers, and pending return work.
   51     82   
    pub(super) owner_spawner: StdArc<dyn DriverSpawner>,
   52     83   
    /// Transport connection timeout configured for this operation.
   53     84   
    pub(super) connect_timeout: Option<TransportTimeout>,
   54     85   
}
   55     86   
   56         -
impl ConnectionPool {
   57         -
    /// Resolves common acquisition state and dispatches one request.
   58         -
    pub(super) async fn send_request(
   59         -
        &self,
          87  +
/// Resolves request-scoped pool state and runs acquisition through dispatch.
          88  +
pub(super) async fn send(
          89  +
    pool: &ConnectionPool,
   60     90   
    partition: Arc<PartitionState>,
   61     91   
    request: Request<SdkBody>,
   62         -
        connect_timeout: Option<TransportTimeout>,
   63         -
    ) -> Result<Response<SdkBody>, ConnectorError> {
          92  +
    options: RequestOptions,
          93  +
) -> Result<Response<SdkBody>, ConnectorError> {
   64     94   
    validate_request_before_acquisition(&request)
   65     95   
        .map_err(|error| ConnectorError::user(error.into()))?;
   66     96   
   67     97   
    let absolute_uri = request.uri().clone();
   68         -
        let cell = self
          98  +
    let cell = pool
   69     99   
        .inner
   70    100   
        .registry
   71    101   
        .resolve_cell(&partition, &absolute_uri)
   72    102   
        .map_err(|error| ConnectorError::user(error.into()))?;
   73    103   
    tracing::trace!(
   74    104   
        request_partition = ?partition.id(),
   75    105   
        origin_scheme = %cell.id().origin().scheme(),
   76    106   
        origin_host = cell.id().origin().host(),
   77    107   
        origin_port = ?cell.id().origin().port(),
   78    108   
        "request resolved to connection-pool cell"
   79    109   
    );
   80    110   
   81    111   
    let owner_spawner = partition
   82    112   
        .owner_spawner()
   83    113   
        .map_err(|error| ConnectorError::user(error.into()))?;
   84    114   
    partition.ensure_maintenance_started(owner_spawner.as_ref());
   85    115   
   86    116   
    let context = AcquisitionContext {
   87         -
            pool: self.inner.clone(),
         117  +
        pool: pool.inner.clone(),
   88    118   
        partition,
   89    119   
        cell,
   90    120   
        absolute_uri,
   91    121   
        owner_spawner,
   92         -
            connect_timeout,
         122  +
        connect_timeout: options.connect_timeout,
   93    123   
    };
   94    124   
   95         -
        self.dispatch_acquired(context, request).await
   96         -
    }
         125  +
    acquire_and_dispatch(context, request).await
         126  +
}
         127  +
         128  +
/// Request authority ready for one protocol-specific dispatch attempt.
         129  +
enum DispatchTarget {
         130  +
    /// Exclusive HTTP/1 sender ownership.
         131  +
    H1(H1Selection),
         132  +
    /// Prospective request reservation on one exact HTTP/2 generation.
         133  +
    H2(H2Activation),
         134  +
}
   97    135   
   98         -
    /// Acquires a compatible protocol value and dispatches the request.
   99         -
    ///
  100         -
    /// Reacquisition is allowed only while a protocol dispatcher returns the
  101         -
    /// original request envelope.
  102         -
    async fn dispatch_acquired(
  103         -
        &self,
         136  +
/// Acquires protocol dispatch authority and submits the request to Hyper.
         137  +
///
         138  +
/// A protocol dispatcher may return the original request only when Hyper did
         139  +
/// not accept it. After one HTTP/2 target reaches dispatch, at most two stale
         140  +
/// replacements may return the request for another selection.
         141  +
async fn acquire_and_dispatch(
  104    142   
    context: AcquisitionContext,
  105    143   
    mut request: Request<SdkBody>,
  106         -
    ) -> Result<Response<SdkBody>, ConnectorError> {
         144  +
) -> Result<Response<SdkBody>, ConnectorError> {
  107    145   
    let requirement = protocol_requirement(&request);
  108         -
        let mut h2_reacquisitions = H2ReacquisitionBudget::default();
         146  +
    let mut h2_replacements = H2ReplacementBudget::default();
  109    147   
  110    148   
    loop {
  111         -
            match Self::acquire(&context, requirement).await? {
  112         -
                AcquisitionResult::H1(selection) => {
  113         -
                    match self.dispatch_h1(&context, request, selection).await? {
  114         -
                        H1DispatchResult::Response(response) => return Ok(response),
  115         -
                        H1DispatchResult::Reacquire(returned) => request = returned,
  116         -
                    }
  117         -
                }
  118         -
                AcquisitionResult::H2(activation) => {
  119         -
                    match self.dispatch_h2(&context, request, activation).await? {
  120         -
                        H2DispatchResult::Response(response) => return Ok(response),
  121         -
                        H2DispatchResult::Reacquire(reacquisition) => {
  122         -
                            let (returned, error) = reacquisition.into_parts();
  123         -
                            if !h2_reacquisitions.admit_replacement() {
         149  +
        match acquire_for_dispatch(&context, requirement).await? {
         150  +
            DispatchTarget::H1(selection) => {
         151  +
                match h1::dispatch(&context, request, selection).await? {
         152  +
                    H1DispatchOutcome::Response(response) => return Ok(response),
         153  +
                    H1DispatchOutcome::NotAccepted(returned) => request = returned,
         154  +
                }
         155  +
            }
         156  +
            DispatchTarget::H2(activation) => {
         157  +
                match h2::dispatch(&context, request, activation).await? {
         158  +
                    H2DispatchOutcome::Response(response) => return Ok(response),
         159  +
                    H2DispatchOutcome::NotAccepted(unaccepted) => {
         160  +
                        let (returned, error) = unaccepted.into_parts();
         161  +
                        if !h2_replacements.admit_replacement() {
  124    162   
                            return Err(error);
  125    163   
                        }
  126    164   
                        request = returned;
  127    165   
                    }
  128    166   
                }
  129    167   
            }
  130         -
                AcquisitionResult::Failed(_) => {
  131         -
                    unreachable!("failed acquisition returned as a successful result")
  132         -
                }
  133         -
                AcquisitionResult::Reacquire => {
  134         -
                    unreachable!("internal reacquisition escaped the acquisition loop")
  135         -
                }
  136         -
            }
  137    168   
        }
  138    169   
    }
         170  +
}
  139    171   
  140         -
    /// Acquires one protocol value compatible with the request.
  141         -
    ///
  142         -
    /// Local reusable values complete immediately. Otherwise one waiter
  143         -
    /// remains registered while it receives a reusable protocol value, an
  144         -
    /// establishment failure, or authority to start establishment.
  145         -
    async fn acquire(
         172  +
/// Acquires the next protocol value that can dispatch this request.
         173  +
///
         174  +
/// Local reusable values complete immediately under the cell lock. On a miss,
         175  +
/// one waiter remains registered while reuse and establishment race to supply
         176  +
/// dispatch authority. `poll_waiter` performs one synchronous state poll; no
         177  +
/// cell lock is retained while this future is pending.
         178  +
async fn acquire_for_dispatch(
  146    179   
    context: &AcquisitionContext,
  147    180   
    requirement: ProtocolRequirement,
  148         -
    ) -> Result<AcquisitionResult, ConnectorError> {
         181  +
) -> Result<DispatchTarget, ConnectorError> {
  149    182   
    'acquire: loop {
  150    183   
        if requirement.accepts_h2() {
  151    184   
            if let Some(activation) = OriginCell::select_h2(&context.cell) {
  152    185   
                tracing::trace!(
  153    186   
                    connection_id = %activation.connection().id(),
  154    187   
                    request_partition = ?context.partition.id(),
  155    188   
                    connection_partition = ?activation.connection().owner_partition(),
  156    189   
                    origin_scheme = %context.cell.id().origin().scheme(),
  157    190   
                    origin_host = context.cell.id().origin().host(),
  158    191   
                    origin_port = ?context.cell.id().origin().port(),
  159    192   
                    "HTTP/2 pool hit; activating local generation"
  160    193   
                );
  161         -
                    return Ok(AcquisitionResult::H2(activation));
         194  +
                return Ok(DispatchTarget::H2(activation));
  162    195   
            }
  163    196   
        }
  164    197   
        if requirement.accepts_h1() {
  165    198   
            if let Some(selection) = OriginCell::select_h1(&context.cell) {
  166    199   
                tracing::trace!(
  167    200   
                    connection_id = %selection.connection_id(),
  168    201   
                    request_partition = ?context.partition.id(),
  169    202   
                    connection_partition = ?selection.connection().owner_partition(),
  170    203   
                    origin_scheme = %selection.connection().info().origin().scheme(),
  171    204   
                    origin_host = selection.connection().info().origin().host(),
  172    205   
                    origin_port = ?selection.connection().info().origin().port(),
  173    206   
                    "HTTP/1 pool hit; reusing idle connection"
  174    207   
                );
  175         -
                    return Ok(AcquisitionResult::H1(selection));
         208  +
                return Ok(DispatchTarget::H1(selection));
  176    209   
            }
  177    210   
        }
  178    211   
  179    212   
        let waiter = OriginCell::register_waiter(&context.cell, requirement);
  180    213   
        tracing::trace!(
  181    214   
            request_partition = ?context.partition.id(),
  182    215   
            origin_scheme = %context.cell.id().origin().scheme(),
  183    216   
            origin_host = context.cell.id().origin().host(),
  184    217   
            origin_port = ?context.cell.id().origin().port(),
  185    218   
            protocol_requirement = ?requirement,
  186    219   
            "connection acquisition queued"
  187    220   
        );
  188         -
            let mut waiter_guard = WaiterGuard::new(context.cell.clone(), waiter);
         221  +
        let mut waiter_guard = WaiterCancellationGuard::new(context.cell.clone(), waiter);
  189    222   
        loop {
  190    223   
            match poll_fn(|cx| context.cell.poll_waiter(waiter, cx)).await {
  191         -
                    AcquisitionEvent::Complete(AcquisitionResult::H1(selection)) => {
         224  +
                AcquisitionStep::Resolved(AcquisitionOutcome::H1(selection)) => {
  192    225   
                    waiter_guard.disarm();
  193         -
                        return Ok(AcquisitionResult::H1(selection));
         226  +
                    return Ok(DispatchTarget::H1(selection));
  194    227   
                }
  195         -
                    AcquisitionEvent::Complete(AcquisitionResult::H2(activation)) => {
         228  +
                AcquisitionStep::Resolved(AcquisitionOutcome::H2(activation)) => {
  196    229   
                    waiter_guard.disarm();
  197         -
                        OriginCell::service_h2_waiters(&context.cell);
  198         -
                        OriginCell::service_peer_h2_waiters(&context.cell);
         230  +
                    OriginCell::offer_local_h2(&context.cell);
         231  +
                    OriginCell::offer_peer_h2(&context.cell);
  199    232   
                    tracing::trace!(
  200    233   
                        connection_id = %activation.connection().id(),
  201    234   
                        request_partition = ?context.partition.id(),
  202    235   
                        connection_partition = ?activation.connection().owner_partition(),
  203    236   
                        origin_scheme = %context.cell.id().origin().scheme(),
  204    237   
                        origin_host = context.cell.id().origin().host(),
  205    238   
                        origin_port = ?context.cell.id().origin().port(),
  206    239   
                        "HTTP/2 acquisition completed"
  207    240   
                    );
  208         -
                        return Ok(AcquisitionResult::H2(activation));
         241  +
                    return Ok(DispatchTarget::H2(activation));
  209    242   
                }
  210         -
                    AcquisitionEvent::Complete(AcquisitionResult::Failed(error)) => {
         243  +
                AcquisitionStep::Resolved(AcquisitionOutcome::Failed(error)) => {
  211    244   
                    waiter_guard.disarm();
  212    245   
                    return Err(error);
  213    246   
                }
  214         -
                    AcquisitionEvent::Complete(AcquisitionResult::Reacquire) => {
         247  +
                AcquisitionStep::Resolved(AcquisitionOutcome::RetryAcquisition) => {
  215    248   
                    waiter_guard.disarm();
  216    249   
                    continue 'acquire;
  217    250   
                }
  218         -
                    AcquisitionEvent::Establish(permit) => {
         251  +
                AcquisitionStep::StartEstablishment(permit) => {
  219    252   
                    tracing::trace!(
  220    253   
                        request_partition = ?context.partition.id(),
  221    254   
                        origin_scheme = %context.cell.id().origin().scheme(),
  222    255   
                        origin_host = context.cell.id().origin().host(),
  223    256   
                        origin_port = ?context.cell.id().origin().port(),
  224    257   
                        "connection establishment starting"
  225    258   
                    );
  226    259   
                    let attempt =
  227    260   
                        establish::establish(context.clone(), waiter, permit, requirement);
  228    261   
                    let completion =
  229    262   
                        EstablishmentCompletionGuard::new(context.cell.clone(), waiter);
  230    263   
                    context.owner_spawner.spawn(Box::pin(async move {
  231    264   
                        let mut completion = completion;
  232    265   
                        if !completion.start() {
  233    266   
                            drop(attempt);
  234    267   
                            completion.disarm();
  235    268   
                            return;
  236    269   
                        }
  237    270   
                        match attempt.await {
  238    271   
                            establish::EstablishmentOutcome::Complete(result) => {
  239    272   
                                completion.complete(result);
  240    273   
                            }
  241         -
                                establish::EstablishmentOutcome::Transferred => completion.disarm(),
         274  +
                            establish::EstablishmentOutcome::WaiterCompletionTransferred => {
         275  +
                                completion.disarm();
         276  +
                            }
  242    277   
                        }
  243    278   
                    }));
  244    279   
                }
  245    280   
            }
  246    281   
        }
  247    282   
    }
  248         -
    }
  249    283   
}
  250         -
  251    284   
/// Per-request bound on replacement HTTP/2 selections.
  252    285   
#[derive(Default)]
  253         -
struct H2ReacquisitionBudget {
         286  +
struct H2ReplacementBudget {
  254    287   
    /// Replacement selections admitted after the initial selection.
  255         -
    completed: usize,
         288  +
    used: usize,
  256    289   
}
  257    290   
  258         -
impl H2ReacquisitionBudget {
         291  +
impl H2ReplacementBudget {
  259    292   
    /// Returns whether one more replacement selection may proceed.
  260    293   
    fn admit_replacement(&mut self) -> bool {
  261         -
        if self.completed >= MAX_H2_REACQUISITIONS {
         294  +
        if self.used >= MAX_H2_REPLACEMENTS {
  262    295   
            return false;
  263    296   
        }
  264         -
        self.completed += 1;
         297  +
        self.used += 1;
  265    298   
        true
  266    299   
    }
  267    300   
}
  268    301   
  269    302   
/// Guarantees a terminal result for one submitted establishment attempt.
  270    303   
///
  271    304   
/// The guard enters the owner-runtime future before submission. Runtime task
  272    305   
/// drop reports failure to the waiter. A result that wins before the submitted
  273    306   
/// future starts disarms the guard without polling the connector.
  274    307   
struct EstablishmentCompletionGuard {
  275    308   
    cell: Arc<OriginCell>,
  276    309   
    waiter: WaiterId,
  277    310   
    active: bool,
  278    311   
}
  279    312   
  280    313   
impl EstablishmentCompletionGuard {
  281    314   
    fn new(cell: Arc<OriginCell>, waiter: WaiterId) -> Self {
  282    315   
        Self {
  283    316   
            cell,
  284    317   
            waiter,
  285    318   
            active: true,
  286    319   
        }
  287    320   
    }
  288    321   
  289    322   
    fn start(&self) -> bool {
  290    323   
        self.cell.start_establishment(self.waiter)
  291    324   
    }
  292    325   
  293         -
    fn complete(mut self, result: AcquisitionResult) {
         326  +
    fn complete(mut self, result: AcquisitionOutcome) {
  294    327   
        self.active = false;
  295    328   
        self.cell.complete_establishment(self.waiter, result);
  296    329   
    }
  297    330   
  298    331   
    fn disarm(&mut self) {
  299    332   
        self.active = false;
  300    333   
    }
  301    334   
}
  302    335   
  303    336   
impl Drop for EstablishmentCompletionGuard {
  304    337   
    fn drop(&mut self) {
  305    338   
        if self.active {
  306    339   
            let error = ConnectorError::io(EstablishmentTaskDropped.into());
  307    340   
            self.cell
  308         -
                .complete_establishment(self.waiter, AcquisitionResult::Failed(error));
         341  +
                .complete_establishment(self.waiter, AcquisitionOutcome::Failed(error));
  309    342   
            tracing::debug!(
  310    343   
                request_partition = ?self.cell.id().partition(),
  311    344   
                connection_partition = ?self.cell.id().partition(),
  312    345   
                origin_scheme = %self.cell.id().origin().scheme(),
  313    346   
                origin_host = self.cell.id().origin().host(),
  314    347   
                origin_port = ?self.cell.id().origin().port(),
  315    348   
                "connection establishment task dropped"
  316    349   
            );
  317    350   
        }
  318    351   
    }
  319    352   
}
  320    353   
  321    354   
/// The owner runtime discarded an establishment future before completion.
  322    355   
#[derive(Debug)]
  323    356   
struct EstablishmentTaskDropped;
  324    357   
  325    358   
impl std::fmt::Display for EstablishmentTaskDropped {
  326    359   
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  327    360   
        f.write_str("the owner-runtime connection establishment task was dropped")
  328    361   
    }
  329    362   
}
  330    363   
  331    364   
impl std::error::Error for EstablishmentTaskDropped {}
  332    365   
  333         -
/// Cancels a request's waiter until it consumes a terminal acquisition result.
  334         -
struct WaiterGuard {
         366  +
/// Cancels a request's waiter until it consumes a terminal acquisition outcome.
         367  +
struct WaiterCancellationGuard {
  335    368   
    cell: Arc<OriginCell>,
  336    369   
    waiter: WaiterId,
  337    370   
    active: bool,
  338    371   
}
  339    372   
  340         -
impl WaiterGuard {
         373  +
impl WaiterCancellationGuard {
  341    374   
    fn new(cell: Arc<OriginCell>, waiter: WaiterId) -> Self {
  342    375   
        Self {
  343    376   
            cell,
  344    377   
            waiter,
  345    378   
            active: true,
  346    379   
        }
  347    380   
    }
  348    381   
  349    382   
    fn disarm(&mut self) {
  350    383   
        self.active = false;
  351    384   
    }
  352    385   
}
  353    386   
  354         -
impl Drop for WaiterGuard {
         387  +
impl Drop for WaiterCancellationGuard {
  355    388   
    fn drop(&mut self) {
  356    389   
        if self.active {
  357    390   
            OriginCell::cancel_waiter(&self.cell, self.waiter);
  358    391   
        }
  359    392   
    }
  360    393   
}
  361    394   
  362    395   
/// Rejects request forms the pool cannot route before acquiring connection state.
  363    396   
fn validate_request_before_acquisition(
  364    397   
    request: &Request<SdkBody>,
@@ -493,526 +530,563 @@
  513    546   
        upgrade
  514    547   
            .headers_mut()
  515    548   
            .insert(header::UPGRADE, "websocket".parse().unwrap());
  516    549   
        assert!(matches!(
  517    550   
            validate_request_before_acquisition(&upgrade),
  518    551   
            Err(RequestDispatchError::UpgradeOverHttp2)
  519    552   
        ));
  520    553   
    }
  521    554   
  522    555   
    #[test]
  523         -
    fn h2_reacquisition_is_bounded_after_two_replacements() {
  524         -
        let mut budget = H2ReacquisitionBudget::default();
  525         -
        for _ in 0..MAX_H2_REACQUISITIONS {
         556  +
    fn h2_replacement_is_bounded_after_two_selections() {
         557  +
        let mut budget = H2ReplacementBudget::default();
         558  +
        for _ in 0..MAX_H2_REPLACEMENTS {
  526    559   
            assert!(budget.admit_replacement());
  527    560   
        }
  528    561   
        assert!(!budget.admit_replacement());
  529    562   
    }
  530    563   
}

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

@@ -1,1 +248,239 @@
    8      8   
//! The parent dispatcher has already validated the request, resolved its
    9      9   
//! origin cell, acquired an HTTP/1 selection, and selected the partition
   10     10   
//! runtime. This module prepares the HTTP/1 wire form and owns the exclusive
   11     11   
//! sender until the response reaches a reusable message boundary. Every
   12     12   
//! cancellation path returns the sender to its connection cell or retires the
   13     13   
//! connection.
   14     14   
   15     15   
use super::super::cell::h1::{H1Exchange, H1Selection};
   16     16   
use super::super::connection::{CloseReason, ConnectionState, DispatchGuard};
   17     17   
use super::super::partition::DriverSpawner;
   18         -
use super::super::ConnectionPool;
   19     18   
use super::{AcquisitionContext, H1HostHeaderInserted, RequestDispatchError};
          19  +
use crate::client::downcast_error;
   20     20   
use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
   21     21   
use aws_smithy_runtime_api::client::result::ConnectorError;
   22     22   
use aws_smithy_types::body::SdkBody;
   23     23   
use http_1x::{Method, Request, Response, Uri, Version};
   24     24   
use hyper::body::Body;
   25     25   
use std::future::poll_fn;
   26     26   
use std::pin::Pin;
   27     27   
use std::sync::Arc as StdArc;
   28     28   
use std::task::{Context, Poll};
   29     29   
   30     30   
/// Result of one HTTP/1 dispatch attempt while the pool owns the request.
   31         -
pub(super) enum H1DispatchResult {
          31  +
pub(super) enum H1DispatchOutcome {
   32     32   
    /// Hyper accepted the request and produced a guarded response.
   33     33   
    Response(Response<SdkBody>),
   34     34   
    /// The selection became stale or Hyper returned the request unsent.
   35         -
    Reacquire(Request<SdkBody>),
          35  +
    NotAccepted(Request<SdkBody>),
   36     36   
}
   37     37   
   38         -
impl ConnectionPool {
   39         -
    /// Attempts one dispatch through an acquired HTTP/1 selection.
   40         -
    ///
   41         -
    /// The parent may reacquire only when this function returns the original
   42         -
    /// request. A fresh connection's send error is terminal even if Hyper
   43         -
    /// returns the request.
   44         -
    pub(super) async fn dispatch_h1(
   45         -
        &self,
          38  +
/// Attempts one dispatch through an acquired HTTP/1 selection.
          39  +
///
          40  +
/// The parent may reacquire only when this function returns the original
          41  +
/// request. A fresh connection's send error is terminal even if Hyper returns
          42  +
/// the request.
          43  +
pub(super) async fn dispatch(
   46     44   
    context: &AcquisitionContext,
   47     45   
    mut request: Request<SdkBody>,
   48     46   
    mut selection: H1Selection,
   49         -
    ) -> Result<H1DispatchResult, ConnectorError> {
          47  +
) -> Result<H1DispatchOutcome, ConnectorError> {
   50     48   
    let request_method = request.method().clone();
   51     49   
    let reused = selection.is_reused();
   52     50   
    let connection = selection.connection().clone();
   53     51   
    let close_handle = selection.close_handle();
   54     52   
    let captured_metadata = request
   55     53   
        .extensions()
   56     54   
        .get::<CaptureSmithyConnection>()
   57     55   
        .cloned()
   58     56   
        .map(|capture| {
   59     57   
            let metadata = connection.info().metadata(close_handle.clone());
   60     58   
            let captured = metadata.clone();
   61     59   
            capture.set_connection_retriever(move || Some(captured.clone()));
   62     60   
            metadata
   63     61   
        });
   64     62   
   65     63   
    if request.version() == Version::HTTP_2 {
   66     64   
        let metadata =
   67     65   
            captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
   68     66   
        return Err(ConnectorError::user(
   69     67   
            "an HTTP/2 request cannot use an HTTP/1 connection".into(),
   70     68   
        )
   71     69   
        .with_connection(metadata));
   72     70   
    }
   73     71   
   74     72   
    add_host_header(&mut request, &context.absolute_uri)
   75     73   
        .map_err(|error| ConnectorError::user(error.into()))?;
   76     74   
    rewrite_h1_request_target(&mut request, connection.info().is_proxied());
   77     75   
   78     76   
    // Commit against logical close before transferring the request to
   79     77   
    // Hyper. A close that wins this race leaves the request untouched.
   80     78   
    let Some(dispatch) = ConnectionState::try_commit_dispatch(&connection) else {
   81     79   
        tracing::trace!(
   82     80   
            connection_id = %connection.id(),
   83     81   
            request_partition = ?context.partition.id(),
   84     82   
            connection_partition = ?connection.owner_partition(),
   85     83   
            origin_scheme = %connection.info().origin().scheme(),
   86     84   
            origin_host = connection.info().origin().host(),
   87     85   
            origin_port = ?connection.info().origin().port(),
   88     86   
            "HTTP/1 selection became stale before dispatch"
   89     87   
        );
   90     88   
        *request.uri_mut() = context.absolute_uri.clone();
   91     89   
        selection.retire_connection(CloseReason::ProtocolClosed);
   92         -
            return Ok(H1DispatchResult::Reacquire(request));
          90  +
        return Ok(H1DispatchOutcome::NotAccepted(request));
   93     91   
    };
   94     92   
    let send = selection.sender_mut().hyper_mut().try_send_request(request);
   95     93   
   96     94   
    let exchange = selection.into_exchange();
   97     95   
    match send.await {
   98     96   
        Ok(mut response) => {
   99     97   
            // Response-body ownership keeps both the accepted dispatch
  100     98   
            // and exclusive request handle out of the pool until Hyper
  101     99   
            // proves a complete message boundary.
  102    100   
            connection
  103    101   
                .info()
  104    102   
                .apply_connector_extras(response.extensions_mut());
  105         -
                Ok(H1DispatchResult::Response(guard_response(
         103  +
            Ok(H1DispatchOutcome::Response(guard_h1_response(
  106    104   
                response,
  107    105   
                request_method,
  108    106   
                exchange,
  109    107   
                dispatch,
  110    108   
                context.owner_spawner.clone(),
  111    109   
            )))
  112    110   
        }
  113    111   
        Err(mut error) => {
  114    112   
            if let Some(mut returned) = error.take_message() {
  115    113   
                exchange.retire_connection(CloseReason::ProtocolClosed);
  116    114   
                drop(dispatch);
  117    115   
                if reused {
  118    116   
                    tracing::trace!(
  119    117   
                        connection_id = %connection.id(),
  120    118   
                        request_partition = ?context.partition.id(),
  121    119   
                        connection_partition = ?connection.owner_partition(),
  122    120   
                        origin_scheme = %connection.info().origin().scheme(),
  123    121   
                        origin_host = connection.info().origin().host(),
  124    122   
                        origin_port = ?connection.info().origin().port(),
  125    123   
                        "reused HTTP/1 connection returned request unsent"
  126    124   
                    );
  127    125   
                    *returned.uri_mut() = context.absolute_uri.clone();
  128         -
                        return Ok(H1DispatchResult::Reacquire(returned));
         126  +
                    return Ok(H1DispatchOutcome::NotAccepted(returned));
  129    127   
                }
  130         -
                    let metadata = captured_metadata
  131         -
                        .unwrap_or_else(|| connection.info().metadata(close_handle));
  132         -
                    return Err(
  133         -
                        super::super::super::downcast_error(Box::new(error.into_error()))
  134         -
                            .with_connection(metadata),
  135         -
                    );
         128  +
                let metadata =
         129  +
                    captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
         130  +
                return Err(downcast_error(Box::new(error.into_error())).with_connection(metadata));
  136    131   
            }
  137    132   
            exchange.retire_connection(CloseReason::IncompleteH1Exchange);
  138    133   
            drop(dispatch);
  139    134   
            let metadata =
  140    135   
                captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
  141         -
                Err(
  142         -
                    super::super::super::downcast_error(Box::new(error.into_error()))
  143         -
                        .with_connection(metadata),
  144         -
                )
  145         -
            }
         136  +
            Err(downcast_error(Box::new(error.into_error())).with_connection(metadata))
  146    137   
        }
  147    138   
    }
  148    139   
}
  149    140   
  150    141   
/// Attaches connection return and dispatch completion to an accepted response.
  151    142   
///
  152    143   
/// An ordinary response retains both guards until Hyper proves a complete
  153    144   
/// message boundary. Successful CONNECT and `101` responses retire the pool
  154    145   
/// record before the response exposes Hyper's upgraded root I/O.
  155         -
fn guard_response(
         146  +
fn guard_h1_response(
  156    147   
    response: Response<hyper::body::Incoming>,
  157    148   
    method: Method,
  158    149   
    exchange: H1Exchange,
  159    150   
    dispatch: DispatchGuard,
  160    151   
    spawner: StdArc<dyn DriverSpawner>,
  161    152   
) -> Response<SdkBody> {
  162    153   
    let upgrade = response.status() == http_1x::StatusCode::SWITCHING_PROTOCOLS
  163    154   
        || (method == Method::CONNECT && response.status().is_success());
  164    155   
    let (parts, body) = response.into_parts();
  165    156   
    if upgrade {
  166    157   
        exchange.retire_connection(CloseReason::Upgraded);
  167         -
        dispatch.complete();
         158  +
        dispatch.release();
  168    159   
        return Response::from_parts(parts, SdkBody::from_body_1_x(body));
  169    160   
    }
  170    161   
    let body = H1ResponseBody::new(body, exchange, dispatch, spawner);
  171    162   
    Response::from_parts(parts, SdkBody::from_body_1_x(body))
  172    163   
}
  173    164   
  174    165   
/// Response body that retains the HTTP/1 exchange until its message boundary.
  175    166   
///
  176    167   
/// End-of-stream returns a ready request handle to pool policy. A body error,
  177    168   
/// or dropping the body before Hyper reports end-of-stream, retires the
  178    169   
/// connection instead.
  179    170   
struct H1ResponseBody {
  180    171   
    /// Hyper response body whose terminal state determines message completion.
  181    172   
    inner: hyper::body::Incoming,
  182    173   
    /// Sender and dispatch ownership until one terminal body observation.
  183    174   
    lifecycle: Option<H1ResponseLifecycle>,
  184    175   
}
  185    176   
  186    177   
impl H1ResponseBody {
  187    178   
    /// Wraps a response and immediately completes a body already at end stream.
  188    179   
    fn new(
  189    180   
        inner: hyper::body::Incoming,
  190    181   
        exchange: H1Exchange,
  191    182   
        dispatch: DispatchGuard,
  192    183   
        spawner: StdArc<dyn DriverSpawner>,
  193    184   
    ) -> Self {
  194    185   
        let mut body = Self {
  195    186   
            inner,
  196    187   
            lifecycle: Some(H1ResponseLifecycle {
  197    188   
                exchange: Some(exchange),
  198    189   
                dispatch: Some(dispatch),
  199    190   
                spawner,
  200    191   
            }),
  201    192   
        };
  202    193   
        if body.inner.is_end_stream() {
  203    194   
            body.finish_without_context();
  204    195   
        }
  205    196   
        body
  206    197   
    }
  207    198   
  208    199   
    /// Completes response ownership using the current body-task waker.
  209    200   
    fn finish_with_context(&mut self, cx: &mut Context<'_>) {
  210    201   
        if let Some(lifecycle) = self.lifecycle.take() {
  211         -
            lifecycle.finish(Some(cx));
         202  +
            lifecycle.resolve(Some(cx));
  212    203   
        }
  213    204   
    }
  214    205   
  215    206   
    /// Completes response ownership without a task context when already ready.
  216    207   
    fn finish_without_context(&mut self) {
  217    208   
        if let Some(lifecycle) = self.lifecycle.take() {
  218         -
            lifecycle.finish(None);
         209  +
            lifecycle.resolve(None);
  219    210   
        }
  220    211   
    }
  221    212   
  222    213   
    /// Retires an exchange whose response body ended with an error.
  223    214   
    fn fail(&mut self) {
  224    215   
        if let Some(mut lifecycle) = self.lifecycle.take() {
  225    216   
            if let Some(exchange) = lifecycle.exchange.take() {
  226    217   
                exchange.retire_connection(CloseReason::IncompleteH1Exchange);
  227    218   
            }
  228    219   
            drop(lifecycle.dispatch.take());
@@ -278,269 +366,357 @@
  298    289   
    /// Exchange retired as owner-runtime shutdown if the task is dropped.
  299    290   
    exchange: Option<H1Exchange>,
  300    291   
}
  301    292   
  302    293   
impl H1ResponseLifecycle {
  303    294   
    /// Returns or retires the request handle and completes dispatch accounting.
  304    295   
    ///
  305    296   
    /// Readiness is first polled with the response body's task context when one
  306    297   
    /// is available. Pending readiness moves to an owner-runtime task so the
  307    298   
    /// completed body does not retain connection ownership.
  308         -
    fn finish(mut self, cx: Option<&mut Context<'_>>) {
         299  +
    fn resolve(mut self, cx: Option<&mut Context<'_>>) {
  309    300   
        let mut exchange = self
  310    301   
            .exchange
  311    302   
            .take()
  312    303   
            .expect("HTTP/1 response lifecycle lost its exchange");
  313    304   
        let ready = match cx {
  314    305   
            Some(cx) => exchange.poll_ready(cx),
  315    306   
            None if exchange.is_ready() => Poll::Ready(Ok(())),
  316    307   
            None => Poll::Pending,
  317    308   
        };
  318    309   
        match ready {
  319    310   
            Poll::Ready(Ok(())) => {
  320    311   
                exchange.offer_for_reuse();
  321    312   
            }
  322    313   
            Poll::Ready(Err(_)) => {
  323    314   
                exchange.retire_connection(CloseReason::ProtocolClosed);
  324    315   
            }
  325    316   
            Poll::Pending => {
  326    317   
                let mut readiness = H1ReadinessTask::new(exchange);
  327    318   
                self.spawner.spawn(Box::pin(async move {
  328    319   
                    match poll_fn(|cx| readiness.poll_ready(cx)).await {
  329    320   
                        Ok(()) => readiness.reuse(),
  330    321   
                        Err(_) => readiness.retire_connection(CloseReason::ProtocolClosed),
  331    322   
                    }
  332    323   
                }));
  333    324   
            }
  334    325   
        }
  335    326   
        if let Some(dispatch) = self.dispatch.take() {
  336         -
            dispatch.complete();
         327  +
            dispatch.release();
  337    328   
        }
  338    329   
    }
  339    330   
}
  340    331   
  341    332   
impl H1ReadinessTask {
  342    333   
    /// Arms owner-runtime fallback around one pending exchange.
  343    334   
    fn new(exchange: H1Exchange) -> Self {
  344    335   
        Self {
  345    336   
            exchange: Some(exchange),
  346    337   
        }
@@ -408,399 +467,459 @@
  428    419   
    parts.path_and_query = request.uri().path_and_query().cloned();
  429    420   
    *request.uri_mut() = Uri::from_parts(parts).expect("request path and query are a valid URI");
  430    421   
}
  431    422   
  432    423   
#[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
  433    424   
mod tests {
  434    425   
    use super::*;
  435    426   
    use crate::client::pool::cell::h1::H1Sender;
  436    427   
    use crate::client::pool::cell::OriginCell;
  437    428   
    use crate::client::pool::connection::ConnectionInfo;
         429  +
    use crate::client::pool::dispatch::RequestOptions;
  438    430   
    use crate::client::pool::registry::PartitionState;
  439    431   
    use crate::client::pool::{
  440    432   
        Client, ConnectionId, ConnectionPool, ConnectionReuseScope, Partition, PartitionId,
  441    433   
        TokioDriverSpawner,
  442    434   
    };
  443    435   
    use crate::client::timeout::test::{NeverConnects, NeverReplies};
  444    436   
    use crate::sync::Arc;
  445    437   
    use aws_smithy_async::rt::sleep::TokioSleep;
  446    438   
    use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
  447    439   
    use aws_smithy_runtime_api::client::http::{HttpClient, HttpConnector, HttpConnectorSettings};
@@ -605,597 +692,684 @@
  625    617   
            .build_http()
  626    618   
            .unwrap();
  627    619   
        let partition = anonymous_partition(&pool);
  628    620   
        let uri = "http://example.com/".parse().unwrap();
  629    621   
        let cell = pool.inner.registry.resolve_cell(&partition, &uri).unwrap();
  630    622   
        let (connection, _physical) = ConnectionState::unbounded(ConnectionInfo::for_test(
  631    623   
            ConnectionId::new(1),
  632    624   
            PartitionId::ANONYMOUS,
  633    625   
        ));
  634    626   
        let selection =
  635         -
            OriginCell::install_selected_h1(&cell, connection.clone(), H1Sender::test(11));
         627  +
            OriginCell::insert_selected_h1(&cell, connection.clone(), H1Sender::test(11));
  636    628   
  637    629   
        drop(H1ReadinessTask::new(selection.into_exchange()));
  638    630   
  639    631   
        assert_eq!(
  640    632   
            Some(CloseReason::OwnerRuntimeShutdown),
  641         -
            connection.snapshot().close_reason
         633  +
            connection.probe().close_reason
  642    634   
        );
  643    635   
    }
  644    636   
  645    637   
    #[tokio::test]
  646    638   
    async fn dropped_owner_task_completes_the_establishment_waiter() {
  647    639   
        let pool = ConnectionPool::builder()
  648    640   
            .idle_timeout(None)
  649    641   
            .partitions([Partition::new(PartitionId::from_index(7), DroppingSpawner)])
  650    642   
            .build_http()
  651    643   
            .unwrap();
  652    644   
        let partition = pool
  653    645   
            .inner
  654    646   
            .registry
  655    647   
            .partition(PartitionId::from_index(7))
  656    648   
            .unwrap();
  657    649   
        let request = Request::get("http://127.0.0.1:9/")
  658    650   
            .body(SdkBody::empty())
  659    651   
            .unwrap();
  660    652   
  661    653   
        let error = pool
  662         -
            .send_request(partition, request, None)
         654  +
            .send_request(partition, request, RequestOptions::default())
  663    655   
            .await
  664    656   
            .expect_err("dropped establishment task did not fail the request");
  665    657   
        assert!(error.is_io());
  666    658   
        assert_eq!(
  667    659   
            "the owner-runtime connection establishment task was dropped",
  668    660   
            std::error::Error::source(&error).unwrap().to_string()
  669    661   
        );
  670    662   
    }
  671    663   
  672    664   
    struct TestServer {
@@ -734,726 +794,786 @@
  754    746   
    }
  755    747   
  756    748   
    async fn request(
  757    749   
        pool: &ConnectionPool,
  758    750   
        partition: Arc<PartitionState>,
  759    751   
        uri: Uri,
  760    752   
    ) -> Response<SdkBody> {
  761    753   
        pool.send_request(
  762    754   
            partition,
  763    755   
            Request::get(uri).body(SdkBody::empty()).unwrap(),
  764         -
            None,
         756  +
            RequestOptions::default(),
  765    757   
        )
  766    758   
        .await
  767    759   
        .unwrap()
  768    760   
    }
  769    761   
  770    762   
    async fn consume(response: Response<SdkBody>) -> hyper::body::Bytes {
  771    763   
        response.into_body().collect().await.unwrap().to_bytes()
  772    764   
    }
  773    765   
  774    766   
    #[test]
@@ -918,910 +1007,999 @@
  938    930   
        let exchange = selection.into_exchange();
  939    931   
        assert!(exchange.is_ready());
  940    932   
  941    933   
        H1ResponseLifecycle {
  942    934   
            exchange: Some(exchange),
  943    935   
            dispatch: Some(dispatch),
  944    936   
            spawner: StdArc::new(CountingSpawner {
  945    937   
                submitted: submitted.clone(),
  946    938   
            }),
  947    939   
        }
  948         -
        .finish(None);
         940  +
        .resolve(None);
  949    941   
  950    942   
        assert_eq!(baseline, submitted.load(Ordering::SeqCst));
  951    943   
    }
  952    944   
  953    945   
    #[tokio::test]
  954    946   
    async fn cancelling_launched_establishment_returns_its_connection_to_a_later_waiter() {
  955    947   
        let server = TestServer::start().await;
  956    948   
        let started = StdArc::new(Notify::new());
  957    949   
        let release = StdArc::new(Notify::new());
  958    950   
        let calls = StdArc::new(AtomicUsize::new(0));
  959    951   
        let pool = super::super::super::builder::Builder::default()
  960    952   
            .idle_timeout(None)
  961    953   
            .max_connections_per_host(1)
  962    954   
            .build_with_connector(PausedConnector {
  963    955   
                started: started.clone(),
  964    956   
                release: release.clone(),
  965    957   
                calls: calls.clone(),
  966    958   
            })
  967    959   
            .unwrap();
  968    960   
        let partition = anonymous_partition(&pool);
  969    961   
        let first_pool = pool.clone();
  970    962   
        let first_partition = partition.clone();
  971    963   
        let first_uri = server.uri("/cancelled-establishment");
  972    964   
        let first = tokio::spawn(async move {
  973    965   
            first_pool
  974    966   
                .send_request(
  975    967   
                    first_partition,
  976    968   
                    Request::get(first_uri).body(SdkBody::empty()).unwrap(),
  977         -
                    None,
         969  +
                    RequestOptions::default(),
  978    970   
                )
  979    971   
                .await
  980    972   
        });
  981    973   
        started.notified().await;
  982    974   
  983    975   
        first.abort();
  984    976   
        assert!(first.await.unwrap_err().is_cancelled());
  985    977   
        release.notify_one();
  986    978   
  987    979   
        let response = tokio::time::timeout(
@@ -1010,1002 +1086,1078 @@
 1030   1022   
            .unwrap();
 1031   1023   
        let partition = anonymous_partition(&pool);
 1032   1024   
        let first_pool = pool.clone();
 1033   1025   
        let first_partition = partition.clone();
 1034   1026   
        let first_uri: Uri = format!("{endpoint}/cancelled-exchange").parse().unwrap();
 1035   1027   
        let first = tokio::spawn(async move {
 1036   1028   
            first_pool
 1037   1029   
                .send_request(
 1038   1030   
                    first_partition,
 1039   1031   
                    Request::get(first_uri).body(SdkBody::empty()).unwrap(),
 1040         -
                    None,
        1032  +
                    RequestOptions::default(),
 1041   1033   
                )
 1042   1034   
                .await
 1043   1035   
        });
 1044   1036   
        first_request.notified().await;
 1045   1037   
        let cell = pool
 1046   1038   
            .inner
 1047   1039   
            .registry
 1048   1040   
            .resolve_cell(&partition, &format!("{endpoint}/").parse().unwrap())
 1049   1041   
            .unwrap();
 1050   1042   
        let connection = cell.only_h1_connection_for_test();
 1051   1043   
 1052   1044   
        first.abort();
 1053   1045   
        assert!(first.await.unwrap_err().is_cancelled());
 1054   1046   
        assert_eq!(
 1055   1047   
            Some(CloseReason::IncompleteH1Exchange),
 1056         -
            connection.snapshot().close_reason
        1048  +
            connection.probe().close_reason
 1057   1049   
        );
 1058   1050   
 1059   1051   
        let second_uri: Uri = format!("{endpoint}/after-cancelled-exchange")
 1060   1052   
            .parse()
 1061   1053   
            .unwrap();
 1062   1054   
        let second = tokio::time::timeout(
 1063   1055   
            std::time::Duration::from_secs(1),
 1064   1056   
            request(&pool, partition, second_uri),
 1065   1057   
        )
 1066   1058   
        .await
@@ -1143,1135 +1311,1303 @@
 1163   1155   
            .unwrap();
 1164   1156   
        let partition = anonymous_partition(&pool);
 1165   1157   
        let capture = CaptureSmithyConnection::new();
 1166   1158   
        let mut h2_request = Request::get(server.uri("/h2"))
 1167   1159   
            .version(Version::HTTP_2)
 1168   1160   
            .body(SdkBody::empty())
 1169   1161   
            .unwrap();
 1170   1162   
        h2_request.extensions_mut().insert(capture.clone());
 1171   1163   
 1172   1164   
        let error = pool
 1173         -
            .send_request(partition.clone(), h2_request, None)
        1165  +
            .send_request(partition.clone(), h2_request, RequestOptions::default())
 1174   1166   
            .await
 1175   1167   
            .expect_err("an HTTP/2 request unexpectedly used HTTP/1");
 1176   1168   
        assert!(error.is_user());
 1177   1169   
        let error_connection = error
 1178   1170   
            .connection_metadata()
 1179   1171   
            .expect("the mismatch error omitted connection metadata");
 1180   1172   
        let captured_connection = capture
 1181   1173   
            .get()
 1182   1174   
            .expect("the mismatch request did not capture its connection");
 1183   1175   
        assert_eq!(
 1184   1176   
            error_connection.connection_id(),
 1185   1177   
            captured_connection.connection_id()
 1186   1178   
        );
 1187   1179   
 1188   1180   
        let response = request(&pool, partition, server.uri("/h1")).await;
 1189   1181   
        assert_eq!(
 1190   1182   
            hyper::body::Bytes::from_static(b"ok"),
 1191   1183   
            consume(response).await
 1192   1184   
        );
 1193   1185   
        assert_eq!(
 1194   1186   
            1,
 1195   1187   
            server.accepted(),
 1196   1188   
            "the rejected HTTP/2 request discarded a reusable HTTP/1 connection"
 1197   1189   
        );
 1198   1190   
    }
 1199   1191   
 1200   1192   
    #[tokio::test]
 1201   1193   
    async fn captured_metadata_poisons_only_selected_generation() {
 1202   1194   
        let server = TestServer::start().await;
 1203   1195   
        let pool = ConnectionPool::builder().build_http().unwrap();
 1204   1196   
        let partition = anonymous_partition(&pool);
 1205   1197   
        let capture = CaptureSmithyConnection::new();
 1206   1198   
        let mut first = Request::get(server.uri("/first"))
 1207   1199   
            .body(SdkBody::empty())
 1208   1200   
            .unwrap();
 1209   1201   
        first.extensions_mut().insert(capture.clone());
 1210   1202   
        let response = pool
 1211         -
            .send_request(partition.clone(), first, None)
        1203  +
            .send_request(partition.clone(), first, RequestOptions::default())
 1212   1204   
            .await
 1213   1205   
            .unwrap();
 1214   1206   
        consume(response).await;
 1215   1207   
 1216   1208   
        let metadata = capture.get().expect("connection metadata was captured");
 1217   1209   
        assert!(metadata.connection_id().is_some());
 1218   1210   
        metadata.poison();
 1219   1211   
 1220   1212   
        consume(request(&pool, partition.clone(), server.uri("/second")).await).await;
 1221   1213   
        assert_eq!(2, server.accepted());
 1222   1214   
 1223   1215   
        metadata.poison();
 1224   1216   
        consume(request(&pool, partition, server.uri("/third")).await).await;
 1225   1217   
        assert_eq!(
 1226   1218   
            2,
 1227   1219   
            server.accepted(),
 1228   1220   
            "stale metadata poisoned the newer connection generation"
 1229   1221   
        );
 1230   1222   
    }
 1231   1223   
 1232   1224   
    #[tokio::test]
 1233   1225   
    async fn connection_id_exhaustion_is_an_internal_error() {
 1234   1226   
        let server = TestServer::start().await;
 1235   1227   
        let pool = ConnectionPool::builder().build_http().unwrap();
 1236   1228   
        pool.inner
 1237   1229   
            .next_connection_id
 1238   1230   
            .store(u64::MAX, Ordering::Relaxed);
 1239   1231   
        let partition = anonymous_partition(&pool);
 1240   1232   
        let request = Request::get(server.uri("/id-exhausted"))
 1241   1233   
            .body(SdkBody::empty())
 1242   1234   
            .unwrap();
 1243   1235   
 1244   1236   
        let error = pool
 1245         -
            .send_request(partition, request, None)
        1237  +
            .send_request(partition, request, RequestOptions::default())
 1246   1238   
            .await
 1247   1239   
            .expect_err("exhausted connection identifiers unexpectedly established a connection");
 1248   1240   
        assert!(error.is_other());
 1249   1241   
        assert_eq!(
 1250   1242   
            "connection identifier space exhausted",
 1251   1243   
            std::error::Error::source(&error).unwrap().to_string()
 1252   1244   
        );
 1253   1245   
    }
 1254   1246   
 1255   1247   
    #[tokio::test]
 1256   1248   
    async fn explicit_clients_resolve_declared_partitions() {
 1257   1249   
        let pool = ConnectionPool::builder()
 1258   1250   
            .partitions([Partition::new(
 1259   1251   
                PartitionId::from_index(7),
 1260   1252   
                TokioDriverSpawner::current(),
 1261   1253   
            )])
 1262   1254   
            .build_http()
 1263   1255   
            .unwrap();
 1264   1256   
 1265   1257   
        assert!(Client::new(&pool).is_err());
 1266   1258   
        assert!(Client::from_partition(&pool, PartitionId::from_index(7)).is_ok());
 1267   1259   
        let error = Client::from_partition(&pool, PartitionId::from_index(8)).unwrap_err();
 1268   1260   
        assert_eq!(Some(PartitionId::from_index(8)), error.partition());
 1269   1261   
    }
 1270   1262   
 1271   1263   
    #[test]
 1272   1264   
    fn anonymous_partition_outside_tokio_is_a_user_error() {
 1273   1265   
        let pool = ConnectionPool::builder()
 1274   1266   
            .idle_timeout(None)
 1275   1267   
            .build_http()
 1276   1268   
            .unwrap();
 1277   1269   
        let partition = anonymous_partition(&pool);
 1278   1270   
        let request = Request::get("http://example.com/")
 1279   1271   
            .body(SdkBody::empty())
 1280   1272   
            .unwrap();
 1281         -
        let mut send = Box::pin(pool.send_request(partition, request, None));
        1273  +
        let mut send = Box::pin(pool.send_request(partition, request, RequestOptions::default()));
 1282   1274   
        let waker = std::task::Waker::noop();
 1283   1275   
        let mut context = Context::from_waker(waker);
 1284   1276   
 1285   1277   
        let Poll::Ready(Err(error)) = send.as_mut().poll(&mut context) else {
 1286   1278   
            panic!("missing anonymous runtime did not fail on first poll");
 1287   1279   
        };
 1288   1280   
        assert!(error.is_user());
 1289   1281   
        assert_eq!(
 1290   1282   
            "the anonymous connection-pool partition requires an active Tokio runtime on first use",
 1291   1283   
            std::error::Error::source(&error).unwrap().to_string()
@@ -1414,1406 +1474,1466 @@
 1434   1426   
        consume(request(&pool, matching, server.uri("/matching-interface")).await).await;
 1435   1427   
        assert_eq!(
 1436   1428   
            1,
 1437   1429   
            server.accepted(),
 1438   1430   
            "matching interface groups did not share the existing connection"
 1439   1431   
        );
 1440   1432   
 1441   1433   
        consume(request(&pool, mismatched, server.uri("/mismatched-interface")).await).await;
 1442   1434   
        assert_eq!(
 1443   1435   
            Some(CloseReason::Reclaimed),
 1444         -
            first_connection.snapshot().close_reason
        1436  +
            first_connection.probe().close_reason
 1445   1437   
        );
 1446   1438   
        assert_eq!(
 1447   1439   
            2,
 1448   1440   
            server.accepted(),
 1449   1441   
            "mismatched interface groups did not reclaim bounded capacity"
 1450   1442   
        );
 1451   1443   
    }
 1452   1444   
 1453   1445   
    #[tokio::test]
 1454   1446   
    async fn smithy_client_boundary_sends_through_the_shared_pool() {
@@ -1542,1534 +1631,1627 @@
 1562   1554   
                .headers_mut()
 1563   1555   
                .insert(http_1x::header::CONNECTION, "upgrade".parse().unwrap());
 1564   1556   
            upgrade_request
 1565   1557   
                .headers_mut()
 1566   1558   
                .insert(http_1x::header::UPGRADE, "test".parse().unwrap());
 1567   1559   
        }
 1568   1560   
        let request_pool = pool.clone();
 1569   1561   
        let request_partition = partition.clone();
 1570   1562   
        let response = tokio::spawn(async move {
 1571   1563   
            request_pool
 1572         -
                .send_request(request_partition, upgrade_request, None)
        1564  +
                .send_request(
        1565  +
                    request_partition,
        1566  +
                    upgrade_request,
        1567  +
                    RequestOptions::default(),
        1568  +
                )
 1573   1569   
                .await
 1574   1570   
                .unwrap()
 1575   1571   
        });
 1576   1572   
        request_received.notified().await;
 1577   1573   
        let cell = pool
 1578   1574   
            .inner
 1579   1575   
            .registry
 1580   1576   
            .resolve_cell(&partition, &upgrade_uri)
 1581   1577   
            .unwrap();
 1582   1578   
        let connection = cell.only_h1_connection_for_test();
 1583   1579   
        release_response.notify_one();
 1584   1580   
        let mut response = response.await.unwrap();
 1585   1581   
        assert_eq!(expected_status, response.status());
 1586   1582   
 1587   1583   
        let upgraded = tokio::time::timeout(
 1588   1584   
            std::time::Duration::from_secs(1),
 1589   1585   
            hyper::upgrade::on(&mut response),
 1590   1586   
        )
 1591   1587   
        .await
 1592   1588   
        .expect("upgrade timed out")
 1593   1589   
        .expect("upgrade failed");
 1594   1590   
        let mut upgraded = hyper_util::rt::TokioIo::new(upgraded);
 1595   1591   
        let mut greeting = [0_u8; 5];
 1596   1592   
        upgraded.read_exact(&mut greeting).await.unwrap();
 1597   1593   
        assert_eq!(b"hello", &greeting);
 1598   1594   
 1599   1595   
        assert_eq!(
 1600   1596   
            Some(CloseReason::Upgraded),
 1601         -
            connection.snapshot().close_reason,
        1597  +
            connection.probe().close_reason,
 1602   1598   
            "the upgraded HTTP/1 sender returned to pool policy"
 1603   1599   
        );
 1604   1600   
        assert_eq!(1, cell.admission().unwrap().available_capacity_for_test());
 1605   1601   
 1606   1602   
        let second_uri: Uri = format!("{endpoint}/after").parse().unwrap();
 1607   1603   
        let second = tokio::time::timeout(
 1608   1604   
            std::time::Duration::from_secs(1),
 1609   1605   
            request(&pool, partition, second_uri),
 1610   1606   
        )
 1611   1607   
        .await

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

@@ -1,1 +642,639 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! HTTP/2 dispatch and two-ended request completion.
    7      7   
//!
    8         -
//! [`H2Activation`] arrives with a prospective generation lease and a transient
           8  +
//! [`H2Activation`] arrives with prospective request authority and a transient
    9      9   
//! sender cloned from the connection-owning cell. Dispatch first checks sender
   10     10   
//! and logical-connection state, then polls Hyper once. A request returned from
   11     11   
//! that poll was not accepted and may re-enter protocol acquisition.
   12     12   
//!
   13         -
//! After Hyper accepts the request, the activation becomes an accepted lease.
   14         -
//! [`H2RequestBody`] owns its send endpoint and [`H2ResponseBody`] owns its
   15         -
//! receive endpoint. Either may finish first; the generation request count is
          13  +
//! After Hyper accepts the request, the activation becomes an accepted claim.
          14  +
//! [`H2RequestBody`] owns the upload guard and [`H2ResponseBody`] owns the
          15  +
//! response guard. Either may finish first; the generation request count is
   16     16   
//! released only after both finish. A returned request re-arms the existing
   17     17   
//! body wrapper rather than nesting wrappers around the original body.
   18     18   
   19         -
use super::super::cell::h2::{H2Activation, H2CloseHandle, H2DispatchParts, H2LeaseEndpoint};
   20         -
use super::super::connection::ConnectionState;
   21         -
use super::super::ConnectionPool;
          19  +
use super::super::cell::h2::{
          20  +
    H2Activation, H2CloseHandle, H2DispatchParts, H2ResponseGuard, H2UploadGuard,
          21  +
};
          22  +
use super::super::connection::{CloseReason, ConnectionState};
   22     23   
use super::{AcquisitionContext, H1HostHeaderInserted};
          24  +
use crate::client::downcast_error;
   23     25   
use crate::sync::{Arc, Mutex};
   24     26   
use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
   25     27   
use aws_smithy_runtime_api::client::result::ConnectorError;
   26     28   
use aws_smithy_types::body::SdkBody;
   27     29   
use http_1x::{Request, Response};
   28     30   
use hyper::body::Body;
   29     31   
use std::future::{poll_fn, Future};
   30     32   
use std::pin::Pin;
   31     33   
use std::task::{Context, Poll};
   32     34   
   33     35   
/// Result of one checked HTTP/2 dispatch attempt.
   34     36   
#[allow(
   35     37   
    clippy::large_enum_variant,
   36     38   
    reason = "boxing the successful response would allocate on every HTTP/2 request"
   37     39   
)]
   38         -
pub(super) enum H2DispatchResult {
          40  +
pub(super) enum H2DispatchOutcome {
   39     41   
    /// Hyper accepted the request and produced a guarded response.
   40     42   
    Response(Response<SdkBody>),
   41     43   
    /// Hyper did not accept the request, so it may re-enter acquisition.
   42         -
    Reacquire(Box<H2Reacquisition>),
          44  +
    NotAccepted(Box<H2UnacceptedRequest>),
   43     45   
}
   44     46   
   45     47   
/// Request and terminal fallback retained for one replacement selection.
   46         -
pub(super) struct H2Reacquisition {
          48  +
pub(super) struct H2UnacceptedRequest {
   47     49   
    /// Original request Hyper did not accept.
   48     50   
    request: Request<SdkBody>,
   49     51   
    /// Error returned if the request exhausts its replacement budget.
   50         -
    error: ConnectorError,
          52  +
    terminal_error: ConnectorError,
   51     53   
}
   52     54   
   53         -
impl H2Reacquisition {
          55  +
impl H2UnacceptedRequest {
   54     56   
    /// Returns the original request and its terminal fallback.
   55     57   
    pub(super) fn into_parts(self: Box<Self>) -> (Request<SdkBody>, ConnectorError) {
   56         -
        let Self { request, error } = *self;
   57         -
        (request, error)
          58  +
        let Self {
          59  +
            request,
          60  +
            terminal_error,
          61  +
        } = *self;
          62  +
        (request, terminal_error)
   58     63   
    }
   59     64   
}
   60     65   
   61     66   
/// State retained after Hyper accepts one request.
   62     67   
struct H2AcceptedDispatch {
   63     68   
    /// Connection retained through response creation.
   64     69   
    connection: Arc<ConnectionState>,
   65     70   
    /// Generation close authority used for connection-wide failures.
   66     71   
    close: H2CloseHandle,
   67     72   
    /// Metadata captured before the request moved into Hyper.
   68     73   
    captured_metadata: Option<aws_smithy_runtime_api::client::connection::ConnectionMetadata>,
   69         -
    /// Receive endpoint transferred to the guarded response body.
   70         -
    receive_endpoint: H2LeaseEndpoint,
          74  +
    /// Response guard transferred to the guarded response body.
          75  +
    response_guard: H2ResponseGuard,
   71     76   
    /// Whether the generation accepted an earlier request.
   72     77   
    reused: bool,
   73     78   
}
   74     79   
   75         -
impl ConnectionPool {
   76         -
    /// Dispatches one request through a prospective H2 generation lease.
   77         -
    ///
   78         -
    /// Pool-side staleness reacquires before Hyper sees the request. Once
   79         -
    /// Hyper returns an envelope, only a reused generation may reacquire.
   80         -
    pub(super) async fn dispatch_h2(
   81         -
        &self,
          80  +
/// Dispatches one request through prospective H2 request authority.
          81  +
///
          82  +
/// Pool-side staleness reacquires before Hyper sees the request. Once Hyper
          83  +
/// returns an envelope, only a reused generation may reacquire.
          84  +
pub(super) async fn dispatch(
   82     85   
    context: &AcquisitionContext,
   83     86   
    mut request: Request<SdkBody>,
   84     87   
    mut activation: H2Activation,
   85         -
    ) -> Result<H2DispatchResult, ConnectorError> {
          88  +
) -> Result<H2DispatchOutcome, ConnectorError> {
   86     89   
    prepare_h2_request(&mut request, &context.absolute_uri);
   87     90   
    let connection = activation.connection().clone();
   88     91   
    let reused = activation.is_reused();
   89     92   
    let close = activation.close_handle();
   90     93   
    let captured_metadata = request
   91     94   
        .extensions()
   92     95   
        .get::<CaptureSmithyConnection>()
   93     96   
        .cloned()
   94     97   
        .map(|capture| {
   95     98   
            let metadata = connection.info().h2_metadata(close.clone());
   96     99   
            let captured = metadata.clone();
   97    100   
            capture.set_connection_retriever(move || Some(captured.clone()));
   98    101   
            metadata
   99    102   
        });
  100    103   
  101    104   
    let H2DispatchParts {
  102    105   
        mut sender,
  103         -
            send_endpoint,
  104         -
            receive_endpoint,
         106  +
        upload,
         107  +
        response,
  105    108   
    } = activation.take_dispatch_parts();
  106    109   
    if sender.is_closed() {
  107         -
            close.close(super::super::connection::CloseReason::ProtocolClosed);
  108         -
            let metadata =
  109         -
                captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  110         -
            return finish_unaccepted_request(
         110  +
        close.close(CloseReason::ProtocolClosed);
         111  +
        let metadata = captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
         112  +
        return resolve_unaccepted_request(
  111    113   
            request,
  112    114   
            reused,
  113    115   
            UnacceptedStage::BeforeHyper,
  114    116   
            ConnectorError::other(H2ConnectionClosedBeforeDispatch.into(), None)
  115    117   
                .with_connection(metadata),
  116    118   
        );
  117    119   
    }
  118    120   
    let Some(dispatch) = ConnectionState::try_commit_dispatch(&connection) else {
  119         -
            let metadata =
  120         -
                captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  121         -
            return finish_unaccepted_request(
         121  +
        let metadata = captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
         122  +
        return resolve_unaccepted_request(
  122    123   
            request,
  123    124   
            reused,
  124    125   
            UnacceptedStage::BeforeHyper,
  125    126   
            ConnectorError::other(H2ConnectionClosedBeforeDispatch.into(), None)
  126    127   
                .with_connection(metadata),
  127    128   
        );
  128    129   
    };
  129    130   
  130         -
        let body = H2RequestBodyHandle::arm(&mut request, send_endpoint);
         131  +
    let body = H2RequestBodyHandle::arm(&mut request, upload);
  131    132   
  132    133   
    let mut send = Box::pin(sender.hyper_mut().try_send_request(request));
  133    134   
    let first = poll_fn(|cx| Poll::Ready(send.as_mut().poll(cx))).await;
  134    135   
  135    136   
    match first {
  136    137   
        Poll::Ready(Err(mut error)) if error.message().is_some() => {
  137    138   
            let returned = error
  138    139   
                .take_message()
  139    140   
                .expect("checked returned request disappeared");
  140    141   
            body.clear();
  141         -
                drop(receive_endpoint);
         142  +
            drop(response);
  142    143   
            drop(dispatch);
  143    144   
            drop(activation);
  144         -
                close.close(super::super::connection::CloseReason::ProtocolClosed);
         145  +
            close.close(CloseReason::ProtocolClosed);
  145    146   
            let metadata =
  146    147   
                captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  147         -
                finish_unaccepted_request(
         148  +
            resolve_unaccepted_request(
  148    149   
                returned,
  149    150   
                reused,
  150    151   
                UnacceptedStage::ReturnedByHyper,
  151         -
                    super::super::super::downcast_error(Box::new(error.into_error()))
  152         -
                        .with_connection(metadata),
         152  +
                downcast_error(Box::new(error.into_error())).with_connection(metadata),
  153    153   
            )
  154    154   
        }
  155    155   
        Poll::Ready(result) => {
  156    156   
            activation.accept(dispatch);
  157    157   
            let sender_closed = sender.is_closed();
  158         -
                self.finish_h2_send(
         158  +
            resolve_h2_send(
  159    159   
                sender_closed,
  160    160   
                result,
  161    161   
                H2AcceptedDispatch {
  162    162   
                    connection,
  163    163   
                    close,
  164    164   
                    captured_metadata,
  165         -
                        receive_endpoint,
         165  +
                    response_guard: response,
  166    166   
                    reused,
  167    167   
                },
  168    168   
            )
  169    169   
        }
  170    170   
        Poll::Pending => {
  171    171   
            activation.accept(dispatch);
  172    172   
            let result = send.await;
  173    173   
            let sender_closed = sender.is_closed();
  174         -
                self.finish_h2_send(
         174  +
            resolve_h2_send(
  175    175   
                sender_closed,
  176    176   
                result,
  177    177   
                H2AcceptedDispatch {
  178    178   
                    connection,
  179    179   
                    close,
  180    180   
                    captured_metadata,
  181         -
                        receive_endpoint,
         181  +
                    response_guard: response,
  182    182   
                    reused,
  183    183   
                },
  184    184   
            )
  185    185   
        }
  186    186   
    }
  187         -
    }
         187  +
}
  188    188   
  189         -
    #[allow(
         189  +
#[allow(
  190    190   
    clippy::result_large_err,
  191    191   
    reason = "ConnectorError preserves SDK classification and connection metadata"
  192         -
    )]
  193         -
    fn finish_h2_send(
  194         -
        &self,
         192  +
)]
         193  +
fn resolve_h2_send(
  195    194   
    sender_closed: bool,
  196    195   
    result: Result<
  197    196   
        Response<hyper::body::Incoming>,
  198    197   
        hyper::client::conn::TrySendError<Request<SdkBody>>,
  199    198   
    >,
  200    199   
    accepted: H2AcceptedDispatch,
  201         -
    ) -> Result<H2DispatchResult, ConnectorError> {
         200  +
) -> Result<H2DispatchOutcome, ConnectorError> {
  202    201   
    let H2AcceptedDispatch {
  203    202   
        connection,
  204    203   
        close,
  205    204   
        captured_metadata,
  206         -
            receive_endpoint,
         205  +
        response_guard,
  207    206   
        reused,
  208    207   
    } = accepted;
  209    208   
    match result {
  210    209   
        Ok(mut response) => {
  211    210   
            connection
  212    211   
                .info()
  213    212   
                .apply_connector_extras(response.extensions_mut());
  214    213   
            let (parts, body) = response.into_parts();
  215         -
                let body = H2ResponseBody::new(body, receive_endpoint);
  216         -
                Ok(H2DispatchResult::Response(Response::from_parts(
         214  +
            let body = H2ResponseBody::new(body, response_guard);
         215  +
            Ok(H2DispatchOutcome::Response(Response::from_parts(
  217    216   
                parts,
  218    217   
                SdkBody::from_body_1_x(body),
  219    218   
            )))
  220    219   
        }
  221    220   
        Err(mut error) => {
  222    221   
            // Hyper may return a queued envelope when its connection task
  223    222   
            // drops after the first poll. The returned request is the
  224    223   
            // authority for reacquisition; an error without it is terminal.
  225    224   
            if let Some(request) = error.take_message() {
  226    225   
                if let Some(body) = request.extensions().get::<H2RequestBodyHandle>() {
  227    226   
                    body.clear();
  228    227   
                }
  229         -
                    drop(receive_endpoint);
  230         -
                    close.close(super::super::connection::CloseReason::ProtocolClosed);
         228  +
                drop(response_guard);
         229  +
                close.close(CloseReason::ProtocolClosed);
  231    230   
                let metadata =
  232    231   
                    captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  233         -
                    return finish_unaccepted_request(
         232  +
                return resolve_unaccepted_request(
  234    233   
                    request,
  235    234   
                    reused,
  236    235   
                    UnacceptedStage::ReturnedByHyper,
  237         -
                        super::super::super::downcast_error(Box::new(error.into_error()))
  238         -
                            .with_connection(metadata),
         236  +
                    downcast_error(Box::new(error.into_error())).with_connection(metadata),
  239    237   
                );
  240    238   
            }
  241    239   
            if sender_closed {
  242         -
                    close.close(super::super::connection::CloseReason::ProtocolClosed);
         240  +
                close.close(CloseReason::ProtocolClosed);
  243    241   
            }
  244         -
                drop(receive_endpoint);
         242  +
            drop(response_guard);
  245    243   
            let metadata =
  246    244   
                captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  247         -
                Err(
  248         -
                    super::super::super::downcast_error(Box::new(error.into_error()))
  249         -
                        .with_connection(metadata),
  250         -
                )
  251         -
            }
         245  +
            Err(downcast_error(Box::new(error.into_error())).with_connection(metadata))
  252    246   
        }
  253    247   
    }
  254    248   
}
  255    249   
  256    250   
/// Restores the absolute URI and removes only a pool-synthesized H1 `Host`.
  257    251   
fn prepare_h2_request(request: &mut Request<SdkBody>, absolute_uri: &http_1x::Uri) {
  258    252   
    *request.uri_mut() = absolute_uri.clone();
  259    253   
    if request
  260    254   
        .extensions_mut()
  261    255   
        .remove::<H1HostHeaderInserted>()
  262    256   
        .is_some()
  263    257   
    {
  264    258   
        request.headers_mut().remove(http_1x::header::HOST);
  265    259   
    }
  266    260   
}
  267    261   
  268    262   
/// Point at which the pool learned that Hyper had not accepted the request.
  269    263   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
  270    264   
enum UnacceptedStage {
  271    265   
    /// The pool rejected a stale generation before calling Hyper.
  272    266   
    BeforeHyper,
  273    267   
    /// Hyper returned the original request envelope.
  274    268   
    ReturnedByHyper,
  275    269   
}
  276    270   
  277    271   
/// Applies retry authority to a request that Hyper did not accept.
  278    272   
///
  279    273   
/// Pool-side checks always reacquire because no protocol code observed the
  280    274   
/// request. A returned Hyper envelope reacquires only after prior successful
  281    275   
/// use proves that replacing a stale pooled generation is appropriate.
  282    276   
#[allow(
  283    277   
    clippy::result_large_err,
  284    278   
    reason = "ConnectorError preserves SDK classification and connection metadata"
  285    279   
)]
  286         -
fn finish_unaccepted_request(
         280  +
fn resolve_unaccepted_request(
  287    281   
    request: Request<SdkBody>,
  288    282   
    reused: bool,
  289    283   
    stage: UnacceptedStage,
  290    284   
    error: ConnectorError,
  291         -
) -> Result<H2DispatchResult, ConnectorError> {
         285  +
) -> Result<H2DispatchOutcome, ConnectorError> {
  292    286   
    if stage == UnacceptedStage::BeforeHyper || reused {
  293         -
        Ok(H2DispatchResult::Reacquire(Box::new(H2Reacquisition {
         287  +
        Ok(H2DispatchOutcome::NotAccepted(Box::new(
         288  +
            H2UnacceptedRequest {
  294    289   
                request,
  295         -
            error,
  296         -
        })))
         290  +
                terminal_error: error,
         291  +
            },
         292  +
        )))
  297    293   
    } else {
  298    294   
        Err(error)
  299    295   
    }
  300    296   
}
  301    297   
  302    298   
/// A fresh HTTP/2 generation closed before accepting its first request.
  303    299   
#[derive(Debug)]
  304    300   
struct H2ConnectionClosedBeforeDispatch;
  305    301   
  306    302   
impl std::fmt::Display for H2ConnectionClosedBeforeDispatch {
  307    303   
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  308    304   
        f.write_str("the fresh HTTP/2 connection closed before request dispatch")
  309    305   
    }
  310    306   
}
  311    307   
  312    308   
impl std::error::Error for H2ConnectionClosedBeforeDispatch {}
  313    309   
  314    310   
/// Handle retained in a request extension after its body is wrapped once.
  315    311   
#[derive(Clone)]
  316    312   
struct H2RequestBodyHandle {
  317         -
    /// Re-armable send endpoint shared with the wrapped request body.
  318         -
    slot: Arc<Mutex<Option<H2LeaseEndpoint>>>,
         313  +
    /// Re-armable upload guard shared with the wrapped request body.
         314  +
    slot: Arc<Mutex<Option<H2UploadGuard>>>,
  319    315   
}
  320    316   
  321    317   
impl H2RequestBodyHandle {
  322         -
    /// Wraps an unwrapped body and arms the current send endpoint.
  323         -
    fn arm(request: &mut Request<SdkBody>, endpoint: H2LeaseEndpoint) -> Self {
         318  +
    /// Wraps an unwrapped body and arms the current upload guard.
         319  +
    fn arm(request: &mut Request<SdkBody>, upload: H2UploadGuard) -> Self {
  324    320   
        let is_end_stream = request.body().is_end_stream();
  325    321   
        let handle = request
  326    322   
            .extensions()
  327    323   
            .get::<Self>()
  328    324   
            .cloned()
  329    325   
            .unwrap_or_else(|| {
  330    326   
                let handle = Self {
  331    327   
                    slot: Arc::new(Mutex::new(None)),
  332    328   
                };
  333    329   
                let body = std::mem::replace(request.body_mut(), SdkBody::taken());
  334    330   
                *request.body_mut() = SdkBody::from_body_1_x(H2RequestBody {
  335    331   
                    inner: body,
  336    332   
                    slot: handle.slot.clone(),
  337    333   
                });
  338    334   
                request.extensions_mut().insert(handle.clone());
  339    335   
                handle
  340    336   
            });
  341         -
        arm_send_endpoint(&handle.slot, endpoint);
         337  +
        arm_upload_guard(&handle.slot, upload);
  342    338   
        if is_end_stream {
  343         -
            finish_send_endpoint(&handle.slot);
         339  +
            finish_upload(&handle.slot);
  344    340   
        }
  345    341   
        handle
  346    342   
    }
  347    343   
  348         -
    /// Disarms an endpoint after Hyper returns the request unaccepted.
         344  +
    /// Disarms the upload guard after Hyper returns the request unaccepted.
  349    345   
    fn clear(&self) {
  350         -
        finish_send_endpoint(&self.slot);
         346  +
        finish_upload(&self.slot);
  351    347   
    }
  352    348   
}
  353    349   
  354         -
/// Request body whose endpoint can be re-armed after certified non-acceptance.
         350  +
/// Request body whose upload guard can be re-armed after non-acceptance.
  355    351   
struct H2RequestBody {
  356    352   
    /// Original SDK body wrapped exactly once.
  357    353   
    inner: SdkBody,
  358         -
    /// Endpoint replaced when Hyper returns an unaccepted request.
  359         -
    slot: Arc<Mutex<Option<H2LeaseEndpoint>>>,
         354  +
    /// Guard replaced when Hyper returns an unaccepted request.
         355  +
    slot: Arc<Mutex<Option<H2UploadGuard>>>,
  360    356   
}
  361    357   
  362    358   
impl Body for H2RequestBody {
  363    359   
    type Data = <SdkBody as Body>::Data;
  364    360   
    type Error = <SdkBody as Body>::Error;
  365    361   
  366    362   
    fn poll_frame(
  367    363   
        mut self: Pin<&mut Self>,
  368    364   
        cx: &mut Context<'_>,
  369    365   
    ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
  370    366   
        let result = Pin::new(&mut self.inner).poll_frame(cx);
  371    367   
        if matches!(result, Poll::Ready(None) | Poll::Ready(Some(Err(_)))) {
  372         -
            finish_send_endpoint(&self.slot);
         368  +
            finish_upload(&self.slot);
  373    369   
        }
  374    370   
        result
  375    371   
    }
  376    372   
  377    373   
    fn is_end_stream(&self) -> bool {
  378    374   
        self.inner.is_end_stream()
  379    375   
    }
  380    376   
  381    377   
    fn size_hint(&self) -> hyper::body::SizeHint {
  382    378   
        self.inner.size_hint()
  383    379   
    }
  384    380   
}
  385    381   
  386    382   
impl Drop for H2RequestBody {
  387    383   
    fn drop(&mut self) {
  388         -
        finish_send_endpoint(&self.slot);
         384  +
        finish_upload(&self.slot);
  389    385   
    }
  390    386   
}
  391    387   
  392         -
/// Response body that owns the receive endpoint through stream completion.
         388  +
/// Response body that owns the response guard through stream completion.
  393    389   
struct H2ResponseBody {
  394    390   
    /// Hyper response stream.
  395    391   
    inner: hyper::body::Incoming,
  396         -
    /// Receive endpoint completed on terminal frame, error, or drop.
  397         -
    receive: Option<H2LeaseEndpoint>,
         392  +
    /// Response guard finished on terminal frame, error, or drop.
         393  +
    response: Option<H2ResponseGuard>,
  398    394   
}
  399    395   
  400    396   
impl H2ResponseBody {
  401         -
    /// Wraps a response and completes an endpoint already at end stream.
  402         -
    fn new(inner: hyper::body::Incoming, receive: H2LeaseEndpoint) -> Self {
         397  +
    /// Wraps a response and finishes a guard already at end stream.
         398  +
    fn new(inner: hyper::body::Incoming, response: H2ResponseGuard) -> Self {
  403    399   
        let is_end_stream = inner.is_end_stream();
  404    400   
        Self {
  405    401   
            inner,
  406         -
            receive: retain_receive_endpoint(is_end_stream, receive),
         402  +
            response: retain_response_guard(is_end_stream, response),
  407    403   
        }
  408    404   
    }
  409    405   
}
  410    406   
  411         -
/// Retains a receive endpoint only while response frames may remain.
  412         -
fn retain_receive_endpoint(
         407  +
/// Retains a response guard only while response frames may remain.
         408  +
fn retain_response_guard(
  413    409   
    is_end_stream: bool,
  414         -
    receive: H2LeaseEndpoint,
  415         -
) -> Option<H2LeaseEndpoint> {
         410  +
    response: H2ResponseGuard,
         411  +
) -> Option<H2ResponseGuard> {
  416    412   
    if is_end_stream {
  417         -
        receive.complete();
         413  +
        response.finish();
  418    414   
        None
  419    415   
    } else {
  420         -
        Some(receive)
         416  +
        Some(response)
  421    417   
    }
  422    418   
}
  423    419   
  424    420   
impl Body for H2ResponseBody {
  425    421   
    type Data = <hyper::body::Incoming as Body>::Data;
  426    422   
    type Error = <hyper::body::Incoming as Body>::Error;
  427    423   
  428    424   
    fn poll_frame(
  429    425   
        mut self: Pin<&mut Self>,
  430    426   
        cx: &mut Context<'_>,
  431    427   
    ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
  432    428   
        let result = Pin::new(&mut self.inner).poll_frame(cx);
  433    429   
        if matches!(result, Poll::Ready(None) | Poll::Ready(Some(Err(_)))) {
  434         -
            if let Some(endpoint) = self.receive.take() {
  435         -
                endpoint.complete();
         430  +
            if let Some(response) = self.response.take() {
         431  +
                response.finish();
  436    432   
            }
  437    433   
        }
  438    434   
        result
  439    435   
    }
  440    436   
  441    437   
    fn is_end_stream(&self) -> bool {
  442    438   
        self.inner.is_end_stream()
  443    439   
    }
  444    440   
  445    441   
    fn size_hint(&self) -> hyper::body::SizeHint {
  446    442   
        self.inner.size_hint()
  447    443   
    }
  448    444   
}
  449    445   
  450         -
fn arm_send_endpoint(slot: &Arc<Mutex<Option<H2LeaseEndpoint>>>, endpoint: H2LeaseEndpoint) {
  451         -
    let previous = slot.lock().replace(endpoint);
         446  +
fn arm_upload_guard(slot: &Arc<Mutex<Option<H2UploadGuard>>>, upload: H2UploadGuard) {
         447  +
    let previous = slot.lock().replace(upload);
  452    448   
    drop(previous);
  453    449   
}
  454    450   
  455         -
/// Terminates the send endpoint after upload completion, error, or rejection.
  456         -
fn finish_send_endpoint(slot: &Arc<Mutex<Option<H2LeaseEndpoint>>>) {
  457         -
    let endpoint = slot.lock().take();
  458         -
    if let Some(endpoint) = endpoint {
  459         -
        endpoint.complete();
         451  +
/// Finishes the upload guard after upload completion, error, or rejection.
         452  +
fn finish_upload(slot: &Arc<Mutex<Option<H2UploadGuard>>>) {
         453  +
    let upload = slot.lock().take();
         454  +
    if let Some(upload) = upload {
         455  +
        upload.finish();
  460    456   
    }
  461    457   
}
  462    458   
  463    459   
#[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
  464    460   
mod tests {
  465    461   
    use super::*;
  466    462   
    use crate::client::pool::cell::OriginCell;
  467    463   
    use crate::client::pool::origin::OriginKey;
  468    464   
    use crate::client::pool::partition::{EligibilityGroup, PartitionId};
  469    465   
    use crate::sync::Arc;
  470    466   
    use http_1x::uri::Scheme;
  471    467   
    use http_body_util::BodyExt;
  472    468   
  473    469   
    /// Request body that fails on its first frame.
  474    470   
    struct FailingBody;
  475    471   
  476    472   
    impl Body for FailingBody {
  477    473   
        type Data = hyper::body::Bytes;
  478    474   
        type Error = std::io::Error;
  479    475   
  480    476   
        fn poll_frame(
  481    477   
            self: Pin<&mut Self>,
  482    478   
            _cx: &mut Context<'_>,
  483    479   
        ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
  484    480   
            Poll::Ready(Some(Err(std::io::Error::other("request body failed"))))
  485    481   
        }
  486    482   
    }
  487    483   
  488    484   
    fn cell() -> Arc<OriginCell> {
  489    485   
        Arc::new(OriginCell::new(
  490    486   
            PartitionId::from_index(1),
  491    487   
            OriginKey::from_parts(Scheme::HTTPS, "example.com", None).unwrap(),
  492    488   
            EligibilityGroup::Pool,
  493    489   
            None,
  494    490   
            None,
  495    491   
        ))
  496    492   
    }
  497    493   
  498    494   
    #[tokio::test]
  499    495   
    async fn returned_request_rearms_one_body_wrapper() {
  500    496   
        let cell = cell();
  501    497   
        let mut request = Request::post("https://example.com/upload")
  502    498   
            .body(SdkBody::from("payload"))
  503    499   
            .unwrap();
  504    500   
  505         -
        let (first_endpoint, first_probe) = H2LeaseEndpoint::send_for_test(&cell);
  506         -
        let first = H2RequestBodyHandle::arm(&mut request, first_endpoint);
         501  +
        let (first_upload, first_probe) = H2UploadGuard::for_test(&cell);
         502  +
        let first = H2RequestBodyHandle::arm(&mut request, first_upload);
  507    503   
        let first_slot = first.slot.clone();
  508         -
        assert!(!first_probe.send_complete());
         504  +
        assert!(!first_probe.upload_finished());
  509    505   
  510         -
        let (second_endpoint, second_probe) = H2LeaseEndpoint::send_for_test(&cell);
  511         -
        let second = H2RequestBodyHandle::arm(&mut request, second_endpoint);
         506  +
        let (second_upload, second_probe) = H2UploadGuard::for_test(&cell);
         507  +
        let second = H2RequestBodyHandle::arm(&mut request, second_upload);
  512    508   
        assert!(
  513    509   
            Arc::ptr_eq(&first_slot, &second.slot),
  514    510   
            "rearming replaced the request-body wrapper"
  515    511   
        );
  516    512   
        assert!(
  517         -
            first_probe.send_complete(),
  518         -
            "rearming did not cancel the prior prospective endpoint"
         513  +
            first_probe.upload_finished(),
         514  +
            "rearming did not cancel the prior prospective upload guard"
  519    515   
        );
  520         -
        assert!(!second_probe.send_complete());
         516  +
        assert!(!second_probe.upload_finished());
  521    517   
  522    518   
        second.clear();
  523    519   
        assert!(
  524         -
            second_probe.send_complete(),
  525         -
            "returned request retained its rejected send endpoint"
         520  +
            second_probe.upload_finished(),
         521  +
            "returned request retained its rejected upload guard"
  526    522   
        );
  527    523   
        let body = request
  528    524   
            .into_body()
  529    525   
            .collect()
  530    526   
            .await
  531    527   
            .expect("wrapped request body failed")
  532    528   
            .to_bytes();
  533    529   
        assert_eq!(hyper::body::Bytes::from_static(b"payload"), body);
  534    530   
    }
  535    531   
  536    532   
    #[test]
  537         -
    fn empty_request_body_completes_its_send_endpoint_when_armed() {
         533  +
    fn empty_request_body_finishes_its_upload_guard_when_armed() {
  538    534   
        let cell = cell();
  539    535   
        let mut request = Request::new(SdkBody::empty());
  540         -
        let (endpoint, probe) = H2LeaseEndpoint::send_for_test(&cell);
         536  +
        let (upload, probe) = H2UploadGuard::for_test(&cell);
  541    537   
  542         -
        H2RequestBodyHandle::arm(&mut request, endpoint);
         538  +
        H2RequestBodyHandle::arm(&mut request, upload);
  543    539   
  544         -
        assert!(probe.send_complete());
         540  +
        assert!(probe.upload_finished());
  545    541   
    }
         542  +
  546    543   
    #[test]
  547         -
    fn dropping_request_body_completes_its_send_endpoint() {
         544  +
    fn dropping_request_body_finishes_its_upload_guard() {
  548    545   
        let cell = cell();
  549    546   
        let mut request = Request::new(SdkBody::from("payload"));
  550         -
        let (endpoint, probe) = H2LeaseEndpoint::send_for_test(&cell);
         547  +
        let (upload, probe) = H2UploadGuard::for_test(&cell);
  551    548   
  552         -
        H2RequestBodyHandle::arm(&mut request, endpoint);
         549  +
        H2RequestBodyHandle::arm(&mut request, upload);
  553    550   
        let body = std::mem::replace(request.body_mut(), SdkBody::empty());
  554    551   
        drop(body);
  555    552   
  556         -
        assert!(probe.send_complete());
         553  +
        assert!(probe.upload_finished());
  557    554   
    }
  558    555   
  559    556   
    #[tokio::test]
  560         -
    async fn request_body_error_completes_its_send_endpoint() {
         557  +
    async fn request_body_error_finishes_its_upload_guard() {
  561    558   
        let cell = cell();
  562    559   
        let mut request = Request::new(SdkBody::from_body_1_x(FailingBody));
  563         -
        let (endpoint, probe) = H2LeaseEndpoint::send_for_test(&cell);
         560  +
        let (upload, probe) = H2UploadGuard::for_test(&cell);
  564    561   
  565         -
        H2RequestBodyHandle::arm(&mut request, endpoint);
         562  +
        H2RequestBodyHandle::arm(&mut request, upload);
  566    563   
        let frame = request
  567    564   
            .body_mut()
  568    565   
            .frame()
  569    566   
            .await
  570    567   
            .expect("failing body omitted its error frame");
  571    568   
  572    569   
        assert!(frame.is_err());
  573         -
        assert!(probe.send_complete());
         570  +
        assert!(probe.upload_finished());
  574    571   
    }
  575    572   
  576    573   
    #[test]
  577         -
    fn bodyless_response_completes_its_receive_endpoint_without_drop() {
         574  +
    fn bodyless_response_finishes_its_response_guard_without_drop() {
  578    575   
        let cell = cell();
  579         -
        let (endpoint, probe) = H2LeaseEndpoint::receive_for_test(&cell);
         576  +
        let (response, probe) = H2ResponseGuard::for_test(&cell);
  580    577   
  581         -
        let retained = retain_receive_endpoint(true, endpoint);
         578  +
        let retained = retain_response_guard(true, response);
  582    579   
  583    580   
        assert!(retained.is_none());
  584         -
        assert!(probe.receive_complete());
         581  +
        assert!(probe.response_finished());
  585    582   
    }
  586    583   
  587    584   
    #[test]
  588    585   
    fn h2_preparation_restores_uri_and_removes_only_synthesized_host() {
  589    586   
        let absolute: http_1x::Uri = "https://example.com/resource".parse().unwrap();
  590    587   
        let mut request = Request::get("/resource").body(SdkBody::empty()).unwrap();
  591    588   
        request
  592    589   
            .headers_mut()
  593    590   
            .insert(http_1x::header::HOST, "example.com".parse().unwrap());
  594    591   
        request.extensions_mut().insert(H1HostHeaderInserted);
  595    592   
  596    593   
        prepare_h2_request(&mut request, &absolute);
  597    594   
  598    595   
        assert_eq!(&absolute, request.uri());
  599    596   
        assert!(!request.headers().contains_key(http_1x::header::HOST));
  600    597   
        assert!(request.extensions().get::<H1HostHeaderInserted>().is_none());
  601    598   
  602    599   
        let mut user_host = Request::get("/resource").body(SdkBody::empty()).unwrap();
  603    600   
        user_host
  604    601   
            .headers_mut()
  605    602   
            .insert(http_1x::header::HOST, "signed.example".parse().unwrap());
  606    603   
        prepare_h2_request(&mut user_host, &absolute);
  607    604   
        assert_eq!("signed.example", user_host.headers()[http_1x::header::HOST]);
  608    605   
    }
  609    606   
  610    607   
    #[test]
  611    608   
    fn retry_authority_distinguishes_pool_checks_from_hyper_returns() {
  612         -
        let checked_before_hyper = finish_unaccepted_request(
         609  +
        let checked_before_hyper = resolve_unaccepted_request(
  613    610   
            Request::new(SdkBody::empty()),
  614    611   
            false,
  615    612   
            UnacceptedStage::BeforeHyper,
  616    613   
            ConnectorError::user("pre-Hyper failure".into()),
  617    614   
        );
  618    615   
        assert!(matches!(
  619    616   
            checked_before_hyper,
  620         -
            Ok(H2DispatchResult::Reacquire(_))
         617  +
            Ok(H2DispatchOutcome::NotAccepted(_))
  621    618   
        ));
  622    619   
  623         -
        let fresh_hyper_return = finish_unaccepted_request(
         620  +
        let fresh_hyper_return = resolve_unaccepted_request(
  624    621   
            Request::new(SdkBody::empty()),
  625    622   
            false,
  626    623   
            UnacceptedStage::ReturnedByHyper,
  627    624   
            ConnectorError::user("fresh Hyper failure".into()),
  628    625   
        );
  629    626   
        assert!(fresh_hyper_return.is_err());
  630    627   
  631         -
        let reused_hyper_return = finish_unaccepted_request(
         628  +
        let reused_hyper_return = resolve_unaccepted_request(
  632    629   
            Request::new(SdkBody::empty()),
  633    630   
            true,
  634    631   
            UnacceptedStage::ReturnedByHyper,
  635    632   
            ConnectorError::user("reused failure".into()),
  636    633   
        );
  637    634   
        assert!(matches!(
  638    635   
            reused_hyper_return,
  639         -
            Ok(H2DispatchResult::Reacquire(_))
         636  +
            Ok(H2DispatchOutcome::NotAccepted(_))
  640    637   
        ));
  641    638   
    }
  642    639   
}

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

@@ -1,1 +380,162 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6         -
//! Transport construction below HTTP protocol establishment.
           6  +
//! Transport connection followed by HTTP protocol establishment.
           7  +
//!
           8  +
//! [`transport`] creates connected I/O and reports the negotiated protocol.
           9  +
//! This module then routes that transport through the matching Hyper handshake,
          10  +
//! creates the protocol-neutral connection identity, and returns or transfers
          11  +
//! completion ownership for the launching waiter.
    7     12   
    8     13   
mod h1;
    9     14   
mod h2;
          15  +
mod transport;
   10     16   
          17  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
          18  +
pub(super) use transport::from_cached_interface_connector;
          19  +
#[cfg(any(
          20  +
    all(feature = "test-util", aws_sdk_unstable),
          21  +
    all(test, feature = "rt-tokio")
          22  +
))]
          23  +
pub(super) use transport::from_connector;
          24  +
pub(super) use transport::{from_interface_connector, TransportFactory, TransportTimeout};
          25  +
          26  +
use self::transport::TransportConnectContext;
   11     27   
use super::admission::ProtocolRequirement;
   12         -
use super::cell::{AcquisitionResult, EstablishmentPermit, WaiterId};
          28  +
use super::cell::{AcquisitionOutcome, EstablishmentPermit, WaiterId};
   13     29   
use super::dispatch::AcquisitionContext;
   14         -
use super::registry::PartitionState;
   15     30   
use super::PoolInner;
   16         -
use crate::client::connect::{AsyncConn, BoxConn};
   17         -
use crate::client::timeout::{self, TimeoutKind};
   18         -
use aws_smithy_async::rt::sleep::SharedAsyncSleep;
   19         -
use aws_smithy_runtime_api::box_error::BoxError;
          31  +
use crate::client::downcast_error;
   20     32   
use aws_smithy_runtime_api::client::connection::ConnectionId;
   21     33   
use aws_smithy_runtime_api::client::result::ConnectorError;
   22         -
use http_1x::Uri;
   23         -
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
   24         -
use std::collections::HashMap;
   25     34   
use std::error::Error;
   26     35   
use std::fmt;
   27         -
use std::future::{poll_fn, Future};
   28         -
use std::pin::Pin;
   29     36   
use std::sync::atomic::Ordering;
   30         -
use std::sync::Arc as StdArc;
   31         -
use std::time::Duration;
   32         -
use tower::Service;
   33         -
   34         -
/// Timeout policy for one transport connection operation.
   35         -
///
   36         -
/// Connector readiness completes before this timeout starts. Keeping the
   37         -
/// duration and timer together lets request acquisition pass one complete
   38         -
/// timeout value into transport construction.
   39         -
#[derive(Clone, Debug)]
   40         -
pub(super) struct TransportTimeout {
   41         -
    /// Maximum duration of the connector's connection future.
   42         -
    duration: Duration,
   43         -
    /// Runtime timer used to enforce the deadline.
   44         -
    sleep: SharedAsyncSleep,
   45         -
}
   46         -
   47         -
impl TransportTimeout {
   48         -
    /// Creates a timeout enforced by the supplied runtime timer.
   49         -
    pub(super) fn new(duration: Duration, sleep: SharedAsyncSleep) -> Self {
   50         -
        Self { duration, sleep }
   51         -
    }
   52         -
}
   53         -
   54         -
/// Future returned by a type-erased transport factory.
   55         -
type TransportFuture = Pin<Box<dyn Future<Output = Result<BoxConn, BoxError>> + Send + 'static>>;
   56         -
   57         -
/// Type-erased transport construction below HTTP protocol establishment.
   58         -
///
   59         -
/// A pool retains one factory that applies partition interface placement,
   60         -
/// connector readiness, and the operation's connection timeout. The concrete
   61         -
/// connector remains responsible for DNS, proxy, TLS, socket construction,
   62         -
/// and transport metadata. Protocol establishment consumes the resulting
   63         -
/// [`BoxConn`].
   64         -
pub(super) trait TransportFactory: Send + Sync + 'static {
   65         -
    /// Returns whether this factory can guarantee HTTP/1 for a required attempt.
   66         -
    fn guarantees_http1(&self) -> bool;
   67         -
   68         -
    /// Creates one partition-bound transport for `uri`.
   69         -
    ///
   70         -
    /// Connector readiness completes before `timeout` starts.
   71         -
    fn connect(
   72         -
        &self,
   73         -
        partition: &PartitionState,
   74         -
        uri: Uri,
   75         -
        timeout: Option<TransportTimeout>,
   76         -
        alpn_protocols: AlpnProtocols,
   77         -
    ) -> TransportFuture;
   78         -
}
   79         -
   80         -
/// Generic connector factory erased at pool construction.
   81         -
struct ServiceTransportFactory<F> {
   82         -
    /// Builds a connector with the selected network-interface binding.
   83         -
    connector_for_interface: F,
   84         -
    /// Whether every H1-required connection is guaranteed to negotiate H1.
   85         -
    guarantees_http1: bool,
   86         -
}
   87         -
   88         -
impl<F, C, IO> TransportFactory for ServiceTransportFactory<F>
   89         -
where
   90         -
    F: Fn(Option<&str>) -> C + Send + Sync + 'static,
   91         -
    C: Service<Uri, Response = IO> + Send + 'static,
   92         -
    C::Error: Into<BoxError>,
   93         -
    C::Future: Send + 'static,
   94         -
    IO: AsyncConn,
   95         -
{
   96         -
    fn guarantees_http1(&self) -> bool {
   97         -
        self.guarantees_http1
   98         -
    }
   99         -
  100         -
    fn connect(
  101         -
        &self,
  102         -
        partition: &PartitionState,
  103         -
        uri: Uri,
  104         -
        timeout: Option<TransportTimeout>,
  105         -
        _alpn_protocols: AlpnProtocols,
  106         -
    ) -> TransportFuture {
  107         -
        let interface = partition.interface().map(|interface| interface.as_ref());
  108         -
        let mut connector = (self.connector_for_interface)(interface);
  109         -
        Box::pin(async move {
  110         -
            // Readiness follows the existing connector contract and is not timed.
  111         -
            poll_fn(|cx| connector.poll_ready(cx))
  112         -
                .await
  113         -
                .map_err(Into::into)?;
  114         -
            let connect = connector.call(uri);
  115         -
            let io = timeout::maybe_timeout_future(
  116         -
                connect,
  117         -
                timeout.as_ref().map(|timeout| timeout.duration),
  118         -
                timeout.as_ref().map(|timeout| &timeout.sleep),
  119         -
                TimeoutKind::Connect,
  120         -
            )
  121         -
            .await?;
  122         -
            Ok(Box::new(io) as BoxConn)
  123         -
        })
  124         -
    }
  125         -
}
  126         -
  127         -
#[cfg(any(
  128         -
    all(feature = "test-util", aws_sdk_unstable),
  129         -
    all(test, feature = "rt-tokio")
  130         -
))]
  131         -
/// Erases a clonable connector for pool storage.
  132         -
pub(super) fn transport_factory<C, IO>(connector: C) -> StdArc<dyn TransportFactory>
  133         -
where
  134         -
    C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  135         -
    C::Error: Into<BoxError>,
  136         -
    C::Future: Send + 'static,
  137         -
    IO: AsyncConn,
  138         -
{
  139         -
    service_transport_factory_for_interface(move |_| connector.clone(), false)
  140         -
}
  141         -
  142         -
/// Erases a connector constructor that applies an optional interface binding.
  143         -
///
  144         -
/// The default HTTP connector consumes this value. Custom connectors use
  145         -
/// `transport_factory` and ignore pool interface placement.
  146         -
pub(super) fn transport_factory_for_interface<F, C, IO>(
  147         -
    connector_for_interface: F,
  148         -
) -> StdArc<dyn TransportFactory>
  149         -
where
  150         -
    F: Fn(Option<&str>) -> C + Send + Sync + 'static,
  151         -
    C: Service<Uri, Response = IO> + Send + 'static,
  152         -
    C::Error: Into<BoxError>,
  153         -
    C::Future: Send + 'static,
  154         -
    IO: AsyncConn,
  155         -
{
  156         -
    service_transport_factory_for_interface(connector_for_interface, true)
  157         -
}
  158         -
  159         -
/// Erases a service connector and its HTTP/1 negotiation guarantee.
  160         -
fn service_transport_factory_for_interface<F, C, IO>(
  161         -
    connector_for_interface: F,
  162         -
    guarantees_http1: bool,
  163         -
) -> StdArc<dyn TransportFactory>
  164         -
where
  165         -
    F: Fn(Option<&str>) -> C + Send + Sync + 'static,
  166         -
    C: Service<Uri, Response = IO> + Send + 'static,
  167         -
    C::Error: Into<BoxError>,
  168         -
    C::Future: Send + 'static,
  169         -
    IO: AsyncConn,
  170         -
{
  171         -
    StdArc::new(ServiceTransportFactory {
  172         -
        connector_for_interface,
  173         -
        guarantees_http1,
  174         -
    })
  175         -
}
  176         -
  177         -
/// Erases and caches connectors whose construction carries TLS or other setup.
  178         -
///
  179         -
/// Interface placement is immutable for a partition. The first request for
  180         -
/// each `(interface, ALPN offer)` constructs its connector while holding the
  181         -
/// cache lock; later requests clone that retained connector. Provider
  182         -
/// configuration and certificate loading therefore occur at most once for
  183         -
/// each placement and offer rather than once per connection.
  184         -
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
  185         -
pub(super) fn cached_transport_factory_for_interface<F, C, IO>(
  186         -
    connector_for_interface: F,
  187         -
    guarantees_http1: bool,
  188         -
) -> StdArc<dyn TransportFactory>
  189         -
where
  190         -
    F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
  191         -
    C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  192         -
    C::Error: Into<BoxError>,
  193         -
    C::Future: Send + 'static,
  194         -
    IO: AsyncConn,
  195         -
{
  196         -
    struct Cached<F, C> {
  197         -
        factory: F,
  198         -
        guarantees_http1: bool,
  199         -
        connectors: crate::sync::Mutex<HashMap<(Option<String>, AlpnProtocols), C>>,
  200         -
    }
  201         -
  202         -
    impl<F, C, IO> TransportFactory for Cached<F, C>
  203         -
    where
  204         -
        F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
  205         -
        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  206         -
        C::Error: Into<BoxError>,
  207         -
        C::Future: Send + 'static,
  208         -
        IO: AsyncConn,
  209         -
    {
  210         -
        fn guarantees_http1(&self) -> bool {
  211         -
            self.guarantees_http1
  212         -
        }
  213         -
  214         -
        fn connect(
  215         -
            &self,
  216         -
            partition: &PartitionState,
  217         -
            uri: Uri,
  218         -
            timeout: Option<TransportTimeout>,
  219         -
            alpn_protocols: AlpnProtocols,
  220         -
        ) -> TransportFuture {
  221         -
            let interface = partition.interface().map(|value| value.to_string());
  222         -
            let mut connector = self
  223         -
                .connectors
  224         -
                .lock()
  225         -
                .entry((interface.clone(), alpn_protocols))
  226         -
                .or_insert_with(|| (self.factory)(interface.as_deref(), alpn_protocols))
  227         -
                .clone();
  228         -
            Box::pin(async move {
  229         -
                poll_fn(|cx| connector.poll_ready(cx))
  230         -
                    .await
  231         -
                    .map_err(Into::into)?;
  232         -
                let connect = connector.call(uri);
  233         -
                let io = timeout::maybe_timeout_future(
  234         -
                    connect,
  235         -
                    timeout.as_ref().map(|timeout| timeout.duration),
  236         -
                    timeout.as_ref().map(|timeout| &timeout.sleep),
  237         -
                    TimeoutKind::Connect,
  238         -
                )
  239         -
                .await?;
  240         -
                Ok(Box::new(io) as BoxConn)
  241         -
            })
  242         -
        }
  243         -
    }
  244         -
  245         -
    StdArc::new(Cached {
  246         -
        factory: connector_for_interface,
  247         -
        guarantees_http1,
  248         -
        connectors: crate::sync::Mutex::new(HashMap::new()),
  249         -
    })
  250         -
}
  251     37   
  252     38   
/// Result of one owner-runtime establishment task.
  253     39   
pub(super) enum EstablishmentOutcome {
  254     40   
    /// The launching waiter receives this terminal result.
  255         -
    Complete(AcquisitionResult),
  256         -
    /// Waiter completion transferred to an H2 flight or generation.
  257         -
    Transferred,
          41  +
    Complete(AcquisitionOutcome),
          42  +
    /// An H2 flight or generation now owns the launching waiter's completion.
          43  +
    WaiterCompletionTransferred,
  258     44   
}
  259     45   
  260     46   
/// Connects one transport and dispatches protocol establishment after ALPN.
  261     47   
pub(super) async fn establish(
  262     48   
    context: AcquisitionContext,
  263     49   
    waiter: WaiterId,
  264     50   
    permit: EstablishmentPermit,
  265     51   
    requirement: ProtocolRequirement,
  266     52   
) -> EstablishmentOutcome {
  267         -
    let io = match context
  268         -
        .pool
  269         -
        .transport
  270         -
        .connect(
          53  +
    let connect = TransportConnectContext::new(
  271     54   
        &context.partition,
  272     55   
        context.absolute_uri.clone(),
  273     56   
        context.connect_timeout.clone(),
  274         -
            alpn_protocols(requirement),
  275         -
        )
  276         -
        .await
  277         -
    {
          57  +
        requirement,
          58  +
    );
          59  +
    let io = match context.pool.transport.connect(connect).await {
  278     60   
        Ok(io) => io,
  279     61   
        Err(error) => {
  280     62   
            tracing::debug!(
  281     63   
                request_partition = ?context.partition.id(),
  282     64   
                connection_partition = ?context.cell.id().partition(),
  283     65   
                origin_scheme = %context.cell.id().origin().scheme(),
  284     66   
                origin_host = context.cell.id().origin().host(),
  285     67   
                origin_port = ?context.cell.id().origin().port(),
  286     68   
                error = ?error,
  287     69   
                "transport establishment failed"
  288     70   
            );
  289         -
            return EstablishmentOutcome::Complete(AcquisitionResult::Failed(
  290         -
                super::super::downcast_error(error),
  291         -
            ));
          71  +
            return EstablishmentOutcome::Complete(AcquisitionOutcome::Failed(downcast_error(
          72  +
                error,
          73  +
            )));
  292     74   
        }
  293     75   
    };
  294     76   
    let connected = io.connected();
  295     77   
    let negotiated_h2 = connected.is_negotiated_h2();
  296     78   
    tracing::debug!(
  297     79   
        request_partition = ?context.partition.id(),
  298     80   
        connection_partition = ?context.cell.id().partition(),
  299     81   
        origin_scheme = %context.cell.id().origin().scheme(),
  300     82   
        origin_host = context.cell.id().origin().host(),
  301     83   
        origin_port = ?context.cell.id().origin().port(),
  302     84   
        negotiated_protocol = if negotiated_h2 { "HTTP/2" } else { "HTTP/1.1" },
  303     85   
        "transport protocol negotiated"
  304     86   
    );
  305     87   
    if negotiated_h2 && !requirement.accepts_h2() {
  306     88   
        drop(io);
  307     89   
        drop(permit);
  308         -
        return EstablishmentOutcome::Complete(AcquisitionResult::Failed(ConnectorError::user(
  309         -
            NegotiatedProtocolMismatch { requirement }.into(),
  310         -
        )));
          90  +
        return EstablishmentOutcome::Complete(AcquisitionOutcome::Failed(
          91  +
            negotiated_protocol_mismatch(requirement),
          92  +
        ));
  311     93   
    }
  312     94   
  313     95   
    if negotiated_h2 {
  314     96   
        h2::establish_h2(context, waiter, permit, io, connected).await
  315     97   
    } else {
  316     98   
        EstablishmentOutcome::Complete(
  317     99   
            h1::establish_h1(context, permit, io, connected)
  318    100   
                .await
  319         -
                .map(AcquisitionResult::H1)
  320         -
                .unwrap_or_else(AcquisitionResult::Failed),
         101  +
                .map(AcquisitionOutcome::H1)
         102  +
                .unwrap_or_else(AcquisitionOutcome::Failed),
  321    103   
        )
  322    104   
    }
  323    105   
}
  324         -
  325    106   
/// An established transport selected a protocol incompatible with the request.
  326    107   
#[derive(Debug)]
  327    108   
struct NegotiatedProtocolMismatch {
  328    109   
    requirement: ProtocolRequirement,
  329    110   
}
  330    111   
  331    112   
impl fmt::Display for NegotiatedProtocolMismatch {
  332    113   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  333    114   
        write!(
  334    115   
            f,
  335    116   
            "transport negotiated HTTP/2, which does not satisfy {:?} request semantics",
  336    117   
            self.requirement
  337    118   
        )
  338    119   
    }
  339    120   
}
  340    121   
  341    122   
impl Error for NegotiatedProtocolMismatch {}
  342    123   
         124  +
fn negotiated_protocol_mismatch(requirement: ProtocolRequirement) -> ConnectorError {
         125  +
    ConnectorError::other(NegotiatedProtocolMismatch { requirement }.into(), None)
         126  +
}
         127  +
  343    128   
/// Mints one non-wrapping physical-connection identity.
  344    129   
fn next_connection_id(pool: &PoolInner) -> Result<ConnectionId, ConnectionIdExhausted> {
  345    130   
    let value = pool
  346    131   
        .next_connection_id
  347    132   
        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
  348    133   
            current.checked_add(1)
  349    134   
        })
  350    135   
        .map_err(|_| ConnectionIdExhausted)?;
  351    136   
    Ok(ConnectionId::new(value))
  352    137   
}
  353    138   
  354    139   
/// The pool exhausted its monotonic physical-connection identity space.
  355    140   
#[derive(Debug)]
  356    141   
struct ConnectionIdExhausted;
  357    142   
  358    143   
impl fmt::Display for ConnectionIdExhausted {
  359    144   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  360    145   
        f.write_str("connection identifier space exhausted")
  361    146   
    }
  362    147   
}
  363    148   
  364    149   
impl Error for ConnectionIdExhausted {}
  365    150   
  366         -
/// Static ALPN protocol offer used for one transport connection.
  367         -
pub(super) type AlpnProtocols = &'static [&'static [u8]];
  368         -
  369         -
/// Default offer for requests that may use HTTP/2.
  370         -
pub(super) const HTTP_ALPN_PROTOCOLS: AlpnProtocols = &[b"h2", b"http/1.1"];
         151  +
#[cfg(test)]
         152  +
mod tests {
         153  +
    use super::*;
  371    154   
  372         -
/// Narrowed offer for requests that require HTTP/1 wire semantics.
  373         -
pub(super) const HTTP1_ALPN_PROTOCOLS: AlpnProtocols = &[b"http/1.1"];
         155  +
    #[test]
         156  +
    fn negotiated_protocol_mismatch_is_not_a_user_error() {
         157  +
        let error = negotiated_protocol_mismatch(ProtocolRequirement::H1Required);
  374    158   
  375         -
fn alpn_protocols(requirement: ProtocolRequirement) -> AlpnProtocols {
  376         -
    match requirement {
  377         -
        ProtocolRequirement::H1Required => HTTP1_ALPN_PROTOCOLS,
  378         -
        ProtocolRequirement::H1Compatible | ProtocolRequirement::H2Required => HTTP_ALPN_PROTOCOLS,
         159  +
        assert!(error.is_other());
         160  +
        assert!(!error.is_user());
  379    161   
    }
  380    162   
}