AWS SDK

AWS SDK

rev. 39e8f0a4a4cd7fc94cae1e2f796e9c21897d9cb9

Files changed:

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

@@ -25,25 +167,188 @@
   45     45   
//! ```
   46     46   
//!
   47     47   
//! [`H1Selection`], [`H1Exchange`], and [`ProvisionalH1`] are the
   48     48   
//! sender-owning values outside the cell lock. Their drop behavior returns or
   49     49   
//! retires the sender so a cancellation cannot leave a record with no terminal
   50     50   
//! owner. They retain the owning cell weakly because a selected sender may
   51     51   
//! temporarily be the ready result in that same cell; cell teardown makes the
   52     52   
//! fallback close the connection directly.
   53     53   
   54     54   
use super::super::admission::{
   55         -
    AdmissionAction, H1Candidate, H1MatchId, H1SupplyStatus, OriginAdmission,
          55  +
    AdmissionAction, H1Candidate, H1MatchId, H1SupplyStatus, OriginAdmission, PreparedH1IdleProbe,
   56     56   
    PreparedH1Reservation, SupplyRevision,
   57     57   
};
   58     58   
use super::super::connection::{CloseReason, ConnectionState};
   59     59   
use super::{AcquisitionOutcome, OriginCell};
   60     60   
use crate::sync::{Arc, Weak};
   61     61   
use aws_smithy_runtime_api::client::connection::ConnectionId;
   62     62   
use std::collections::{HashMap, VecDeque};
   63     63   
use std::fmt;
   64     64   
use std::task::{Context, Poll};
   65     65   
use std::time::SystemTime;
   66     66   
   67     67   
use aws_smithy_types::body::SdkBody;
   68     68   
   69     69   
/// Exclusive request handle for one Hyper HTTP/1 client connection.
   70     70   
///
   71     71   
/// Hyper creates one `SendRequest<SdkBody>` handle when the client handshake
   72     72   
/// completes. Pool code calls this handle the HTTP/1 sender. Moving it between
   73     73   
/// residence guards transfers authority to send the next request; it does not
   74     74   
/// move the socket or protocol driver. The test-only variant exercises those
   75     75   
/// ownership transitions without running Hyper.
   76     76   
pub(in crate::client::pool) enum H1Sender {
   77     77   
    /// Hyper's exclusive HTTP/1 request sender.
   78     78   
    Hyper(hyper::client::conn::http1::SendRequest<SdkBody>),
   79     79   
    /// Synthetic sender identity used only by ownership tests.
   80     80   
    #[cfg(test)]
   81         -
    Test(u64),
          81  +
    Test { id: u64, ready: bool },
   82     82   
}
   83     83   
   84     84   
impl H1Sender {
   85     85   
    /// Wraps a sender returned by a successful Hyper HTTP/1 handshake.
   86     86   
    pub(in crate::client::pool) fn from_hyper(
   87     87   
        sender: hyper::client::conn::http1::SendRequest<SdkBody>,
   88     88   
    ) -> Self {
   89     89   
        Self::Hyper(sender)
   90     90   
    }
   91     91   
   92     92   
    /// Returns the Hyper sender for readiness and dispatch.
   93     93   
    ///
   94     94   
    /// # Panics
   95     95   
    ///
   96     96   
    /// Panics when a test-only sender reaches the real dispatch path.
   97     97   
    pub(in crate::client::pool) fn hyper_mut(
   98     98   
        &mut self,
   99     99   
    ) -> &mut hyper::client::conn::http1::SendRequest<SdkBody> {
  100    100   
        match self {
  101    101   
            Self::Hyper(sender) => sender,
  102    102   
            #[cfg(test)]
  103         -
            Self::Test(_) => panic!("test HTTP/1 sender reached Hyper dispatch"),
         103  +
            Self::Test { .. } => panic!("test HTTP/1 sender reached Hyper dispatch"),
  104    104   
        }
  105    105   
    }
  106    106   
  107    107   
    /// Returns whether Hyper already permits another request.
  108    108   
    fn is_ready(&self) -> bool {
  109    109   
        match self {
  110    110   
            Self::Hyper(sender) => sender.is_ready(),
  111    111   
            #[cfg(test)]
  112         -
            Self::Test(_) => panic!("test HTTP/1 sender reached Hyper readiness"),
         112  +
            Self::Test { ready, .. } => *ready,
         113  +
        }
         114  +
    }
         115  +
         116  +
    /// Polls Hyper for proof that another request may be sent.
         117  +
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), hyper::Error>> {
         118  +
        match self {
         119  +
            Self::Hyper(sender) => sender.poll_ready(cx),
         120  +
            #[cfg(test)]
         121  +
            Self::Test { ready: true, .. } => Poll::Ready(Ok(())),
         122  +
            #[cfg(test)]
         123  +
            Self::Test { ready: false, .. } => Poll::Pending,
  113    124   
        }
  114    125   
    }
  115    126   
  116    127   
    /// Creates a synthetic sender for state-machine tests.
  117    128   
    #[cfg(test)]
  118    129   
    pub(in crate::client::pool) fn test(id: u64) -> Self {
  119         -
        Self::Test(id)
         130  +
        Self::Test { id, ready: true }
         131  +
    }
         132  +
         133  +
    /// Creates a synthetic sender whose readiness remains pending.
         134  +
    #[cfg(test)]
         135  +
    pub(in crate::client::pool) fn pending_test(id: u64) -> Self {
         136  +
        Self::Test { id, ready: false }
  120    137   
    }
  121    138   
  122    139   
    /// Returns the synthetic sender identity.
  123    140   
    #[cfg(test)]
  124    141   
    pub(super) fn test_id(&self) -> u64 {
  125    142   
        match self {
  126         -
            Self::Test(id) => *id,
         143  +
            Self::Test { id, .. } => *id,
  127    144   
            Self::Hyper(_) => panic!("Hyper sender used in a synthetic ownership test"),
  128    145   
        }
  129    146   
    }
  130    147   
}
  131    148   
  132    149   
impl fmt::Debug for H1Sender {
  133    150   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  134    151   
        match self {
  135    152   
            Self::Hyper(_) => f.write_str("H1Sender::Hyper"),
  136    153   
            #[cfg(test)]
  137         -
            Self::Test(id) => f.debug_tuple("H1Sender::Test").field(id).finish(),
         154  +
            Self::Test { id, ready } => f
         155  +
                .debug_struct("H1Sender::Test")
         156  +
                .field("id", id)
         157  +
                .field("ready", ready)
         158  +
                .finish(),
  138    159   
        }
  139    160   
    }
  140    161   
}
  141    162   
  142    163   
/// Local reservation for one peer match and its fairness debt.
  143    164   
///
  144    165   
/// Reservation residence and fairness debt are separate because a completed
  145    166   
/// transfer releases the reservation immediately, while the debt remains until
  146    167   
/// later local service or the disappearance of compatible local demand.
  147    168   
#[derive(Debug, Default)]
@@ -610,631 +669,704 @@
  630    651   
                H1SenderResidence::Closing => (idle, active),
  631    652   
            })
  632    653   
    }
  633    654   
  634    655   
    /// Returns the installed record and idle counts.
  635    656   
    #[cfg(test)]
  636    657   
    pub(super) fn counts(&self) -> (usize, usize) {
  637    658   
        (self.records.len(), self.idle_order.len())
  638    659   
    }
  639    660   
         661  +
    /// Returns the synthetic identities of every idle sender.
         662  +
    #[cfg(all(test, smithy_http_client_loom))]
         663  +
    pub(super) fn idle_sender_ids(&self) -> Vec<u64> {
         664  +
        self.records
         665  +
            .values()
         666  +
            .filter_map(|record| match &record.sender_state {
         667  +
                H1SenderResidence::Idle { sender, .. } => Some(sender.test_id()),
         668  +
                H1SenderResidence::Selected
         669  +
                | H1SenderResidence::ReservedForPeer
         670  +
                | H1SenderResidence::Closing => None,
         671  +
            })
         672  +
            .collect()
         673  +
    }
         674  +
  640    675   
    /// Returns the sole installed connection for focused dispatch tests.
  641    676   
    #[cfg(all(test, feature = "rt-tokio"))]
  642    677   
    pub(super) fn only_connection_for_test(&self) -> Arc<ConnectionState> {
  643    678   
        assert_eq!(
  644    679   
            1,
  645    680   
            self.records.len(),
  646    681   
            "expected exactly one installed HTTP/1 record"
  647    682   
        );
  648    683   
        self.records
  649    684   
            .values()
@@ -927,962 +1005,1047 @@
  947    982   
/// exchange means a reusable message boundary was not proven and closes the
  948    983   
/// connection as an incomplete HTTP/1 exchange.
  949    984   
pub(in crate::client::pool) struct H1Exchange {
  950    985   
    /// Non-retaining reference to the cell that owns the selected record.
  951    986   
    connection_cell: Weak<OriginCell>,
  952    987   
    /// Sender held until Hyper proves it may return.
  953    988   
    owner: Option<OwnedH1Sender>,
  954    989   
}
  955    990   
  956    991   
impl H1Exchange {
         992  +
    /// Returns the installed connection that owns this exchange.
         993  +
    pub(in crate::client::pool) fn connection(&self) -> &Arc<ConnectionState> {
         994  +
        self.owner
         995  +
            .as_ref()
         996  +
            .expect("HTTP/1 exchange consumed more than once")
         997  +
            .connection()
         998  +
    }
         999  +
  957   1000   
    /// Returns whether Hyper already permits another request.
  958   1001   
    pub(in crate::client::pool) fn is_ready(&self) -> bool {
  959   1002   
        self.owner
  960   1003   
            .as_ref()
  961   1004   
            .expect("HTTP/1 exchange consumed more than once")
  962   1005   
            .sender
  963   1006   
            .is_ready()
  964   1007   
    }
  965   1008   
  966   1009   
    /// Polls Hyper for proof that the sender can accept another request.
  967   1010   
    pub(in crate::client::pool) fn poll_ready(
  968   1011   
        &mut self,
  969   1012   
        cx: &mut Context<'_>,
  970   1013   
    ) -> Poll<Result<(), hyper::Error>> {
  971   1014   
        self.owner
  972   1015   
            .as_mut()
  973   1016   
            .expect("HTTP/1 exchange consumed more than once")
  974   1017   
            .sender_mut()
  975         -
            .hyper_mut()
  976   1018   
            .poll_ready(cx)
  977   1019   
    }
  978   1020   
  979   1021   
    /// Offers a proven-ready sender to owning-cell reuse policy.
  980   1022   
    pub(in crate::client::pool) fn offer_for_reuse(mut self) {
  981   1023   
        let mut owner = self
  982   1024   
            .owner
  983   1025   
            .take()
  984   1026   
            .expect("HTTP/1 exchange consumed more than once");
  985   1027   
        let connection = owner.connection().clone();
@@ -1248,1290 +1307,1383 @@
 1268   1310   
            )
 1269   1311   
        });
 1270   1312   
        OriginAdmission::settle_h1_reservation(
 1271   1313   
            &admission,
 1272   1314   
            prepared.match_id,
 1273   1315   
            cell.id.partition(),
 1274   1316   
            decision,
 1275   1317   
        )
 1276   1318   
    }
 1277   1319   
        1320  +
    /// Attempts to extract an idle sender without intercepting a future return.
        1321  +
    pub(in crate::client::pool) fn try_take_idle_h1(
        1322  +
        cell: &Arc<Self>,
        1323  +
        admission: Arc<OriginAdmission>,
        1324  +
        prepared: PreparedH1IdleProbe,
        1325  +
    ) -> Option<AdmissionAction> {
        1326  +
        let decision = {
        1327  +
            let mut state = cell.state.lock();
        1328  +
            state.try_take_idle_h1(prepared.match_id)
        1329  +
        };
        1330  +
        let decision = decision.map_candidate(|owner| {
        1331  +
            let provisional = ProvisionalH1::new(cell, owner);
        1332  +
            H1Candidate::new(
        1333  +
                admission.clone(),
        1334  +
                prepared.match_id,
        1335  +
                cell.id.partition(),
        1336  +
                provisional,
        1337  +
            )
        1338  +
        });
        1339  +
        OriginAdmission::settle_h1_idle_probe(
        1340  +
            &admission,
        1341  +
            prepared.match_id,
        1342  +
            cell.id.partition(),
        1343  +
            decision,
        1344  +
        )
        1345  +
    }
        1346  +
        1347  +
    /// Returns the current admission-facing HTTP/1 supply revision.
        1348  +
    pub(in crate::client::pool) fn current_h1_supply_revision(
        1349  +
        &self,
        1350  +
    ) -> SupplyRevision<H1SupplyStatus> {
        1351  +
        self.state.lock().current_h1_supply_revision()
        1352  +
    }
        1353  +
 1278   1354   
    /// Clears an installed or resolving reservation after request cancellation.
 1279   1355   
    pub(in crate::client::pool) fn cancel_h1_reservation(
 1280   1356   
        &self,
 1281   1357   
        match_id: H1MatchId,
 1282   1358   
    ) -> SupplyRevision<H1SupplyStatus> {
 1283   1359   
        self.state.lock().cancel_h1_reservation(match_id)
 1284   1360   
    }
 1285   1361   
 1286   1362   
    /// Returns a rejected provisional sender through ordinary connection-owning cell handling.
 1287   1363   
    pub(in crate::client::pool) fn reject_h1_match(
@@ -1540,1616 +1599,1681 @@
 1560   1636   
        }
 1561   1637   
        won
 1562   1638   
    }
 1563   1639   
 1564   1640   
    /// Returns H1 record counts for focused ownership tests.
 1565   1641   
    #[cfg(test)]
 1566   1642   
    pub(super) fn h1_counts(&self) -> (usize, usize) {
 1567   1643   
        self.state.lock().h1.counts()
 1568   1644   
    }
 1569   1645   
        1646  +
    /// Returns the synthetic identities of every idle HTTP/1 sender.
        1647  +
    #[cfg(all(test, smithy_http_client_loom))]
        1648  +
    pub(super) fn h1_idle_sender_ids(&self) -> Vec<u64> {
        1649  +
        self.state.lock().h1.idle_sender_ids()
        1650  +
    }
        1651  +
 1570   1652   
    /// Returns the sole installed HTTP/1 connection for focused dispatch tests.
 1571   1653   
    #[cfg(all(test, feature = "rt-tokio"))]
 1572   1654   
    pub(in crate::client::pool) fn only_h1_connection_for_test(&self) -> Arc<ConnectionState> {
 1573   1655   
        self.state.lock().h1.only_connection_for_test()
 1574   1656   
    }
 1575   1657   
}
 1576   1658   
 1577   1659   
/// Terminal outcome for one reusable HTTP/1 sender return.
 1578   1660   
enum H1ReturnTrace {
 1579   1661   
    /// The owning cell no longer accepts the sender.

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

@@ -2895,2895 +3050,3103 @@
 2915   2915   
            Some(CloseReason::PoolDropped),
 2916   2916   
            connection.probe().close_reason
 2917   2917   
        );
 2918   2918   
        assert_eq!(None, cell.accepting_h2_generation());
 2919   2919   
    }
 2920   2920   
}
 2921   2921   
 2922   2922   
#[cfg(all(test, smithy_http_client_loom))]
 2923   2923   
mod loom_tests {
 2924   2924   
    use super::*;
        2925  +
    use crate::client::pool::admission::ProtocolRequirement;
 2925   2926   
    use crate::client::pool::connection::{CloseReason, ConnectionInfo};
 2926   2927   
    use crate::client::pool::origin::OriginKey;
 2927   2928   
    use crate::client::pool::partition::EligibilityGroup;
 2928   2929   
    use aws_smithy_runtime_api::client::connection::ConnectionId;
 2929   2930   
    use http_1x::uri::Scheme;
 2930   2931   
 2931   2932   
    fn cell() -> Arc<OriginCell> {
 2932   2933   
        Arc::new(OriginCell::new(
 2933   2934   
            PartitionId::from_index(1),
 2934   2935   
            OriginKey::from_parts(Scheme::HTTPS, "example.com", None).unwrap(),
 2935   2936   
            EligibilityGroup::Pool,
 2936   2937   
            None,
 2937   2938   
            None,
 2938   2939   
        ))
 2939   2940   
    }
 2940   2941   
 2941   2942   
    fn open_test_generation(cell: &Arc<OriginCell>) -> (H2GenerationId, Arc<ConnectionState>) {
 2942   2943   
        let (connection, _physical) = ConnectionState::unbounded(ConnectionInfo::for_test(
 2943   2944   
            ConnectionId::new(1),
 2944   2945   
            PartitionId::from_index(1),
 2945   2946   
        ));
 2946   2947   
        let generation = OriginCell::install_h2_for_test(cell, connection.clone(), 1, None);
 2947   2948   
        (generation, connection)
 2948   2949   
    }
 2949   2950   
        2951  +
    fn launching_waiter(cell: &Arc<OriginCell>) -> WaiterId {
        2952  +
        let waiter = OriginCell::register_waiter(cell, ProtocolRequirement::H2Required);
        2953  +
        let AcquisitionStep::StartEstablishment(permit) = cell
        2954  +
            .take_ready_event(waiter)
        2955  +
            .expect("new HTTP/2 waiter did not receive establishment authority")
        2956  +
        else {
        2957  +
            panic!("new HTTP/2 waiter completed before establishment");
        2958  +
        };
        2959  +
        assert!(cell.start_establishment(waiter));
        2960  +
        drop(permit);
        2961  +
        waiter
        2962  +
    }
        2963  +
        2964  +
    /// Races activation of one exact generation with close of that generation.
        2965  +
    ///
        2966  +
    /// Close either rejects activation or retains the draining generation until
        2967  +
    /// the prospective request releases its claim.
 2950   2968   
    #[test]
 2951   2969   
    fn activation_linearizes_against_generation_close() {
 2952   2970   
        loom::model(|| {
 2953   2971   
            let cell = cell();
 2954   2972   
            let (generation, connection) = open_test_generation(&cell);
 2955   2973   
            let activating_cell = cell.clone();
 2956   2974   
            let activation = loom::thread::spawn(move || {
 2957   2975   
                OriginCell::activate_h2(
 2958   2976   
                    &activating_cell,
 2959   2977   
                    generation,
 2960   2978   
                    activating_cell.id().partition(),
 2961   2979   
                )
 2962   2980   
            });
 2963   2981   
            let closing_cell = cell.clone();
 2964   2982   
            let close = loom::thread::spawn(move || {
 2965   2983   
                OriginCell::close_h2(&closing_cell, generation, CloseReason::Poisoned)
 2966   2984   
            });
 2967   2985   
 2968         -
            drop(activation.join().unwrap());
        2986  +
            let activation = activation.join().unwrap();
 2969   2987   
            assert!(close.join().unwrap());
 2970   2988   
            assert_eq!(Some(CloseReason::Poisoned), connection.probe().close_reason);
        2989  +
            if let Some(activation) = activation {
        2990  +
                assert_eq!(
        2991  +
                    Some((1, 0)),
        2992  +
                    cell.h2_request_counts(generation),
        2993  +
                    "close did not retain the prospective activation"
        2994  +
                );
        2995  +
                drop(activation);
        2996  +
            }
 2971   2997   
            assert!(!cell.state.lock().h2.generations.contains_key(&generation));
 2972   2998   
        });
 2973   2999   
    }
 2974   3000   
        3001  +
    /// Completes the upload and response sides of one accepted request concurrently.
        3002  +
    ///
        3003  +
    /// The two guards share one request claim and must release its dispatch once.
 2975   3004   
    #[test]
 2976   3005   
    fn concurrent_request_side_completion_releases_one_dispatch() {
 2977   3006   
        loom::model(|| {
 2978   3007   
            let cell = cell();
 2979   3008   
            let (generation, connection) = open_test_generation(&cell);
 2980   3009   
            let mut activation = OriginCell::activate_h2(&cell, generation, cell.id().partition())
 2981   3010   
                .expect("generation did not activate");
 2982   3011   
            let H2DispatchParts {
 2983   3012   
                sender: _sender,
 2984   3013   
                upload,
 2985   3014   
                response,
 2986   3015   
            } = activation.take_dispatch_parts();
 2987   3016   
            let dispatch = ConnectionState::try_commit_dispatch(&connection)
 2988   3017   
                .expect("open connection rejected dispatch");
 2989   3018   
            activation.accept(dispatch);
 2990   3019   
 2991   3020   
            let upload = loom::thread::spawn(move || drop(upload));
 2992   3021   
            let response = loom::thread::spawn(move || drop(response));
 2993   3022   
            upload.join().unwrap();
 2994   3023   
            response.join().unwrap();
 2995   3024   
 2996   3025   
            assert_eq!(0, connection.probe().in_flight);
 2997   3026   
            let state = cell.state.lock();
 2998   3027   
            let record = state
 2999   3028   
                .h2
 3000   3029   
                .generations
 3001   3030   
                .get(&generation)
 3002   3031   
                .expect("accepting generation disappeared");
 3003   3032   
            assert_eq!(0, record.active_requests);
 3004   3033   
        });
 3005   3034   
    }
 3006   3035   
        3036  +
    /// Converges two post-ALPN attempts, then races flight completion with cancellation.
        3037  +
    ///
        3038  +
    /// Exactly one attempt owns the flight task. The live participant receives
        3039  +
    /// the installed generation while the cancelled participant is removed from
        3040  +
    /// either the flight or the generation gate.
 3007   3041   
    #[test]
 3008         -
    fn generation_close_waits_for_both_request_sides() {
        3042  +
    fn concurrent_h2_flight_convergence_installs_one_generation() {
 3009   3043   
        loom::model(|| {
 3010   3044   
            let cell = cell();
 3011         -
            let (connection, _physical) = ConnectionState::unbounded(ConnectionInfo::for_test(
        3045  +
            let first = launching_waiter(&cell);
        3046  +
            let second = launching_waiter(&cell);
        3047  +
        3048  +
            let first_cell = cell.clone();
        3049  +
            let first_convergence =
        3050  +
                loom::thread::spawn(move || first_cell.converge_h2_flight(first));
        3051  +
            let second_cell = cell.clone();
        3052  +
            let second_convergence =
        3053  +
                loom::thread::spawn(move || second_cell.converge_h2_flight(second));
        3054  +
            let first_decision = first_convergence.join().unwrap();
        3055  +
            let second_decision = second_convergence.join().unwrap();
        3056  +
            let flight = match (first_decision, second_decision) {
        3057  +
                (H2FlightDecision::RunFlight(flight), H2FlightDecision::JoinedFlight)
        3058  +
                | (H2FlightDecision::JoinedFlight, H2FlightDecision::RunFlight(flight)) => flight,
        3059  +
                decisions => panic!("convergence did not select one flight owner: {decisions:?}"),
        3060  +
            };
        3061  +
        3062  +
            let (connection, physical) = ConnectionState::unbounded(ConnectionInfo::for_test(
 3012   3063   
                ConnectionId::new(1),
 3013   3064   
                PartitionId::from_index(1),
 3014   3065   
            ));
 3015         -
            let generation = OriginCell::install_h2_for_test(&cell, connection.clone(), 1, None);
 3016         -
            let mut activation = OriginCell::activate_h2(&cell, generation, cell.id().partition())
 3017         -
                .expect("generation did not activate");
 3018         -
            let H2DispatchParts {
 3019         -
                sender: _sender,
 3020         -
                upload,
 3021         -
                response,
 3022         -
            } = activation.take_dispatch_parts();
 3023         -
            let dispatch = ConnectionState::try_commit_dispatch(&connection)
 3024         -
                .expect("open connection rejected dispatch");
 3025         -
            activation.accept(dispatch);
 3026         -
 3027         -
            let first_side = loom::thread::spawn(move || drop(upload));
 3028         -
            let closing_cell = cell.clone();
 3029         -
            let close = loom::thread::spawn(move || {
 3030         -
                OriginCell::close_h2(&closing_cell, generation, CloseReason::ProtocolClosed)
        3066  +
            let completing_cell = cell.clone();
        3067  +
            let completing_connection = connection.clone();
        3068  +
            let completing = loom::thread::spawn(move || {
        3069  +
                OriginCell::complete_h2_flight(
        3070  +
                    &completing_cell,
        3071  +
                    flight,
        3072  +
                    completing_connection,
        3073  +
                    H2Sender::test(1),
        3074  +
                    None,
        3075  +
                )
 3031   3076   
            });
 3032         -
            first_side.join().unwrap();
 3033         -
            assert!(close.join().unwrap());
        3077  +
            let cancelling_cell = cell.clone();
        3078  +
            let cancelling =
        3079  +
                loom::thread::spawn(move || OriginCell::cancel_waiter(&cancelling_cell, second));
        3080  +
        3081  +
            let generation = completing
        3082  +
                .join()
        3083  +
                .unwrap()
        3084  +
                .expect("the sole HTTP/2 flight did not install");
        3085  +
            assert!(cancelling.join().unwrap());
        3086  +
            let AcquisitionStep::Resolved(AcquisitionOutcome::H2(activation)) = cell
        3087  +
                .take_ready_event(first)
        3088  +
                .expect("live flight participant did not receive the generation")
        3089  +
            else {
        3090  +
                panic!("live flight participant received a non-HTTP/2 result");
        3091  +
            };
        3092  +
            drop(activation);
 3034   3093   
 3035         -
            assert_eq!(1, connection.probe().in_flight);
 3036         -
            assert_eq!(
 3037         -
                Some((0, 1)),
 3038         -
                cell.h2_request_counts(generation),
 3039         -
                "draining generation released before its second request side"
 3040         -
            );
 3041         -
            drop(response);
 3042         -
            assert_eq!(0, connection.probe().in_flight);
 3043         -
            assert_eq!(
 3044         -
                None,
 3045         -
                cell.h2_request_counts(generation),
 3046         -
                "finished draining generation remained installed"
 3047         -
            );
        3094  +
            assert_eq!(0, cell.retained_waiters_for_test());
        3095  +
            assert!(OriginCell::close_h2(
        3096  +
                &cell,
        3097  +
                generation,
        3098  +
                CloseReason::PoolDropped,
        3099  +
            ));
        3100  +
            drop(physical);
 3048   3101   
        });
 3049   3102   
    }
 3050   3103   
}

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

@@ -1,1 +46,48 @@
    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;
   12     11   
use crate::client::timeout::{self, TimeoutKind};
          12  +
use crate::client::{downcast_error, ConnectorCallTimer};
   13     13   
use crate::sync::Arc;
   14     14   
use aws_smithy_async::rt::sleep::{default_async_sleep, SharedAsyncSleep};
          15  +
use aws_smithy_async::time::SharedTimeSource;
   15     16   
use aws_smithy_runtime_api::box_error::BoxError;
   16     17   
use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
          18  +
use aws_smithy_runtime_api::client::http::telemetry::CaptureHttpAttemptTelemetry;
   17     19   
use aws_smithy_runtime_api::client::http::{
   18     20   
    HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
   19     21   
};
   20     22   
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
   21     23   
use aws_smithy_runtime_api::client::result::ConnectorError;
   22     24   
use aws_smithy_runtime_api::client::runtime_components::{
   23     25   
    RuntimeComponents, RuntimeComponentsBuilder,
   24     26   
};
   25     27   
use aws_smithy_types::config_bag::ConfigBag;
   26     28   
use std::borrow::Cow;
@@ -79,81 +145,149 @@
   99    101   
    }
  100    102   
  101    103   
    fn http_connector(
  102    104   
        &self,
  103    105   
        settings: &HttpConnectorSettings,
  104    106   
        components: &RuntimeComponents,
  105    107   
    ) -> SharedHttpConnector {
  106    108   
        let connect_timeout = settings.connect_timeout();
  107    109   
        let read_timeout = settings.read_timeout();
  108    110   
        let sleep = components.sleep_impl().or_else(default_async_sleep);
         111  +
        let time_source = components.time_source().unwrap_or_default();
  109    112   
  110    113   
        SharedHttpConnector::new(PoolConnector {
  111    114   
            pool: self.pool.clone(),
  112    115   
            partition: self.partition.clone(),
  113    116   
            connect_timeout,
  114    117   
            read_timeout,
  115    118   
            sleep,
         119  +
            time_source,
  116    120   
        })
  117    121   
    }
  118    122   
  119    123   
    fn connector_metadata(&self) -> Option<ConnectorMetadata> {
  120    124   
        Some(ConnectorMetadata::new("hyper", Some(Cow::Borrowed("1.x"))))
  121    125   
    }
  122    126   
}
  123    127   
  124    128   
/// Error returned when a [`Client`] cannot be resolved from a pool.
  125    129   
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -160,164 +284,318 @@
  180    184   
    /// Partition selected when the client was constructed.
  181    185   
    partition: Arc<PartitionState>,
  182    186   
    /// Maximum duration of the transport connection operation.
  183    187   
    ///
  184    188   
    /// Connector readiness completes before this timeout starts.
  185    189   
    connect_timeout: Option<Duration>,
  186    190   
    /// Maximum duration through response headers.
  187    191   
    read_timeout: Option<Duration>,
  188    192   
    /// Runtime timer used by operation timeouts.
  189    193   
    sleep: Option<SharedAsyncSleep>,
         194  +
    /// Operation runtime clock used for request-attempt telemetry.
         195  +
    time_source: SharedTimeSource,
  190    196   
}
  191    197   
  192    198   
impl fmt::Debug for PoolConnector {
  193    199   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  194    200   
        f.debug_struct("PoolConnector")
  195    201   
            .field("partition", &self.partition.id())
  196    202   
            .field("connect_timeout", &self.connect_timeout)
  197    203   
            .field("read_timeout", &self.read_timeout)
  198    204   
            .finish_non_exhaustive()
  199    205   
    }
  200    206   
}
  201    207   
         208  +
/// Converts and dispatches one request through the partitioned pool.
         209  +
async fn send_pool_request(
         210  +
    pool: &ConnectionPool,
         211  +
    partition: Arc<PartitionState>,
         212  +
    request: HttpRequest,
         213  +
    connect_timeout: Option<Duration>,
         214  +
    read_timeout: Option<Duration>,
         215  +
    sleep: Option<SharedAsyncSleep>,
         216  +
    attempt_telemetry: Option<super::dispatch::AttemptTelemetryInput>,
         217  +
) -> Result<HttpResponse, ConnectorError> {
         218  +
    if (connect_timeout.is_some() || read_timeout.is_some()) && sleep.is_none() {
         219  +
        return Err(ConnectorError::user(MissingAsyncSleep.into()));
         220  +
    }
         221  +
         222  +
    let request = request
         223  +
        .try_into_http1x()
         224  +
        .map_err(|error| ConnectorError::user(error.into()))?;
         225  +
    let options = super::dispatch::RequestOptions::new(
         226  +
        connect_timeout
         227  +
            .zip(sleep.clone())
         228  +
            .map(|(duration, sleep)| super::establish::TransportTimeout::new(duration, sleep)),
         229  +
        attempt_telemetry,
         230  +
    );
         231  +
    let send = pool.send_request(partition, request, options);
         232  +
    let response =
         233  +
        timeout::maybe_timeout_future(send, read_timeout, sleep.as_ref(), TimeoutKind::Read)
         234  +
            .await
         235  +
            .map_err(downcast_error)?;
         236  +
    HttpResponse::try_from(response).map_err(|error| ConnectorError::other(error.into(), None))
         237  +
}
         238  +
  202    239   
impl HttpConnector for PoolConnector {
  203    240   
    fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
         241  +
        let attempt_capture = request.extension::<CaptureHttpAttemptTelemetry>().cloned();
         242  +
        let connector_call_timer = ConnectorCallTimer::start(&request, &self.time_source);
         243  +
        let attempt_telemetry = attempt_capture.as_ref().map(|capture| {
         244  +
            super::dispatch::AttemptTelemetryInput::new(capture.clone(), self.time_source.clone())
         245  +
        });
  204    246   
        let pool = self.pool.clone();
  205    247   
        let partition = self.partition.clone();
  206    248   
        let connect_timeout = self.connect_timeout;
  207    249   
        let read_timeout = self.read_timeout;
  208    250   
        let sleep = self.sleep.clone();
  209    251   
        HttpConnectorFuture::new(async move {
  210         -
            if (connect_timeout.is_some() || read_timeout.is_some()) && sleep.is_none() {
  211         -
                return Err(ConnectorError::user(MissingAsyncSleep.into()));
  212         -
            }
  213         -
  214         -
            let request = request.try_into_http1x().map_err(|error| {
  215         -
                aws_smithy_runtime_api::client::result::ConnectorError::user(error.into())
  216         -
            })?;
  217         -
            let options =
  218         -
                super::dispatch::RequestOptions::new(connect_timeout.zip(sleep.clone()).map(
  219         -
                    |(duration, sleep)| super::establish::TransportTimeout::new(duration, sleep),
  220         -
                ));
  221         -
            let send = pool.send_request(partition, request, options);
  222         -
            let response = timeout::maybe_timeout_future(
  223         -
                send,
         252  +
            let result = send_pool_request(
         253  +
                &pool,
         254  +
                partition,
         255  +
                request,
         256  +
                connect_timeout,
  224    257   
                read_timeout,
  225         -
                sleep.as_ref(),
  226         -
                TimeoutKind::Read,
         258  +
                sleep,
         259  +
                attempt_telemetry,
  227    260   
            )
  228         -
            .await
  229         -
            .map_err(downcast_error)?;
  230         -
            HttpResponse::try_from(response).map_err(|error| {
  231         -
                aws_smithy_runtime_api::client::result::ConnectorError::other(error.into(), None)
  232         -
            })
         261  +
            .await;
         262  +
            if let Some(timer) = connector_call_timer {
         263  +
                timer.finish();
         264  +
            }
         265  +
            result
  233    266   
        })
  234    267   
    }
  235    268   
}
  236    269   
  237    270   
/// Operation timeouts were configured without a runtime timer.
  238    271   
#[derive(Debug)]
  239    272   
struct MissingAsyncSleep;
  240    273   
  241    274   
impl fmt::Display for MissingAsyncSleep {
  242    275   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  243    276   
        f.write_str("an async sleep implementation is required for HTTP connect or read timeouts")
  244    277   
    }
  245    278   
}
  246    279   
  247    280   
impl Error for MissingAsyncSleep {}
  248    281   
  249    282   
#[cfg(all(test, feature = "rt-tokio", not(smithy_http_client_loom)))]
  250    283   
mod tests {
  251    284   
    use super::*;
  252    285   
  253    286   
    #[tokio::test]
  254    287   
    async fn configured_timeout_without_sleep_returns_a_user_error() {
  255    288   
        let pool = ConnectionPool::builder()
  256    289   
            .idle_timeout(None)
  257    290   
            .build_http()
  258    291   
            .unwrap();
  259    292   
        let client = Client::new(&pool).unwrap();
  260    293   
  261    294   
        for (connect_timeout, read_timeout) in [
  262    295   
            (Some(Duration::from_secs(1)), None),
  263    296   
            (None, Some(Duration::from_secs(1))),
  264    297   
        ] {
  265    298   
            let connector = PoolConnector {
  266    299   
                pool: client.pool.clone(),
  267    300   
                partition: client.partition.clone(),
  268    301   
                connect_timeout,
  269    302   
                read_timeout,
  270    303   
                sleep: None,
         304  +
                time_source: SharedTimeSource::default(),
  271    305   
            };
  272    306   
  273    307   
            let error = connector
  274    308   
                .call(HttpRequest::get("http://example.com/").unwrap())
  275    309   
                .await
  276    310   
                .expect_err("a timeout without sleep unexpectedly started a request");
  277    311   
            assert!(error.is_user(), "unexpected connector error: {error:?}");
  278    312   
            assert_eq!(
  279    313   
                "an async sleep implementation is required for HTTP connect or read timeouts",
  280    314   
                std::error::Error::source(&error).unwrap().to_string()

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

@@ -1,1 +62,65 @@
    9      9   
//! [`DispatchGuard`] accounts for accepted requests.
   10     10   
//! [`PhysicalConnectionGuard`] follows root I/O until the client releases its
   11     11   
//! physical connection handle, including transfer through a protocol upgrade.
   12     12   
//! The operating system may continue TCP teardown afterward. Logical close
   13     13   
//! returns bounded capacity before physical completion except while an HTTP/1
   14     14   
//! exchange may still transfer upgraded I/O.
   15     15   
   16     16   
use super::admission::CapacityLease;
   17     17   
use super::events::{LogicalCloseCause, SharedConnectionEventListener};
   18     18   
use super::origin::OriginKey;
   19         -
use super::partition::PartitionId;
          19  +
use super::partition::{DriverSpawner, PartitionId};
   20     20   
use super::stats::CellConnectionStats;
   21     21   
use crate::client::connect::{ConnectPath, ConnectPathInner};
   22     22   
use crate::sync::{Arc, Mutex};
   23     23   
pub use aws_smithy_runtime_api::client::connection::ConnectionId;
   24         -
use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
          24  +
use aws_smithy_runtime_api::client::connection::{
          25  +
    ConnectionEstablishmentMetadata, ConnectionMetadata,
          26  +
};
   25     27   
use http_1x::Extensions;
   26     28   
use hyper::rt::{Read, ReadBufCursor, Write};
   27     29   
use hyper_util::client::legacy::connect::{Connected, Connection, HttpInfo};
   28     30   
use pin_project_lite::pin_project;
   29     31   
use std::fmt;
   30     32   
use std::io::{self, IoSlice};
   31     33   
use std::net::SocketAddr;
   32     34   
use std::pin::Pin;
          35  +
use std::sync::{Arc as StdArc, OnceLock};
   33     36   
use std::task::{Context, Poll};
   34     37   
   35     38   
/// Final protocol and ownership classification for a closed connection.
   36     39   
///
   37     40   
/// [`LogicalCloseCause`] reports the first stable reason that pool dispatch
   38     41   
/// ended. This more specific classification is reported at physical close
   39     42   
/// because HTTP/1 upgrade ownership may be resolved after logical close.
   40     43   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
   41     44   
#[non_exhaustive]
   42     45   
pub enum CloseReason {
@@ -64,67 +252,286 @@
   84     87   
    /// Protocol selected by configuration or ALPN.
   85     88   
    protocol: ConnectionProtocol,
   86     89   
    /// Local socket address reported by the connector, when available.
   87     90   
    local_addr: Option<SocketAddr>,
   88     91   
    /// Remote socket address reported by the connector, when available.
   89     92   
    remote_addr: Option<SocketAddr>,
   90     93   
    /// Connector-owned path state, including request-time proxy authorization.
   91     94   
    connect_path: ConnectPathInner,
   92     95   
    /// Connector metadata copied into every response on this connection.
   93     96   
    connected: Connected,
          97  +
    /// Measurements frozen before this connection becomes visible to dispatch.
          98  +
    establishment: OnceLock<ConnectionEstablishmentMetadata>,
   94     99   
}
   95    100   
   96    101   
impl ConnectionInfo {
   97    102   
    /// Captures immutable facts after transport negotiation selects a protocol.
   98    103   
    pub(super) fn new(
   99    104   
        id: ConnectionId,
  100    105   
        origin: OriginKey,
  101    106   
        owner_partition: PartitionId,
  102    107   
        protocol: ConnectionProtocol,
  103    108   
        connected: Connected,
  104    109   
    ) -> Arc<Self> {
  105    110   
        let mut extras = Extensions::new();
  106    111   
        connected.get_extras(&mut extras);
  107    112   
        let http_info = extras.get::<HttpInfo>();
  108    113   
        let connect_path = ConnectPathInner::from_connected(&connected, &extras);
  109    114   
        Arc::new(Self {
  110    115   
            id,
  111    116   
            origin,
  112    117   
            owner_partition,
  113    118   
            protocol,
  114    119   
            local_addr: http_info.map(HttpInfo::local_addr),
  115    120   
            remote_addr: http_info.map(HttpInfo::remote_addr),
  116    121   
            connect_path,
  117    122   
            connected,
         123  +
            establishment: OnceLock::new(),
  118    124   
        })
  119    125   
    }
  120    126   
  121    127   
    /// Returns the pool-assigned physical connection identity.
  122    128   
    pub fn id(&self) -> ConnectionId {
  123    129   
        self.id
  124    130   
    }
  125    131   
  126    132   
    /// Returns the canonical origin this connection may serve.
  127    133   
    pub fn origin(&self) -> &OriginKey {
  128    134   
        &self.origin
  129    135   
    }
  130    136   
  131    137   
    /// Returns the partition that owns this connection's I/O and driver.
  132    138   
    pub fn owner_partition(&self) -> PartitionId {
  133    139   
        self.owner_partition
  134    140   
    }
  135    141   
  136    142   
    /// Returns the established HTTP protocol.
  137    143   
    pub fn protocol(&self) -> ConnectionProtocol {
  138    144   
        self.protocol
  139    145   
    }
  140    146   
  141    147   
    /// Returns the connector-reported local socket address, when available.
  142    148   
    pub fn local_addr(&self) -> Option<SocketAddr> {
  143    149   
        self.local_addr
  144    150   
    }
  145    151   
  146    152   
    /// Returns the connector-reported remote socket address, when available.
  147    153   
    pub fn remote_addr(&self) -> Option<SocketAddr> {
  148    154   
        self.remote_addr
  149    155   
    }
  150    156   
  151    157   
    /// Returns how this connection reaches its origin.
  152    158   
    pub fn connect_path(&self) -> ConnectPath {
  153    159   
        self.connect_path.public()
  154    160   
    }
  155    161   
  156    162   
    /// Returns whether an HTTP proxy participates in this connection path.
  157    163   
    pub fn is_proxied(&self) -> bool {
  158    164   
        self.connect_path().is_proxied()
  159    165   
    }
  160    166   
         167  +
    /// Returns successful establishment measurements, when installation completed.
         168  +
    pub fn establishment(&self) -> Option<&ConnectionEstablishmentMetadata> {
         169  +
        self.establishment.get()
         170  +
    }
         171  +
         172  +
    /// Freezes successful establishment measurements before pool publication.
         173  +
    pub(super) fn set_establishment(&self, metadata: ConnectionEstablishmentMetadata) {
         174  +
        assert!(
         175  +
            self.establishment.set(metadata).is_ok(),
         176  +
            "connection establishment metadata was set more than once"
         177  +
        );
         178  +
    }
         179  +
  161    180   
    /// Returns connector-owned request-path state.
  162    181   
    pub(super) fn connect_path_inner(&self) -> &ConnectPathInner {
  163    182   
        &self.connect_path
  164    183   
    }
  165    184   
  166    185   
    /// Copies connector-provided values into a response extension map.
  167    186   
    pub(super) fn apply_connector_extras(&self, extensions: &mut Extensions) {
  168    187   
        self.connected.get_extras(extensions);
  169    188   
    }
  170    189   
  171    190   
    /// Builds Smithy metadata with close authority for this H1 record.
  172    191   
    pub(super) fn metadata(&self, close: super::cell::h1::H1CloseHandle) -> ConnectionMetadata {
  173    192   
        let mut builder = ConnectionMetadata::builder()
  174    193   
            .proxied(self.is_proxied())
  175    194   
            .connection_id(self.id)
  176    195   
            .poison_fn(move || {
  177    196   
                close.close(CloseReason::Poisoned);
  178    197   
            });
  179    198   
        builder
  180    199   
            .set_local_addr(self.local_addr)
  181         -
            .set_remote_addr(self.remote_addr);
         200  +
            .set_remote_addr(self.remote_addr)
         201  +
            .set_establishment(self.establishment().cloned());
  182    202   
        builder.build()
  183    203   
    }
  184    204   
  185    205   
    /// Builds Smithy metadata with close authority for this H2 generation.
  186    206   
    pub(super) fn h2_metadata(&self, close: super::cell::h2::H2CloseHandle) -> ConnectionMetadata {
  187    207   
        let mut builder = ConnectionMetadata::builder()
  188    208   
            .proxied(self.is_proxied())
  189    209   
            .connection_id(self.id)
  190    210   
            .poison_fn(move || {
  191    211   
                close.close(CloseReason::Poisoned);
  192    212   
            });
  193    213   
        builder
  194    214   
            .set_local_addr(self.local_addr)
  195         -
            .set_remote_addr(self.remote_addr);
         215  +
            .set_remote_addr(self.remote_addr)
         216  +
            .set_establishment(self.establishment().cloned());
  196    217   
        builder.build()
  197    218   
    }
  198    219   
  199    220   
    /// Creates synthetic HTTP/1 information for state-machine tests.
  200    221   
    #[cfg(test)]
  201    222   
    pub(super) fn for_test(id: ConnectionId, owner_partition: PartitionId) -> Arc<Self> {
  202    223   
        Self::new(
  203    224   
            id,
  204    225   
            OriginKey::from_parts(http_1x::uri::Scheme::HTTPS, "example.com", None)
  205    226   
                .expect("synthetic test origin is valid"),
  206    227   
            owner_partition,
  207    228   
            ConnectionProtocol::Http1,
  208    229   
            Connected::new(),
  209    230   
        )
  210    231   
    }
  211    232   
}
  212    233   
  213    234   
/// Shared connection ownership from negotiated transport through close.
  214    235   
pub(super) struct ConnectionState {
  215    236   
    /// Identity and transport facts shared with metadata and lifecycle events.
  216    237   
    info: Arc<ConnectionInfo>,
         238  +
    /// Runtime that owns protocol and follow-up work for this connection.
         239  +
    owner_spawner: StdArc<dyn DriverSpawner>,
  217    240   
    /// Cell-owned counts for connection lifetimes that outlive protocol records.
  218    241   
    stats: Arc<CellConnectionStats>,
  219    242   
    /// Dispatch, logical-close, and physical-connection completion state.
  220    243   
    lifecycle: Mutex<ConnectionLifecycle>,
  221    244   
}
  222    245   
         246  +
#[cfg(test)]
         247  +
#[derive(Debug)]
         248  +
struct TestDriverSpawner;
         249  +
         250  +
#[cfg(test)]
         251  +
impl DriverSpawner for TestDriverSpawner {
         252  +
    fn spawn(&self, driver: Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>) {
         253  +
        drop(driver);
         254  +
    }
         255  +
}
         256  +
  223    257   
/// Connection lifetime state serialized with dispatch commitment and close.
  224    258   
#[derive(Debug)]
  225    259   
struct ConnectionLifecycle {
  226    260   
    /// Dispatch eligibility and ownership of bounded capacity.
  227    261   
    logical: LogicalState,
  228    262   
    /// Requests that committed before logical close.
  229    263   
    in_flight: usize,
  230    264   
    /// Whether the client has released its physical connection handle.
  231    265   
    physical_connection_complete: bool,
  232    266   
    /// Protocol drain or upgrade count held until physical completion.
@@ -305,339 +493,544 @@
  325    359   
/// Connection state may close while the `Opened` callback is running. Tracking
  326    360   
/// callback progress under the lifecycle lock delays close callbacks until
  327    361   
/// `Opened` returns and preserves `Opened -> LogicalClose -> PhysicalClose`
  328    362   
/// without invoking a listener while the lock is held.
  329    363   
#[derive(Debug)]
  330    364   
enum LifecycleEventProgress {
  331    365   
    /// Pool installation completed, but the `Opened` callback has not returned.
  332    366   
    OpenedPending,
  333    367   
    /// `Opened` returned and logical close is the next event.
  334    368   
    WaitingForLogicalClose(SharedConnectionEventListener),
  335         -
    /// Logical close was reported and physical close is the next event.
         369  +
    /// The logical-close callback is running outside the lifecycle lock.
         370  +
    LogicalClosePending(SharedConnectionEventListener),
         371  +
    /// Logical close returned and physical close is the next event.
  336    372   
    WaitingForPhysicalClose(SharedConnectionEventListener),
  337    373   
    /// No further installed-connection event remains.
  338    374   
    Complete,
  339    375   
}
  340    376   
  341    377   
impl LifecycleEventProgress {
  342    378   
    /// Advances callback progress after the `Opened` callback returns.
  343    379   
    fn opened_completed(
  344    380   
        &mut self,
  345    381   
        listener: Option<&SharedConnectionEventListener>,
  346         -
        close: Option<(LogicalCloseCause, Option<CloseReason>)>,
  347         -
        physically_closed: bool,
  348         -
    ) -> Option<(LogicalCloseCause, Option<CloseReason>)> {
         382  +
        close_cause: Option<LogicalCloseCause>,
         383  +
    ) -> Option<(SharedConnectionEventListener, LogicalCloseCause)> {
  349    384   
        assert!(
  350    385   
            matches!(self, Self::OpenedPending),
  351    386   
            "connection opened event completed more than once"
  352    387   
        );
  353    388   
        let Some(listener) = listener else {
  354    389   
            *self = Self::Complete;
  355    390   
            return None;
  356    391   
        };
  357         -
        match (close, physically_closed) {
  358         -
            (None, _) => {
         392  +
        match close_cause {
         393  +
            None => {
  359    394   
                *self = Self::WaitingForLogicalClose(listener.clone());
  360    395   
                None
  361    396   
            }
  362         -
            (Some((cause, _)), false) => {
  363         -
                *self = Self::WaitingForPhysicalClose(listener.clone());
  364         -
                Some((cause, None))
  365         -
            }
  366         -
            (Some((cause, final_reason)), true) => {
  367         -
                *self = Self::Complete;
  368         -
                Some((
  369         -
                    cause,
  370         -
                    Some(final_reason.expect("physical close completed before H1 classification")),
  371         -
                ))
         397  +
            Some(cause) => {
         398  +
                *self = Self::LogicalClosePending(listener.clone());
         399  +
                Some((listener.clone(), cause))
  372    400   
            }
  373    401   
        }
  374    402   
    }
  375    403   
  376    404   
    /// Returns the listener when logical close may now be reported.
  377         -
    fn logical_closed(
  378         -
        &mut self,
  379         -
        final_reason: Option<CloseReason>,
  380         -
        physically_closed: bool,
  381         -
    ) -> Option<(SharedConnectionEventListener, Option<CloseReason>)> {
         405  +
    fn logical_closed(&mut self) -> Option<SharedConnectionEventListener> {
  382    406   
        match std::mem::replace(self, Self::Complete) {
  383    407   
            Self::OpenedPending => {
  384    408   
                *self = Self::OpenedPending;
  385    409   
                None
  386    410   
            }
  387    411   
            Self::WaitingForLogicalClose(listener) => {
  388         -
                if physically_closed {
  389         -
                    Some((
  390         -
                        listener,
  391         -
                        Some(
  392         -
                            final_reason
  393         -
                                .expect("physical close completed before H1 classification"),
  394         -
                        ),
  395         -
                    ))
  396         -
                } else {
  397         -
                    *self = Self::WaitingForPhysicalClose(listener.clone());
  398         -
                    Some((listener, None))
  399         -
                }
         412  +
                *self = Self::LogicalClosePending(listener.clone());
         413  +
                Some(listener)
         414  +
            }
         415  +
            Self::LogicalClosePending(listener) => {
         416  +
                *self = Self::LogicalClosePending(listener);
         417  +
                None
  400    418   
            }
  401    419   
            Self::WaitingForPhysicalClose(listener) => {
  402    420   
                *self = Self::WaitingForPhysicalClose(listener);
  403    421   
                None
  404    422   
            }
  405    423   
            Self::Complete => None,
  406    424   
        }
  407    425   
    }
  408    426   
         427  +
    /// Advances callback progress after the logical-close callback returns.
         428  +
    fn logical_close_completed(
         429  +
        &mut self,
         430  +
        physically_closed: bool,
         431  +
    ) -> Option<SharedConnectionEventListener> {
         432  +
        match std::mem::replace(self, Self::Complete) {
         433  +
            Self::LogicalClosePending(listener) if physically_closed => Some(listener),
         434  +
            Self::LogicalClosePending(listener) => {
         435  +
                *self = Self::WaitingForPhysicalClose(listener);
         436  +
                None
         437  +
            }
         438  +
            state => {
         439  +
                *self = state;
         440  +
                panic!("connection logical-close event completed out of order");
         441  +
            }
         442  +
        }
         443  +
    }
         444  +
  409    445   
    /// Returns the listener when physical close may now be reported.
  410    446   
    fn physical_closed(&mut self) -> Option<SharedConnectionEventListener> {
  411    447   
        match std::mem::replace(self, Self::Complete) {
  412    448   
            Self::OpenedPending => {
  413    449   
                *self = Self::OpenedPending;
  414    450   
                None
  415    451   
            }
  416    452   
            Self::WaitingForLogicalClose(listener) => {
  417    453   
                *self = Self::WaitingForLogicalClose(listener);
  418    454   
                None
  419    455   
            }
         456  +
            Self::LogicalClosePending(listener) => {
         457  +
                *self = Self::LogicalClosePending(listener);
         458  +
                None
         459  +
            }
  420    460   
            Self::WaitingForPhysicalClose(listener) => Some(listener),
  421    461   
            Self::Complete => None,
  422    462   
        }
  423    463   
    }
  424    464   
}
  425    465   
impl ConnectionState {
  426    466   
    /// Creates state after transport establishment and protocol selection.
  427    467   
    ///
  428    468   
    /// For TLS transports, the connector has completed TLS and ALPN before this
  429    469   
    /// call. Hyper protocol setup and cell installation have not occurred.
  430    470   
    ///
  431    471   
    /// The returned guard uniquely tracks the client's physical connection
  432    472   
    /// handle.
  433    473   
    /// After Hyper returns a request handle, [`Self::open`] attaches optional
  434    474   
    /// bounded capacity before cell installation makes the connection
  435    475   
    /// discoverable.
  436    476   
    pub(super) fn pending_open(
  437    477   
        info: Arc<ConnectionInfo>,
         478  +
        owner_spawner: StdArc<dyn DriverSpawner>,
  438    479   
        stats: Arc<CellConnectionStats>,
  439    480   
    ) -> (Arc<Self>, PhysicalConnectionGuard) {
  440    481   
        stats.physical_connection_started();
  441    482   
        let connection = Arc::new(Self {
  442    483   
            info,
         484  +
            owner_spawner,
  443    485   
            stats,
  444    486   
            lifecycle: Mutex::new(ConnectionLifecycle {
  445    487   
                logical: LogicalState::PendingOpen,
  446    488   
                in_flight: 0,
  447    489   
                physical_connection_complete: false,
  448    490   
                counted_close: None,
  449    491   
                events: LifecycleEventProgress::OpenedPending,
  450    492   
            }),
  451    493   
        });
  452    494   
        let physical = PhysicalConnectionGuard {
  453    495   
            connection: connection.clone(),
  454    496   
            active: true,
  455    497   
        };
  456    498   
        (connection, physical)
  457    499   
    }
  458    500   
  459    501   
    #[cfg(test)]
  460    502   
    pub(super) fn pending_open_for_test(
  461    503   
        info: Arc<ConnectionInfo>,
  462    504   
    ) -> (Arc<Self>, PhysicalConnectionGuard) {
  463         -
        Self::pending_open(info, Arc::new(CellConnectionStats::default()))
         505  +
        Self::pending_open(
         506  +
            info,
         507  +
            StdArc::new(TestDriverSpawner),
         508  +
            Arc::new(CellConnectionStats::default()),
         509  +
        )
         510  +
    }
         511  +
         512  +
    /// Returns the runtime that owns this installed connection.
         513  +
    pub(super) fn owner_spawner(&self) -> StdArc<dyn DriverSpawner> {
         514  +
        self.owner_spawner.clone()
  464    515   
    }
  465    516   
  466    517   
    /// Opens dispatch commitment and transfers optional bounded capacity.
  467    518   
    ///
  468    519   
    /// The caller performs this transition after Hyper protocol setup and
  469    520   
    /// before publishing the connection through its cell. Opening first
  470    521   
    /// ensures that a newly visible request handle can commit dispatch.
  471    522   
    ///
  472    523   
    /// Returns `lease` when logical close won before opening.
  473    524   
    pub(super) fn open(&self, lease: Option<CapacityLease>) -> Result<(), Option<CapacityLease>> {
@@ -530,581 +608,671 @@
  550    601   
    /// `Opened` callback. The lifecycle transition completes immediately, but
  551    602   
    /// its callbacks are delayed until this method can preserve
  552    603   
    /// `Opened`, logical close, and physical close in that order.
  553    604   
    pub(super) fn complete_opened_event(&self, listener: Option<&SharedConnectionEventListener>) {
  554    605   
        let pending_close = {
  555    606   
            let mut lifecycle = self.lifecycle.lock();
  556    607   
            assert!(
  557    608   
                !matches!(lifecycle.logical, LogicalState::PendingOpen),
  558    609   
                "connection opened event completed before the connection opened"
  559    610   
            );
  560         -
            let close = match &lifecycle.logical {
         611  +
            let close_cause = match &lifecycle.logical {
  561    612   
                LogicalState::PendingOpen | LogicalState::Open { .. } => None,
  562         -
                LogicalState::Closed {
  563         -
                    cause, disposition, ..
  564         -
                } => Some((*cause, disposition.final_reason())),
         613  +
                LogicalState::Closed { cause, .. } => Some(*cause),
  565    614   
            };
  566         -
            let physically_closed = lifecycle.physical_connection_complete;
  567         -
            lifecycle
  568         -
                .events
  569         -
                .opened_completed(listener, close, physically_closed)
         615  +
            lifecycle.events.opened_completed(listener, close_cause)
  570    616   
        };
  571         -
        let Some(listener) = listener else {
  572         -
            return;
  573         -
        };
  574         -
        if let Some((cause, physical_reason)) = pending_close {
         617  +
        if let Some((listener, cause)) = pending_close {
  575    618   
            listener.logical_close(&self.info, cause);
  576         -
            if let Some(reason) = physical_reason {
  577         -
                listener.physical_close(&self.info, reason);
  578         -
            }
         619  +
            self.complete_logical_close_event();
         620  +
        }
         621  +
    }
         622  +
         623  +
    /// Completes logical-close callback delivery and reports a deferred
         624  +
    /// physical close.
         625  +
    fn complete_logical_close_event(&self) {
         626  +
        let pending_physical_close = {
         627  +
            let mut lifecycle = self.lifecycle.lock();
         628  +
            let physically_closed = lifecycle.physical_connection_complete;
         629  +
            let listener = lifecycle.events.logical_close_completed(physically_closed);
         630  +
            let final_reason = listener.as_ref().map(|_| match lifecycle.logical {
         631  +
                LogicalState::Closed { disposition, .. } => disposition
         632  +
                    .final_reason()
         633  +
                    .expect("physical close completed before H1 classification"),
         634  +
                LogicalState::PendingOpen | LogicalState::Open { .. } => {
         635  +
                    panic!("physical close followed an open connection")
         636  +
                }
         637  +
            });
         638  +
            listener.zip(final_reason)
         639  +
        };
         640  +
        if let Some((listener, reason)) = pending_physical_close {
         641  +
            listener.physical_close(&self.info, reason);
  579    642   
        }
  580    643   
    }
  581    644   
  582    645   
    /// Performs the first logical-close transition.
  583    646   
    ///
  584    647   
    /// Returns `true` when this call closes the connection and records
  585    648   
    /// `reason`. Returns `false` when another close already won; the original
  586    649   
    /// logical cause remains unchanged.
  587    650   
    ///
  588    651   
    /// Capacity normally returns after the connection lock is released.
@@ -615,678 +773,826 @@
  635    698   
                    }
  636    699   
                    ConnectionProtocol::Http2 => {
  637    700   
                        (CloseDisposition::Http2(reason), None, capacity, true)
  638    701   
                    }
  639    702   
                },
  640    703   
                closed @ LogicalState::Closed { .. } => {
  641    704   
                    lifecycle.logical = closed;
  642    705   
                    return false;
  643    706   
                }
  644    707   
            };
  645         -
            let final_reason = disposition.final_reason();
  646    708   
            if was_open && !lifecycle.physical_connection_complete {
  647    709   
                let counted_close = match self.info.protocol() {
  648    710   
                    ConnectionProtocol::Http1 if reason == CloseReason::Upgraded => {
  649    711   
                        CountedClose::H1Upgraded
  650    712   
                    }
  651    713   
                    ConnectionProtocol::Http1 => CountedClose::H1Draining,
  652    714   
                    ConnectionProtocol::Http2 => CountedClose::H2Draining,
  653    715   
                };
  654    716   
                assert!(
  655    717   
                    lifecycle.counted_close.is_none(),
  656    718   
                    "connection close was counted more than once"
  657    719   
                );
  658    720   
                counted_close.start(&self.stats);
  659    721   
                lifecycle.counted_close = Some(counted_close);
  660    722   
            }
  661    723   
            lifecycle.logical = LogicalState::Closed {
  662    724   
                cause,
  663    725   
                disposition,
  664    726   
                retained_capacity,
  665    727   
            };
  666         -
            let physically_closed = lifecycle.physical_connection_complete;
  667         -
            let pending_events = lifecycle
  668         -
                .events
  669         -
                .logical_closed(final_reason, physically_closed);
         728  +
            let pending_events = lifecycle.events.logical_closed();
  670    729   
            (released_capacity, pending_events)
  671    730   
        };
  672    731   
        drop(released_capacity);
  673         -
        if let Some((listener, physical_reason)) = pending_events {
         732  +
        if let Some(listener) = pending_events {
  674    733   
            listener.logical_close(&self.info, cause);
  675         -
            if let Some(reason) = physical_reason {
  676         -
                listener.physical_close(&self.info, reason);
  677         -
            }
         734  +
            self.complete_logical_close_event();
  678    735   
        }
  679    736   
        tracing::debug!(
  680    737   
            connection_id = %self.id(),
  681    738   
            connection_partition = ?self.owner_partition(),
  682    739   
            protocol = ?self.info.protocol(),
  683    740   
            origin_scheme = %self.info.origin().scheme(),
  684    741   
            origin_host = self.info.origin().host(),
  685    742   
            origin_port = ?self.info.origin().port(),
  686    743   
            close_reason = ?reason,
  687    744   
            "connection logically closed"
  688    745   
        );
  689    746   
        true
  690    747   
    }
  691    748   
  692    749   
    /// Finalizes a driver-observed HTTP/1 close from exchange-side evidence.
  693    750   
    ///
  694    751   
    /// A confirmed upgrade retains bounded capacity until root I/O leaves the
  695    752   
    /// client. Every other classification preserves the driver reason and
  696    753   
    /// returns capacity immediately.
  697    754   
    pub(super) fn complete_h1_exchange(&self, exchange_reason: CloseReason) -> bool {
  698         -
        let (released_capacity, listener, final_reason) = {
         755  +
        let (released_capacity, final_reason) = {
  699    756   
            let mut lifecycle = self.lifecycle.lock();
  700    757   
            let physical_connection_complete = lifecycle.physical_connection_complete;
  701    758   
            let (released_capacity, final_reason) = match &mut lifecycle.logical {
  702    759   
                LogicalState::Closed {
  703    760   
                    disposition: CloseDisposition::Http1(state),
  704    761   
                    retained_capacity,
  705    762   
                    ..
  706    763   
                } => {
  707    764   
                    let H1CloseDisposition::AwaitingExchange { fallback } = *state else {
  708    765   
                        return false;
  709    766   
                    };
         767  +
                    debug_assert!(
         768  +
                        !physical_connection_complete,
         769  +
                        "physical close left HTTP/1 exchange classification pending"
         770  +
                    );
  710    771   
                    let final_reason = if exchange_reason == CloseReason::Upgraded {
  711    772   
                        CloseReason::Upgraded
  712    773   
                    } else {
  713    774   
                        fallback
  714    775   
                    };
  715    776   
                    *state = H1CloseDisposition::Final(final_reason);
  716    777   
                    let release_capacity =
  717    778   
                        final_reason != CloseReason::Upgraded || physical_connection_complete;
  718    779   
                    (
  719    780   
                        release_capacity.then(|| retained_capacity.take()).flatten(),
  720    781   
                        final_reason,
  721    782   
                    )
  722    783   
                }
  723    784   
                _ => return false,
  724    785   
            };
  725    786   
            if final_reason == CloseReason::Upgraded && !physical_connection_complete {
  726    787   
                lifecycle.counted_close = Some(
  727    788   
                    lifecycle
  728    789   
                        .counted_close
  729    790   
                        .expect("upgraded HTTP/1 connection had no draining count")
  730    791   
                        .h1_upgrade(&self.stats),
  731    792   
                );
  732    793   
            }
  733         -
            let listener = if physical_connection_complete {
  734         -
                lifecycle.events.physical_closed()
  735         -
            } else {
  736         -
                None
  737         -
            };
  738         -
            (released_capacity, listener, final_reason)
         794  +
            (released_capacity, final_reason)
  739    795   
        };
  740    796   
        drop(released_capacity);
  741         -
        if let Some(listener) = listener {
  742         -
            listener.physical_close(&self.info, final_reason);
  743         -
        }
  744    797   
        tracing::debug!(
  745    798   
            connection_id = %self.id(),
  746    799   
            connection_partition = ?self.owner_partition(),
  747    800   
            origin_scheme = %self.info.origin().scheme(),
  748    801   
            origin_host = self.info.origin().host(),
  749    802   
            origin_port = ?self.info.origin().port(),
  750    803   
            close_reason = ?final_reason,
  751    804   
            exchange_reason = ?exchange_reason,
  752    805   
            "HTTP/1 connection close classification completed"
  753    806   
        );
@@ -1123,1176 +1183,1237 @@
 1143   1196   
                }
 1144   1197   
            })),
 1145   1198   
            SharedTimeSource::default(),
 1146   1199   
        );
 1147   1200   
        let mut establishment = events.establishment_started(
 1148   1201   
            test_info(1).origin(),
 1149   1202   
            PartitionId::from_index(0),
 1150   1203   
            stats.clone(),
 1151   1204   
        );
 1152   1205   
        establishment.protocol_selected(ConnectionProtocol::Http1);
 1153         -
        let (connection, physical) = ConnectionState::pending_open(test_info(1), stats);
        1206  +
        let (connection, physical) =
        1207  +
            ConnectionState::pending_open(test_info(1), StdArc::new(TestDriverSpawner), stats);
 1154   1208   
        connection.open(None).unwrap();
 1155   1209   
 1156   1210   
        establishment.opened(&connection);
 1157   1211   
        assert!(connection.logical_close(CloseReason::ProtocolClosed));
 1158   1212   
        physical.release();
 1159   1213   
 1160   1214   
        assert_eq!(
 1161   1215   
            &[
 1162   1216   
                (EventKind::Opened, 0, 0, 1),
 1163   1217   
                (EventKind::LogicalClose, 0, 1, 1),
@@ -1416,1470 +1541,1721 @@
 1436   1490   
        );
 1437   1491   
        let (connection, _physical) = ConnectionState::unbounded(info);
 1438   1492   
 1439   1493   
        assert_eq!(ConnectionId::new(7), connection.info().id());
 1440   1494   
        assert_eq!(&origin, connection.info().origin());
 1441   1495   
        assert_eq!(PartitionId::from_index(2), connection.owner_partition());
 1442   1496   
        assert_eq!(ConnectionProtocol::Http1, connection.info().protocol());
 1443   1497   
        assert_eq!(None, connection.info().local_addr());
 1444   1498   
        assert_eq!(None, connection.info().remote_addr());
 1445   1499   
        assert_eq!(ConnectPath::ForwardProxy, connection.info().connect_path());
        1500  +
        assert_eq!(None, connection.info().establishment());
 1446   1501   
        let mut extensions = Extensions::new();
 1447   1502   
        connection.info().apply_connector_extras(&mut extensions);
 1448   1503   
        assert_eq!(
 1449   1504   
            Some(&ConnectorMarker("connector-extra")),
 1450   1505   
            extensions.get::<ConnectorMarker>()
 1451   1506   
        );
 1452   1507   
    }
 1453   1508   
 1454   1509   
    #[test]
 1455   1510   
    fn dropping_connection_io_completes_physical_connection_ownership() {
 1456   1511   
        let (connection, physical) = ConnectionState::unbounded(test_info(1));
 1457   1512   
        let io = ConnectionIo::new("transport", physical);
 1458   1513   
        assert_eq!(&"transport", io.get_ref());
 1459   1514   
        assert!(!connection.probe().physical_connection_complete);
 1460   1515   
 1461   1516   
        drop(io);
 1462   1517   
 1463   1518   
        assert!(connection.probe().physical_connection_complete);
 1464   1519   
    }
 1465   1520   
}
 1466   1521   
 1467   1522   
#[cfg(all(test, smithy_http_client_loom))]
 1468   1523   
mod loom_tests {
 1469   1524   
    use super::*;
 1470   1525   
    use crate::client::pool::admission::OriginAdmission;
        1526  +
    use crate::client::pool::events::{
        1527  +
        ConnectionEvent, ConnectionEvents, SharedConnectionEventListener,
        1528  +
    };
        1529  +
    use aws_smithy_async::time::SharedTimeSource;
        1530  +
    use loom::sync::atomic::{AtomicUsize, Ordering};
 1471   1531   
    use std::num::NonZeroUsize;
 1472   1532   
 1473   1533   
    fn test_info(id: u64) -> Arc<ConnectionInfo> {
 1474   1534   
        ConnectionInfo::for_test(ConnectionId::new(id), PartitionId::from_index(0))
 1475   1535   
    }
 1476   1536   
        1537  +
    /// Races request dispatch commitment with logical close.
        1538  +
    ///
        1539  +
    /// Dispatch either commits before close and drains afterward, or close
        1540  +
    /// rejects it without incrementing the in-flight count.
 1477   1541   
    #[test]
 1478   1542   
    fn dispatch_commit_linearizes_against_close() {
 1479   1543   
        loom::model(|| {
 1480   1544   
            let (connection, _physical) = ConnectionState::unbounded(test_info(1));
 1481   1545   
 1482   1546   
            let dispatch_connection = connection.clone();
 1483   1547   
            let dispatch = loom::thread::spawn(move || {
 1484   1548   
                ConnectionState::try_commit_dispatch(&dispatch_connection)
 1485   1549   
            });
 1486   1550   
            let close_connection = connection.clone();
 1487   1551   
            let close = loom::thread::spawn(move || {
 1488   1552   
                close_connection.logical_close(CloseReason::ProtocolClosed)
 1489   1553   
            });
 1490   1554   
 1491   1555   
            let dispatch = dispatch.join().unwrap();
 1492   1556   
            assert!(close.join().unwrap());
 1493   1557   
            assert!(ConnectionState::try_commit_dispatch(&connection).is_none());
 1494   1558   
            match dispatch {
 1495   1559   
                Some(dispatch) => {
 1496   1560   
                    let probe = connection.probe();
 1497   1561   
                    assert_eq!(None, probe.close_reason);
 1498   1562   
                    assert!(probe.awaiting_h1_exchange);
 1499   1563   
                    assert_eq!(1, probe.in_flight);
 1500   1564   
                    drop(dispatch);
 1501   1565   
                    let probe = connection.probe();
 1502   1566   
                    assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
 1503   1567   
                    assert!(!probe.awaiting_h1_exchange);
 1504   1568   
                    assert_eq!(0, probe.in_flight);
 1505   1569   
                }
 1506   1570   
                None => {
 1507   1571   
                    let probe = connection.probe();
 1508   1572   
                    assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
 1509   1573   
                    assert!(!probe.awaiting_h1_exchange);
 1510   1574   
                    assert_eq!(0, probe.in_flight);
 1511   1575   
                }
 1512   1576   
            }
 1513   1577   
        });
 1514   1578   
    }
 1515   1579   
        1580  +
    /// Races two independent logical-close signals for one bounded connection.
        1581  +
    ///
        1582  +
    /// Exactly one reason becomes authoritative and its capacity lease returns once.
 1516   1583   
    #[test]
 1517   1584   
    fn concurrent_logical_close_releases_one_capacity_lease() {
 1518   1585   
        loom::model(|| {
 1519   1586   
            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
 1520   1587   
            let lease = OriginAdmission::lease_for_test(&origin);
 1521   1588   
            let (connection, _physical) = ConnectionState::bounded(test_info(1), lease);
 1522   1589   
            let first_connection = connection.clone();
 1523   1590   
            let first =
 1524   1591   
                loom::thread::spawn(move || first_connection.logical_close(CloseReason::Poisoned));
 1525   1592   
            let second_connection = connection.clone();
 1526   1593   
            let second = loom::thread::spawn(move || {
 1527   1594   
                second_connection.logical_close(CloseReason::PoolDropped)
 1528   1595   
            });
 1529   1596   
 1530   1597   
            let first = first.join().unwrap();
 1531   1598   
            let second = second.join().unwrap();
 1532   1599   
            assert_ne!(first, second);
 1533   1600   
            assert_eq!(1, origin.available_capacity_for_test());
 1534   1601   
            let reason = connection.probe().close_reason.unwrap();
 1535   1602   
            assert!(matches!(
 1536   1603   
                reason,
 1537   1604   
                CloseReason::Poisoned | CloseReason::PoolDropped
 1538   1605   
            ));
 1539   1606   
        });
 1540   1607   
    }
        1608  +
        1609  +
    /// Races HTTP/1 upgrade classification with physical connection completion.
        1610  +
    ///
        1611  +
    /// The winning transition determines the final reason while both paths
        1612  +
    /// converge on one capacity return and zero retained lifetime counts.
        1613  +
    #[test]
        1614  +
    fn h1_upgrade_classification_linearizes_against_physical_completion() {
        1615  +
        loom::model(|| {
        1616  +
            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
        1617  +
            let lease = OriginAdmission::lease_for_test(&origin);
        1618  +
            let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
        1619  +
            let dispatch = ConnectionState::try_commit_dispatch(&connection)
        1620  +
                .expect("open HTTP/1 connection rejected dispatch");
        1621  +
            assert!(connection.logical_close(CloseReason::ProtocolClosed));
        1622  +
        1623  +
            let classify_connection = connection.clone();
        1624  +
            let classify = loom::thread::spawn(move || {
        1625  +
                classify_connection.complete_h1_exchange(CloseReason::Upgraded)
        1626  +
            });
        1627  +
            let complete_physical = loom::thread::spawn(move || physical.release());
        1628  +
        1629  +
            let classified_as_upgrade = classify.join().unwrap();
        1630  +
            complete_physical.join().unwrap();
        1631  +
            dispatch.release();
        1632  +
        1633  +
            let probe = connection.probe();
        1634  +
            assert_eq!(
        1635  +
                Some(if classified_as_upgrade {
        1636  +
                    CloseReason::Upgraded
        1637  +
                } else {
        1638  +
                    CloseReason::ProtocolClosed
        1639  +
                }),
        1640  +
                probe.close_reason
        1641  +
            );
        1642  +
            assert!(!probe.awaiting_h1_exchange);
        1643  +
            assert_eq!(0, probe.in_flight);
        1644  +
            assert!(probe.physical_connection_complete);
        1645  +
            assert_eq!(1, origin.available_capacity_for_test());
        1646  +
        1647  +
            let stats = connection.stats.snapshot(0, 0, 0, 0, 0);
        1648  +
            assert_eq!(0, stats.h1().draining());
        1649  +
            assert_eq!(0, stats.h1().upgraded());
        1650  +
            assert_eq!(0, stats.physically_live_connections());
        1651  +
        });
        1652  +
    }
        1653  +
        1654  +
    /// Races `Opened`, accepted HTTP/1 completion, and both close transitions.
        1655  +
    ///
        1656  +
    /// The production lifecycle must report each event once and preserve
        1657  +
    /// `Opened -> LogicalClose -> PhysicalClose` for every interleaving.
        1658  +
    #[test]
        1659  +
    fn installed_events_remain_ordered_across_concurrent_close() {
        1660  +
        let mut model = loom::model::Builder::new();
        1661  +
        model.preemption_bound.get_or_insert(3);
        1662  +
        model.check(|| {
        1663  +
            let event_sequence = Arc::new(AtomicUsize::new(0));
        1664  +
            let observed = event_sequence.clone();
        1665  +
            let events = ConnectionEvents::new(
        1666  +
                Some(SharedConnectionEventListener::new(
        1667  +
                    move |event: &ConnectionEvent<'_>| {
        1668  +
                        let code = match event {
        1669  +
                            ConnectionEvent::Opened(_) => 1,
        1670  +
                            ConnectionEvent::LogicalClose(_) => 2,
        1671  +
                            ConnectionEvent::PhysicalClose(_) => 3,
        1672  +
                            ConnectionEvent::EstablishmentFailed(_) => 4,
        1673  +
                        };
        1674  +
                        observed
        1675  +
                            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |sequence| {
        1676  +
                                Some(sequence * 4 + code)
        1677  +
                            })
        1678  +
                            .expect("event sequence update is infallible");
        1679  +
                    },
        1680  +
                )),
        1681  +
                SharedTimeSource::default(),
        1682  +
            );
        1683  +
            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
        1684  +
            let lease = OriginAdmission::lease_for_test(&origin);
        1685  +
            let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
        1686  +
            let dispatch = ConnectionState::try_commit_dispatch(&connection)
        1687  +
                .expect("open HTTP/1 connection rejected dispatch");
        1688  +
            let establishment = events.establishment_started(
        1689  +
                connection.info().origin(),
        1690  +
                connection.owner_partition(),
        1691  +
                connection.stats.clone(),
        1692  +
            );
        1693  +
        1694  +
            let opened_connection = connection.clone();
        1695  +
            let opened = loom::thread::spawn(move || establishment.opened(&opened_connection));
        1696  +
            let close_connection = connection.clone();
        1697  +
            let close = loom::thread::spawn(move || {
        1698  +
                close_connection.logical_close(CloseReason::ProtocolClosed)
        1699  +
            });
        1700  +
            let complete_physical = loom::thread::spawn(move || physical.release());
        1701  +
            let complete_dispatch = loom::thread::spawn(move || dispatch.release());
        1702  +
        1703  +
            opened.join().unwrap();
        1704  +
            assert!(close.join().unwrap());
        1705  +
            complete_physical.join().unwrap();
        1706  +
            complete_dispatch.join().unwrap();
        1707  +
        1708  +
            assert_eq!(
        1709  +
                27,
        1710  +
                event_sequence.load(Ordering::SeqCst),
        1711  +
                "installed connection events were missing, duplicated, or reordered"
        1712  +
            );
        1713  +
            assert_eq!(1, origin.available_capacity_for_test());
        1714  +
            let probe = connection.probe();
        1715  +
            assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
        1716  +
            assert!(!probe.awaiting_h1_exchange);
        1717  +
            assert_eq!(0, probe.in_flight);
        1718  +
            assert!(probe.physical_connection_complete);
        1719  +
        });
        1720  +
    }
 1541   1721   
}

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

@@ -10,10 +152,284 @@
   30     30   
use self::h2::H2DispatchOutcome;
   31     31   
use super::admission::ProtocolRequirement;
   32     32   
use super::cell::h1::H1Selection;
   33     33   
use super::cell::h2::H2Activation;
   34     34   
use super::cell::{AcquisitionOutcome, AcquisitionStep, OriginCell, WaiterId};
   35     35   
use super::establish::{self, TransportTimeout};
   36     36   
use super::partition::DriverSpawner;
   37     37   
use super::registry::PartitionState;
   38     38   
use super::{ConnectionPool, PoolInner};
   39     39   
use crate::sync::Arc;
          40  +
use aws_smithy_async::time::SharedTimeSource;
          41  +
use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
          42  +
use aws_smithy_runtime_api::client::http::telemetry::{
          43  +
    CaptureHttpAttemptTelemetry, ConnectionAcquisitionTelemetry, ConnectionUsage,
          44  +
};
   40     45   
use aws_smithy_runtime_api::client::result::ConnectorError;
   41     46   
use aws_smithy_types::body::SdkBody;
   42     47   
use http_1x::{header, Method, Request, Response, Uri, Version};
   43     48   
use std::future::poll_fn;
   44     49   
use std::sync::Arc as StdArc;
          50  +
use std::time::SystemTime;
   45     51   
   46     52   
/// Replacement selections allowed after the initial HTTP/2 dispatch attempt.
   47     53   
const MAX_H2_REPLACEMENTS: usize = 2;
   48     54   
   49     55   
/// Operation settings known before the request's origin cell is resolved.
   50     56   
///
   51     57   
/// This value is separate from [`AcquisitionContext`], which also contains
   52     58   
/// cell and runtime state discovered by dispatch.
   53     59   
#[derive(Clone, Debug, Default)]
   54     60   
pub(super) struct RequestOptions {
   55     61   
    /// Transport connection timeout configured for this operation.
   56     62   
    connect_timeout: Option<TransportTimeout>,
          63  +
    /// Inputs needed to start request-attempt observation at acquisition.
          64  +
    attempt_telemetry: Option<AttemptTelemetryInput>,
   57     65   
}
   58     66   
   59     67   
impl RequestOptions {
   60     68   
    /// Creates operation settings for one pool request.
   61         -
    pub(super) fn new(connect_timeout: Option<TransportTimeout>) -> Self {
   62         -
        Self { connect_timeout }
          69  +
    pub(super) fn new(
          70  +
        connect_timeout: Option<TransportTimeout>,
          71  +
        attempt_telemetry: Option<AttemptTelemetryInput>,
          72  +
    ) -> Self {
          73  +
        Self {
          74  +
            connect_timeout,
          75  +
            attempt_telemetry,
          76  +
        }
          77  +
    }
          78  +
}
          79  +
          80  +
/// Capture and operation runtime clock paired before pool dispatch.
          81  +
#[derive(Clone, Debug)]
          82  +
pub(super) struct AttemptTelemetryInput {
          83  +
    capture: CaptureHttpAttemptTelemetry,
          84  +
    time_source: SharedTimeSource,
          85  +
}
          86  +
          87  +
impl AttemptTelemetryInput {
          88  +
    /// Pairs the request capture with the clock that measures its attempt.
          89  +
    pub(super) fn new(capture: CaptureHttpAttemptTelemetry, time_source: SharedTimeSource) -> Self {
          90  +
        Self {
          91  +
            capture,
          92  +
            time_source,
          93  +
        }
   63     94   
    }
   64     95   
}
   65     96   
   66     97   
/// Stable state shared by acquisition and protocol dispatch for one request.
   67     98   
///
   68     99   
/// This context owns ordinary shared configuration and identities. Linear
   69    100   
/// acquisition values, such as an establishment permit or an exclusive
   70    101   
/// protocol handle, remain separate arguments so their transfer is explicit.
   71    102   
#[derive(Clone)]
   72    103   
pub(super) struct AcquisitionContext {
   73    104   
    /// Shared pool policy and transport construction.
   74    105   
    pub(super) pool: Arc<PoolInner>,
   75    106   
    /// Partition from which the request was issued.
   76    107   
    pub(super) partition: Arc<PartitionState>,
   77    108   
    /// Partition-local state for the request's canonical origin.
   78    109   
    pub(super) cell: Arc<OriginCell>,
   79    110   
    /// Absolute request URI retained across protocol wire-form changes.
   80    111   
    pub(super) absolute_uri: Uri,
   81    112   
    /// Runtime that owns establishment, drivers, and pending return work.
   82    113   
    pub(super) owner_spawner: StdArc<dyn DriverSpawner>,
   83    114   
    /// Transport connection timeout configured for this operation.
   84    115   
    pub(super) connect_timeout: Option<TransportTimeout>,
         116  +
    /// Request-attempt observation retained until Hyper accepts a connection.
         117  +
    pub(super) attempt_telemetry: Option<AttemptTelemetry>,
         118  +
}
         119  +
         120  +
/// Request-scoped capture and acquisition start time.
         121  +
///
         122  +
/// This value exists only when the request carries a telemetry capture, so an
         123  +
/// unobserved request does not read the clock.
         124  +
#[derive(Clone)]
         125  +
pub(super) struct AttemptTelemetry {
         126  +
    /// Shared attempt capture installed on the Smithy request.
         127  +
    capture: CaptureHttpAttemptTelemetry,
         128  +
    /// Time at which this request entered pool acquisition.
         129  +
    acquisition_started_at: SystemTime,
         130  +
    /// Operation runtime clock used for this request attempt.
         131  +
    time_source: SharedTimeSource,
         132  +
}
         133  +
         134  +
impl AttemptTelemetry {
         135  +
    /// Starts connection acquisition using the operation runtime clock.
         136  +
    fn start(input: AttemptTelemetryInput) -> Self {
         137  +
        let AttemptTelemetryInput {
         138  +
            capture,
         139  +
            time_source,
         140  +
        } = input;
         141  +
        let acquisition_started_at = time_source.now();
         142  +
        Self {
         143  +
            capture,
         144  +
            acquisition_started_at,
         145  +
            time_source,
         146  +
        }
         147  +
    }
         148  +
         149  +
    /// Freezes one selected connection when the protocol accepts the request.
         150  +
    fn freeze_selection(
         151  +
        &self,
         152  +
        reused: bool,
         153  +
        metadata: Option<&ConnectionMetadata>,
         154  +
    ) -> FrozenConnectionSelection {
         155  +
        FrozenConnectionSelection {
         156  +
            capture: self.capture.clone(),
         157  +
            acquisition_started_at: self.acquisition_started_at,
         158  +
            acquisition_completed_at: self.time_source.now(),
         159  +
            usage: if reused {
         160  +
                ConnectionUsage::Reused
         161  +
            } else {
         162  +
                ConnectionUsage::Fresh
         163  +
            },
         164  +
            metadata: metadata
         165  +
                .expect("attempt telemetry requires selected connection metadata")
         166  +
                .clone(),
         167  +
        }
         168  +
    }
         169  +
}
         170  +
         171  +
/// Selected connection facts held until the request cannot return to acquisition.
         172  +
pub(super) struct FrozenConnectionSelection {
         173  +
    capture: CaptureHttpAttemptTelemetry,
         174  +
    acquisition_started_at: SystemTime,
         175  +
    acquisition_completed_at: SystemTime,
         176  +
    usage: ConnectionUsage,
         177  +
    metadata: ConnectionMetadata,
         178  +
}
         179  +
         180  +
impl FrozenConnectionSelection {
         181  +
    /// Commits the selected connection and any valid acquisition interval.
         182  +
    pub(super) fn record(self) {
         183  +
        let acquisition = ConnectionAcquisitionTelemetry::from_interval(
         184  +
            self.acquisition_started_at,
         185  +
            self.acquisition_completed_at,
         186  +
            self.usage,
         187  +
        );
         188  +
        self.capture
         189  +
            .record_connection_selection(acquisition, self.metadata);
         190  +
    }
         191  +
}
         192  +
         193  +
impl AcquisitionContext {
         194  +
    fn captures_connection_selection(&self) -> bool {
         195  +
        self.attempt_telemetry.is_some()
         196  +
    }
         197  +
         198  +
    /// Freezes the selected connection when Hyper may retain the request.
         199  +
    ///
         200  +
    /// Protocol dispatch records the resulting telemetry only after the request
         201  +
    /// can no longer be returned to acquisition.
         202  +
    pub(super) fn freeze_connection_selection(
         203  +
        &self,
         204  +
        reused: bool,
         205  +
        metadata: Option<&ConnectionMetadata>,
         206  +
    ) -> Option<FrozenConnectionSelection> {
         207  +
        self.attempt_telemetry
         208  +
            .as_ref()
         209  +
            .map(|telemetry| telemetry.freeze_selection(reused, metadata))
         210  +
    }
   85    211   
}
   86    212   
   87    213   
/// Resolves request-scoped pool state and runs acquisition through dispatch.
   88    214   
pub(super) async fn send(
   89    215   
    pool: &ConnectionPool,
   90    216   
    partition: Arc<PartitionState>,
   91    217   
    request: Request<SdkBody>,
   92    218   
    options: RequestOptions,
   93    219   
) -> Result<Response<SdkBody>, ConnectorError> {
   94    220   
    validate_request_before_acquisition(&request)
   95    221   
        .map_err(|error| ConnectorError::user(error.into()))?;
   96    222   
         223  +
    let RequestOptions {
         224  +
        connect_timeout,
         225  +
        attempt_telemetry,
         226  +
    } = options;
         227  +
    let attempt_telemetry = attempt_telemetry.map(AttemptTelemetry::start);
   97    228   
    let absolute_uri = request.uri().clone();
   98    229   
    let cell = pool
   99    230   
        .inner
  100    231   
        .registry
  101    232   
        .resolve_cell(&partition, &absolute_uri)
  102    233   
        .map_err(|error| ConnectorError::user(error.into()))?;
  103    234   
    tracing::trace!(
  104    235   
        request_partition = ?partition.id(),
  105    236   
        origin_scheme = %cell.id().origin().scheme(),
  106    237   
        origin_host = cell.id().origin().host(),
  107    238   
        origin_port = ?cell.id().origin().port(),
  108    239   
        "request resolved to connection-pool cell"
  109    240   
    );
  110    241   
  111    242   
    let owner_spawner = partition
  112    243   
        .owner_spawner()
  113    244   
        .map_err(|error| ConnectorError::user(error.into()))?;
  114    245   
    partition.ensure_maintenance_started(owner_spawner.as_ref());
  115    246   
  116    247   
    let context = AcquisitionContext {
  117    248   
        pool: pool.inner.clone(),
  118    249   
        partition,
  119    250   
        cell,
  120    251   
        absolute_uri,
  121    252   
        owner_spawner,
  122         -
        connect_timeout: options.connect_timeout,
         253  +
        connect_timeout,
         254  +
        attempt_telemetry,
  123    255   
    };
  124    256   
  125    257   
    acquire_and_dispatch(context, request).await
  126    258   
}
  127    259   
  128    260   
/// Request authority ready for one protocol-specific dispatch attempt.
  129    261   
enum DispatchTarget {
  130    262   
    /// Exclusive HTTP/1 sender ownership.
  131    263   
    H1(H1Selection),
  132    264   
    /// Prospective request reservation on one exact HTTP/2 generation.

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

@@ -1,1 +223,232 @@
   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     18   
use super::{AcquisitionContext, H1HostHeaderInserted, RequestDispatchError};
   19     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         -
use std::future::poll_fn;
          25  +
use std::future::{poll_fn, Future};
   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     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     35   
    NotAccepted(Request<SdkBody>),
   36     36   
}
   37     37   
   38     38   
/// Attempts one dispatch through an acquired HTTP/1 selection.
   39     39   
///
   40     40   
/// The parent may reacquire only when this function returns the original
   41     41   
/// request. A fresh connection's send error is terminal even if Hyper returns
   42     42   
/// the request.
   43     43   
pub(super) async fn dispatch(
   44     44   
    context: &AcquisitionContext,
   45     45   
    mut request: Request<SdkBody>,
   46     46   
    mut selection: H1Selection,
   47     47   
) -> Result<H1DispatchOutcome, ConnectorError> {
   48     48   
    let request_method = request.method().clone();
   49     49   
    let reused = selection.is_reused();
   50     50   
    let connection = selection.connection().clone();
   51     51   
    let close_handle = selection.close_handle();
   52         -
    let captured_metadata = request
          52  +
    let connection_capture = request
   53     53   
        .extensions()
   54     54   
        .get::<CaptureSmithyConnection>()
   55         -
        .cloned()
   56         -
        .map(|capture| {
   57         -
            let metadata = connection.info().metadata(close_handle.clone());
   58         -
            let captured = metadata.clone();
   59         -
            capture.set_connection_retriever(move || Some(captured.clone()));
   60         -
            metadata
   61         -
        });
          55  +
        .cloned();
          56  +
    let captured_metadata = (connection_capture.is_some()
          57  +
        || context.captures_connection_selection())
          58  +
    .then(|| connection.info().metadata(close_handle.clone()));
          59  +
    if let (Some(capture), Some(metadata)) = (connection_capture, &captured_metadata) {
          60  +
        let captured = metadata.clone();
          61  +
        capture.set_connection_retriever(move || Some(captured.clone()));
          62  +
    }
   62     63   
   63     64   
    if request.version() == Version::HTTP_2 {
   64     65   
        let metadata =
   65     66   
            captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
   66     67   
        return Err(ConnectorError::user(
   67     68   
            "an HTTP/2 request cannot use an HTTP/1 connection".into(),
   68     69   
        )
   69     70   
        .with_connection(metadata));
   70     71   
    }
   71     72   
   72     73   
    add_host_header(&mut request, &context.absolute_uri)
   73     74   
        .map_err(|error| ConnectorError::user(error.into()))?;
   74     75   
    let connect_path = connection.info().connect_path_inner();
   75     76   
    connect_path.apply_proxy_authorization(request.headers_mut());
   76     77   
    rewrite_h1_request_target(&mut request, connect_path.uses_absolute_form());
   77     78   
   78     79   
    // Commit against logical close before transferring the request to
   79     80   
    // Hyper. A close that wins this race leaves the request untouched.
   80     81   
    let Some(dispatch) = ConnectionState::try_commit_dispatch(&connection) else {
   81     82   
        tracing::trace!(
   82     83   
            connection_id = %connection.id(),
   83     84   
            request_partition = ?context.partition.id(),
   84     85   
            connection_partition = ?connection.owner_partition(),
   85     86   
            origin_scheme = %connection.info().origin().scheme(),
   86     87   
            origin_host = connection.info().origin().host(),
   87     88   
            origin_port = ?connection.info().origin().port(),
   88     89   
            "HTTP/1 selection became stale before dispatch"
   89     90   
        );
   90     91   
        *request.uri_mut() = context.absolute_uri.clone();
   91     92   
        selection.retire_connection(CloseReason::ProtocolClosed);
   92     93   
        return Ok(H1DispatchOutcome::NotAccepted(request));
   93     94   
    };
   94     95   
    let send = selection.sender_mut().hyper_mut().try_send_request(request);
   95         -
   96     96   
    let exchange = selection.into_exchange();
   97         -
    match send.await {
          97  +
    let mut send = std::pin::pin!(send);
          98  +
    // Freeze acquisition time when Hyper first retains the request. Publish it
          99  +
    // only after a later error can no longer return the request envelope.
         100  +
    let first = poll_fn(|cx| Poll::Ready(send.as_mut().poll(cx))).await;
         101  +
    let (result, connection_selection) = match first {
         102  +
        Poll::Ready(result) => (
         103  +
            result,
         104  +
            context.freeze_connection_selection(reused, captured_metadata.as_ref()),
         105  +
        ),
         106  +
        Poll::Pending => {
         107  +
            let selection = context.freeze_connection_selection(reused, captured_metadata.as_ref());
         108  +
            (send.await, selection)
         109  +
        }
         110  +
    };
         111  +
    match result {
   98    112   
        Ok(mut response) => {
         113  +
            if let Some(selection) = connection_selection {
         114  +
                selection.record();
         115  +
            }
   99    116   
            // Response-body ownership keeps both the accepted dispatch
  100    117   
            // and exclusive request handle out of the pool until Hyper
  101    118   
            // proves a complete message boundary.
  102    119   
            connection
  103    120   
                .info()
  104    121   
                .apply_connector_extras(response.extensions_mut());
  105    122   
            Ok(H1DispatchOutcome::Response(guard_h1_response(
  106    123   
                response,
  107    124   
                request_method,
  108    125   
                exchange,
  109    126   
                dispatch,
  110         -
                context.owner_spawner.clone(),
  111    127   
            )))
  112    128   
        }
  113    129   
        Err(mut error) => {
  114    130   
            if let Some(mut returned) = error.take_message() {
  115    131   
                exchange.retire_connection(CloseReason::ProtocolClosed);
  116    132   
                drop(dispatch);
  117    133   
                if reused {
  118    134   
                    tracing::trace!(
  119    135   
                        connection_id = %connection.id(),
  120    136   
                        request_partition = ?context.partition.id(),
  121    137   
                        connection_partition = ?connection.owner_partition(),
  122    138   
                        origin_scheme = %connection.info().origin().scheme(),
  123    139   
                        origin_host = connection.info().origin().host(),
  124    140   
                        origin_port = ?connection.info().origin().port(),
  125    141   
                        "reused HTTP/1 connection returned request unsent"
  126    142   
                    );
  127    143   
                    *returned.uri_mut() = context.absolute_uri.clone();
  128    144   
                    return Ok(H1DispatchOutcome::NotAccepted(returned));
  129    145   
                }
  130    146   
                let metadata =
  131    147   
                    captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
  132    148   
                return Err(downcast_error(Box::new(error.into_error())).with_connection(metadata));
  133    149   
            }
  134    150   
            exchange.retire_connection(CloseReason::IncompleteH1Exchange);
  135    151   
            drop(dispatch);
         152  +
            if let Some(selection) = connection_selection {
         153  +
                selection.record();
         154  +
            }
  136    155   
            let metadata =
  137    156   
                captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
  138    157   
            Err(downcast_error(Box::new(error.into_error())).with_connection(metadata))
  139    158   
        }
  140    159   
    }
  141    160   
}
  142    161   
  143    162   
/// Attaches connection return and dispatch completion to an accepted response.
  144    163   
///
  145    164   
/// An ordinary response retains both guards until Hyper proves a complete
  146    165   
/// message boundary. Successful CONNECT and `101` responses retire the pool
  147    166   
/// record before the response exposes Hyper's upgraded root I/O.
  148    167   
fn guard_h1_response(
  149    168   
    response: Response<hyper::body::Incoming>,
  150    169   
    method: Method,
  151    170   
    exchange: H1Exchange,
  152    171   
    dispatch: DispatchGuard,
  153         -
    spawner: StdArc<dyn DriverSpawner>,
  154    172   
) -> Response<SdkBody> {
  155    173   
    let upgrade = response.status() == http_1x::StatusCode::SWITCHING_PROTOCOLS
  156    174   
        || (method == Method::CONNECT && response.status().is_success());
  157    175   
    let (parts, body) = response.into_parts();
  158    176   
    if upgrade {
  159    177   
        exchange.retire_connection(CloseReason::Upgraded);
  160    178   
        dispatch.release();
  161    179   
        return Response::from_parts(parts, SdkBody::from_body_1_x(body));
  162    180   
    }
  163         -
    let body = H1ResponseBody::new(body, exchange, dispatch, spawner);
         181  +
    let body = H1ResponseBody::new(body, exchange, dispatch);
  164    182   
    Response::from_parts(parts, SdkBody::from_body_1_x(body))
  165    183   
}
  166    184   
  167    185   
/// Response body that retains the HTTP/1 exchange until its message boundary.
  168    186   
///
  169    187   
/// End-of-stream returns a ready request handle to pool policy. A body error,
  170    188   
/// or dropping the body before Hyper reports end-of-stream, retires the
  171    189   
/// connection instead.
  172    190   
struct H1ResponseBody {
  173    191   
    /// Hyper response body whose terminal state determines message completion.
  174    192   
    inner: hyper::body::Incoming,
  175    193   
    /// Sender and dispatch ownership until one terminal body observation.
  176    194   
    lifecycle: Option<H1ResponseLifecycle>,
  177    195   
}
  178    196   
  179    197   
impl H1ResponseBody {
  180    198   
    /// Wraps a response and immediately completes a body already at end stream.
  181         -
    fn new(
  182         -
        inner: hyper::body::Incoming,
  183         -
        exchange: H1Exchange,
  184         -
        dispatch: DispatchGuard,
  185         -
        spawner: StdArc<dyn DriverSpawner>,
  186         -
    ) -> Self {
         199  +
    fn new(inner: hyper::body::Incoming, exchange: H1Exchange, dispatch: DispatchGuard) -> Self {
  187    200   
        let mut body = Self {
  188    201   
            inner,
  189         -
            lifecycle: Some(H1ResponseLifecycle {
  190         -
                exchange: Some(exchange),
  191         -
                dispatch: Some(dispatch),
  192         -
                spawner,
  193         -
            }),
         202  +
            lifecycle: Some(H1ResponseLifecycle::new(exchange, dispatch)),
  194    203   
        };
  195    204   
        if body.inner.is_end_stream() {
  196    205   
            body.finish_without_context();
  197    206   
        }
  198    207   
        body
  199    208   
    }
  200    209   
  201    210   
    /// Completes response ownership using the current body-task waker.
  202    211   
    fn finish_with_context(&mut self, cx: &mut Context<'_>) {
  203    212   
        if let Some(lifecycle) = self.lifecycle.take() {
@@ -266,275 +325,344 @@
  286    295   
/// returns the sender through [`H1Exchange::offer_for_reuse`]; readiness failure
  287    296   
/// retires the connection. Dropping the submitted task before either result retires the
  288    297   
/// exchange as [`CloseReason::OwnerRuntimeShutdown`], so an unproven sender can
  289    298   
/// never re-enter the pool.
  290    299   
struct H1ReadinessTask {
  291    300   
    /// Exchange retired as owner-runtime shutdown if the task is dropped.
  292    301   
    exchange: Option<H1Exchange>,
  293    302   
}
  294    303   
  295    304   
impl H1ResponseLifecycle {
         305  +
    /// Retains response ownership and derives follow-up placement from the connection.
         306  +
    fn new(exchange: H1Exchange, dispatch: DispatchGuard) -> Self {
         307  +
        let spawner = exchange.connection().owner_spawner();
         308  +
        Self {
         309  +
            exchange: Some(exchange),
         310  +
            dispatch: Some(dispatch),
         311  +
            spawner,
         312  +
        }
         313  +
    }
         314  +
  296    315   
    /// Returns or retires the request handle and completes dispatch accounting.
  297    316   
    ///
  298    317   
    /// Readiness is first polled with the response body's task context when one
  299    318   
    /// is available. Pending readiness moves to an owner-runtime task so the
  300    319   
    /// completed body does not retain connection ownership.
  301    320   
    fn resolve(mut self, cx: Option<&mut Context<'_>>) {
  302    321   
        let mut exchange = self
  303    322   
            .exchange
  304    323   
            .take()
  305    324   
            .expect("HTTP/1 response lifecycle lost its exchange");
@@ -399,418 +531,568 @@
  419    438   
    }
  420    439   
    let mut parts = Parts::default();
  421    440   
    parts.path_and_query = request.uri().path_and_query().cloned();
  422    441   
    *request.uri_mut() = Uri::from_parts(parts).expect("request path and query are a valid URI");
  423    442   
}
  424    443   
  425    444   
#[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
  426    445   
mod tests {
  427    446   
    use super::*;
  428    447   
    use crate::client::connect::ConnectPathInner;
         448  +
    use crate::client::pool::admission::{OriginAdmission, ProtocolRequirement};
  429    449   
    use crate::client::pool::cell::h1::H1Sender;
  430         -
    use crate::client::pool::cell::OriginCell;
         450  +
    use crate::client::pool::cell::{AcquisitionOutcome, AcquisitionStep, OriginCell};
  431    451   
    use crate::client::pool::connection::ConnectionInfo;
  432    452   
    use crate::client::pool::dispatch::RequestOptions;
         453  +
    use crate::client::pool::origin::OriginKey;
         454  +
    use crate::client::pool::partition::EligibilityGroup;
  433    455   
    use crate::client::pool::registry::PartitionState;
         456  +
    use crate::client::pool::stats::CellConnectionStats;
  434    457   
    use crate::client::pool::{
  435    458   
        Client, ConnectionId, ConnectionPool, ConnectionReuseScope, Partition, PartitionId,
  436    459   
        TokioDriverSpawner,
  437    460   
    };
  438    461   
    use crate::client::timeout::test::{NeverConnects, NeverReplies};
  439    462   
    use crate::sync::Arc;
  440    463   
    use aws_smithy_async::rt::sleep::TokioSleep;
  441    464   
    use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
  442    465   
    use aws_smithy_runtime_api::client::http::{HttpClient, HttpConnector, HttpConnectorSettings};
  443    466   
    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
  444    467   
    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
         468  +
    use http_1x::uri::Scheme;
  445    469   
    use http_body_util::BodyExt;
  446    470   
    use hyper_util::client::legacy::connect::{Connected, Connection};
  447    471   
    use hyper_util::rt::TokioIo;
  448    472   
    use std::future::Future;
  449    473   
    use std::io::{self, IoSlice};
         474  +
    use std::num::NonZeroUsize;
  450    475   
    use std::sync::atomic::{AtomicUsize, Ordering};
  451    476   
    use std::task::{Context, Poll};
  452    477   
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
  453    478   
    use tokio::net::{TcpListener, TcpStream};
  454    479   
    use tokio::sync::Notify;
  455    480   
    use tower::Service;
  456    481   
  457    482   
    #[derive(Debug)]
  458    483   
    struct DroppingSpawner;
  459    484   
  460    485   
    impl DriverSpawner for DroppingSpawner {
  461    486   
        fn spawn(&self, driver: Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>) {
  462    487   
            drop(driver);
  463    488   
        }
  464    489   
    }
  465    490   
  466    491   
    #[derive(Clone, Debug)]
  467    492   
    struct TrackingSpawner {
  468    493   
        active: StdArc<AtomicUsize>,
  469    494   
    }
  470    495   
  471    496   
    impl DriverSpawner for TrackingSpawner {
  472    497   
        fn spawn(&self, driver: Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>) {
  473    498   
            self.active.fetch_add(1, Ordering::SeqCst);
  474    499   
            let active = self.active.clone();
  475    500   
            tokio::spawn(async move {
  476    501   
                struct ActiveGuard(StdArc<AtomicUsize>);
  477    502   
  478    503   
                impl Drop for ActiveGuard {
  479    504   
                    fn drop(&mut self) {
  480    505   
                        self.0.fetch_sub(1, Ordering::SeqCst);
  481    506   
                    }
  482    507   
                }
  483    508   
  484    509   
                let _guard = ActiveGuard(active);
  485    510   
                driver.await;
  486    511   
            });
  487    512   
        }
  488    513   
    }
  489    514   
  490    515   
    #[derive(Clone, Debug)]
  491    516   
    struct CountingSpawner {
  492    517   
        submitted: StdArc<AtomicUsize>,
  493    518   
    }
  494    519   
  495    520   
    impl DriverSpawner for CountingSpawner {
  496    521   
        fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
  497    522   
            self.submitted.fetch_add(1, Ordering::SeqCst);
  498    523   
            drop(tokio::spawn(driver));
  499    524   
        }
  500    525   
    }
  501    526   
         527  +
    #[derive(Clone, Debug)]
         528  +
    struct CountingDroppingSpawner {
         529  +
        submitted: StdArc<AtomicUsize>,
         530  +
    }
         531  +
         532  +
    impl DriverSpawner for CountingDroppingSpawner {
         533  +
        fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
         534  +
            self.submitted.fetch_add(1, Ordering::SeqCst);
         535  +
            drop(driver);
         536  +
        }
         537  +
    }
         538  +
  502    539   
    #[derive(Clone, Debug)]
  503    540   
    struct PausedConnector {
  504    541   
        started: StdArc<Notify>,
  505    542   
        release: StdArc<Notify>,
  506    543   
        calls: StdArc<AtomicUsize>,
  507    544   
    }
  508    545   
  509    546   
    impl Service<Uri> for PausedConnector {
  510    547   
        type Response = TokioIo<TcpStream>;
  511    548   
        type Error = io::Error;
@@ -933,970 +1004,1124 @@
  953    990   
        consume(request(&pool, partition.clone(), uri.clone()).await).await;
  954    991   
        let baseline = submitted.load(Ordering::SeqCst);
  955    992   
        let cell = pool.inner.registry.resolve_cell(&partition, &uri).unwrap();
  956    993   
        let selection = OriginCell::select_h1(&cell).expect("ready sender was not reusable");
  957    994   
        let connection = selection.connection().clone();
  958    995   
        let dispatch =
  959    996   
            ConnectionState::try_commit_dispatch(&connection).expect("connection was not open");
  960    997   
        let exchange = selection.into_exchange();
  961    998   
        assert!(exchange.is_ready());
  962    999   
  963         -
        H1ResponseLifecycle {
  964         -
            exchange: Some(exchange),
  965         -
            dispatch: Some(dispatch),
  966         -
            spawner: StdArc::new(CountingSpawner {
  967         -
                submitted: submitted.clone(),
  968         -
            }),
  969         -
        }
  970         -
        .resolve(None);
        1000  +
        H1ResponseLifecycle::new(exchange, dispatch).resolve(None);
  971   1001   
  972   1002   
        assert_eq!(baseline, submitted.load(Ordering::SeqCst));
  973   1003   
    }
  974   1004   
        1005  +
    #[test]
        1006  +
    fn response_lifecycle_uses_the_connection_owner_spawner() {
        1007  +
        let pool = ConnectionPool::builder()
        1008  +
            .idle_timeout(None)
        1009  +
            .build_http()
        1010  +
            .unwrap();
        1011  +
        let partition = anonymous_partition(&pool);
        1012  +
        let uri = "http://example.com/".parse().unwrap();
        1013  +
        let cell = pool.inner.registry.resolve_cell(&partition, &uri).unwrap();
        1014  +
        let submitted = StdArc::new(AtomicUsize::new(0));
        1015  +
        let owner_spawner: StdArc<dyn DriverSpawner> = StdArc::new(CountingSpawner { submitted });
        1016  +
        let (connection, _physical) = ConnectionState::pending_open(
        1017  +
            ConnectionInfo::for_test(ConnectionId::new(1), PartitionId::ANONYMOUS),
        1018  +
            owner_spawner.clone(),
        1019  +
            Arc::new(CellConnectionStats::default()),
        1020  +
        );
        1021  +
        connection.open(None).unwrap();
        1022  +
        let selection =
        1023  +
            OriginCell::insert_selected_h1(&cell, connection.clone(), H1Sender::test(11));
        1024  +
        let dispatch =
        1025  +
            ConnectionState::try_commit_dispatch(&connection).expect("connection should be open");
        1026  +
        1027  +
        let lifecycle = H1ResponseLifecycle::new(selection.into_exchange(), dispatch);
        1028  +
        1029  +
        assert!(StdArc::ptr_eq(&owner_spawner, &lifecycle.spawner));
        1030  +
    }
        1031  +
        1032  +
    #[test]
        1033  +
    fn borrowed_pending_sender_readiness_uses_the_supplier_runtime() {
        1034  +
        let owner_partition = PartitionId::from_index(1);
        1035  +
        let requester_partition = PartitionId::from_index(2);
        1036  +
        let admission = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
        1037  +
        let origin = OriginKey::from_parts(Scheme::HTTPS, "example.com", None).unwrap();
        1038  +
        let owner_cell = OriginAdmission::register_cell(
        1039  +
            &admission,
        1040  +
            Arc::new(OriginCell::new(
        1041  +
                owner_partition,
        1042  +
                origin.clone(),
        1043  +
                EligibilityGroup::Pool,
        1044  +
                Some(admission.clone()),
        1045  +
                None,
        1046  +
            )),
        1047  +
        );
        1048  +
        let requester_cell = OriginAdmission::register_cell(
        1049  +
            &admission,
        1050  +
            Arc::new(OriginCell::new(
        1051  +
                requester_partition,
        1052  +
                origin,
        1053  +
                EligibilityGroup::Pool,
        1054  +
                Some(admission.clone()),
        1055  +
                None,
        1056  +
            )),
        1057  +
        );
        1058  +
        let submitted = StdArc::new(AtomicUsize::new(0));
        1059  +
        let owner_spawner: StdArc<dyn DriverSpawner> = StdArc::new(CountingDroppingSpawner {
        1060  +
            submitted: submitted.clone(),
        1061  +
        });
        1062  +
        let (connection, _physical) = ConnectionState::pending_open(
        1063  +
            ConnectionInfo::for_test(ConnectionId::new(1), owner_partition),
        1064  +
            owner_spawner.clone(),
        1065  +
            owner_cell.connection_stats(),
        1066  +
        );
        1067  +
        connection
        1068  +
            .open(Some(OriginAdmission::lease_for_test(&admission)))
        1069  +
            .unwrap();
        1070  +
        OriginCell::insert_idle_h1(&owner_cell, connection.clone(), H1Sender::pending_test(11));
        1071  +
        1072  +
        let waiter =
        1073  +
            OriginCell::register_waiter(&requester_cell, ProtocolRequirement::H1Compatible);
        1074  +
        let selection = match requester_cell
        1075  +
            .poll_waiter(waiter, &mut Context::from_waker(std::task::Waker::noop()))
        1076  +
        {
        1077  +
            Poll::Ready(AcquisitionStep::Resolved(AcquisitionOutcome::H1(selection))) => selection,
        1078  +
            other => panic!("peer demand did not borrow the pending HTTP/1 sender: {other:?}"),
        1079  +
        };
        1080  +
        assert_eq!(owner_partition, selection.connection().owner_partition());
        1081  +
        let dispatch =
        1082  +
            ConnectionState::try_commit_dispatch(&connection).expect("connection should be open");
        1083  +
        1084  +
        let lifecycle = H1ResponseLifecycle::new(selection.into_exchange(), dispatch);
        1085  +
        assert!(StdArc::ptr_eq(&owner_spawner, &lifecycle.spawner));
        1086  +
        lifecycle.resolve(None);
        1087  +
        1088  +
        assert_eq!(1, submitted.load(Ordering::SeqCst));
        1089  +
        assert_eq!(
        1090  +
            Some(CloseReason::OwnerRuntimeShutdown),
        1091  +
            connection.probe().close_reason
        1092  +
        );
        1093  +
    }
        1094  +
  975   1095   
    #[tokio::test]
  976   1096   
    async fn cancelling_launched_establishment_returns_its_connection_to_a_later_waiter() {
  977   1097   
        let server = TestServer::start().await;
  978   1098   
        let started = StdArc::new(Notify::new());
  979   1099   
        let release = StdArc::new(Notify::new());
  980   1100   
        let calls = StdArc::new(AtomicUsize::new(0));
  981   1101   
        let pool = super::super::super::builder::Builder::default()
  982   1102   
            .idle_timeout(None)
  983   1103   
            .max_connections_per_host(1)
  984   1104   
            .build_with_connector(PausedConnector {
@@ -1506,1626 +1565,1688 @@
 1526   1646   
            .build();
 1527   1647   
 1528   1648   
        let error = client
 1529   1649   
            .http_connector(&settings, &components)
 1530   1650   
            .call(HttpRequest::get("http://example.com/").unwrap())
 1531   1651   
            .await
 1532   1652   
            .unwrap_err();
 1533   1653   
        assert!(error.is_timeout(), "unexpected connector error: {error:?}");
 1534   1654   
    }
 1535   1655   
        1656  +
    /// Proves that a successful HTTP/1 upgrade transfers root I/O to the
        1657  +
    /// response while retaining bounded capacity. The pool may establish a
        1658  +
    /// replacement only after the caller drops the upgraded I/O.
 1536   1659   
    async fn assert_h1_upgrade_retains_capacity(
 1537   1660   
        method: Method,
 1538   1661   
        response_head: &'static [u8],
 1539   1662   
        expected_status: http_1x::StatusCode,
 1540   1663   
    ) {
 1541   1664   
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
 1542   1665   
        let endpoint = format!("http://{}", listener.local_addr().unwrap());
 1543   1666   
        let accepted = StdArc::new(AtomicUsize::new(0));
 1544   1667   
        let accepted_for_task = accepted.clone();
 1545   1668   
        let request_received = StdArc::new(Notify::new());

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

@@ -1,1 +309,317 @@
   13     13   
//! After Hyper accepts the request, the activation becomes an accepted claim.
   14     14   
//! [`H2RequestBody`] owns the upload guard and [`H2ResponseBody`] owns the
   15     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     19   
use super::super::cell::h2::{
   20     20   
    H2Activation, H2CloseHandle, H2DispatchParts, H2ResponseGuard, H2UploadGuard,
   21     21   
};
   22     22   
use super::super::connection::{CloseReason, ConnectionState};
   23         -
use super::{AcquisitionContext, H1HostHeaderInserted};
          23  +
use super::{AcquisitionContext, FrozenConnectionSelection, H1HostHeaderInserted};
   24     24   
use crate::client::downcast_error;
   25     25   
use crate::sync::{Arc, Mutex};
   26     26   
use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
   27     27   
use aws_smithy_runtime_api::client::result::ConnectorError;
   28     28   
use aws_smithy_types::body::SdkBody;
   29     29   
use http_1x::{Request, Response};
   30     30   
use hyper::body::Body;
   31     31   
use std::future::{poll_fn, Future};
   32     32   
use std::pin::Pin;
   33     33   
use std::task::{Context, Poll};
   34     34   
   35     35   
/// Result of one checked HTTP/2 dispatch attempt.
   36     36   
#[allow(
   37     37   
    clippy::large_enum_variant,
   38     38   
    reason = "boxing the successful response would allocate on every HTTP/2 request"
   39     39   
)]
   40     40   
pub(super) enum H2DispatchOutcome {
   41     41   
    /// Hyper accepted the request and produced a guarded response.
   42     42   
    Response(Response<SdkBody>),
   43     43   
    /// Hyper did not accept the request, so it may re-enter acquisition.
   44     44   
    NotAccepted(Box<H2UnacceptedRequest>),
   45     45   
}
   46     46   
   47     47   
/// Request and terminal fallback retained for one replacement selection.
   48     48   
pub(super) struct H2UnacceptedRequest {
   49     49   
    /// Original request Hyper did not accept.
   50     50   
    request: Request<SdkBody>,
   51     51   
    /// Error returned if the request exhausts its replacement budget.
   52     52   
    terminal_error: ConnectorError,
   53     53   
}
   54     54   
   55     55   
impl H2UnacceptedRequest {
   56     56   
    /// Returns the original request and its terminal fallback.
   57     57   
    pub(super) fn into_parts(self: Box<Self>) -> (Request<SdkBody>, ConnectorError) {
   58     58   
        let Self {
   59     59   
            request,
   60     60   
            terminal_error,
   61     61   
        } = *self;
   62     62   
        (request, terminal_error)
   63     63   
    }
   64     64   
}
   65     65   
   66     66   
/// State retained after Hyper accepts one request.
   67     67   
struct H2AcceptedDispatch {
   68     68   
    /// Connection retained through response creation.
   69     69   
    connection: Arc<ConnectionState>,
   70     70   
    /// Generation close authority used for connection-wide failures.
   71     71   
    close: H2CloseHandle,
   72     72   
    /// Metadata captured before the request moved into Hyper.
   73     73   
    captured_metadata: Option<aws_smithy_runtime_api::client::connection::ConnectionMetadata>,
   74     74   
    /// Response guard transferred to the guarded response body.
   75     75   
    response_guard: H2ResponseGuard,
   76     76   
    /// Whether the generation accepted an earlier request.
   77     77   
    reused: bool,
          78  +
    /// Selected-connection observation held until the request cannot return.
          79  +
    connection_selection: Option<FrozenConnectionSelection>,
   78     80   
}
   79     81   
   80     82   
/// Dispatches one request through prospective H2 request authority.
   81     83   
///
   82     84   
/// Pool-side staleness reacquires before Hyper sees the request. Once Hyper
   83     85   
/// returns an envelope, only a reused generation may reacquire.
   84     86   
pub(super) async fn dispatch(
   85     87   
    context: &AcquisitionContext,
   86     88   
    mut request: Request<SdkBody>,
   87     89   
    mut activation: H2Activation,
   88     90   
) -> Result<H2DispatchOutcome, ConnectorError> {
   89     91   
    prepare_h2_request(&mut request, &context.absolute_uri);
   90     92   
    let connection = activation.connection().clone();
   91     93   
    let reused = activation.is_reused();
   92     94   
    let close = activation.close_handle();
   93         -
    let captured_metadata = request
          95  +
    let connection_capture = request
   94     96   
        .extensions()
   95     97   
        .get::<CaptureSmithyConnection>()
   96         -
        .cloned()
   97         -
        .map(|capture| {
   98         -
            let metadata = connection.info().h2_metadata(close.clone());
   99         -
            let captured = metadata.clone();
  100         -
            capture.set_connection_retriever(move || Some(captured.clone()));
  101         -
            metadata
  102         -
        });
          98  +
        .cloned();
          99  +
    let captured_metadata = (connection_capture.is_some()
         100  +
        || context.captures_connection_selection())
         101  +
    .then(|| connection.info().h2_metadata(close.clone()));
         102  +
    if let (Some(capture), Some(metadata)) = (connection_capture, &captured_metadata) {
         103  +
        let captured = metadata.clone();
         104  +
        capture.set_connection_retriever(move || Some(captured.clone()));
         105  +
    }
  103    106   
  104    107   
    let H2DispatchParts {
  105    108   
        mut sender,
  106    109   
        upload,
  107    110   
        response,
  108    111   
    } = activation.take_dispatch_parts();
  109    112   
    if sender.is_closed() {
  110    113   
        close.close(CloseReason::ProtocolClosed);
  111    114   
        let metadata = captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  112    115   
        return resolve_unaccepted_request(
  113    116   
            request,
  114    117   
            reused,
  115    118   
            UnacceptedStage::BeforeHyper,
  116    119   
            ConnectorError::other(H2ConnectionClosedBeforeDispatch.into(), None)
  117    120   
                .with_connection(metadata),
  118    121   
        );
  119    122   
    }
  120    123   
    let Some(dispatch) = ConnectionState::try_commit_dispatch(&connection) else {
  121    124   
        let metadata = captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  122    125   
        return resolve_unaccepted_request(
  123    126   
            request,
  124    127   
            reused,
  125    128   
            UnacceptedStage::BeforeHyper,
  126    129   
            ConnectorError::other(H2ConnectionClosedBeforeDispatch.into(), None)
  127    130   
                .with_connection(metadata),
  128    131   
        );
  129    132   
    };
  130    133   
  131    134   
    let body = H2RequestBodyHandle::arm(&mut request, upload);
  132    135   
  133    136   
    let mut send = Box::pin(sender.hyper_mut().try_send_request(request));
  134    137   
    let first = poll_fn(|cx| Poll::Ready(send.as_mut().poll(cx))).await;
  135    138   
  136    139   
    match first {
  137    140   
        Poll::Ready(Err(mut error)) if error.message().is_some() => {
  138    141   
            let returned = error
  139    142   
                .take_message()
  140    143   
                .expect("checked returned request disappeared");
  141    144   
            body.clear();
  142    145   
            drop(response);
  143    146   
            drop(dispatch);
  144    147   
            drop(activation);
  145    148   
            close.close(CloseReason::ProtocolClosed);
  146    149   
            let metadata =
  147    150   
                captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  148    151   
            resolve_unaccepted_request(
  149    152   
                returned,
  150    153   
                reused,
  151    154   
                UnacceptedStage::ReturnedByHyper,
  152    155   
                downcast_error(Box::new(error.into_error())).with_connection(metadata),
  153    156   
            )
  154    157   
        }
  155    158   
        Poll::Ready(result) => {
  156    159   
            activation.accept(dispatch);
         160  +
            let connection_selection =
         161  +
                context.freeze_connection_selection(reused, captured_metadata.as_ref());
  157    162   
            let sender_closed = sender.is_closed();
  158    163   
            resolve_h2_send(
  159    164   
                sender_closed,
  160    165   
                result,
  161    166   
                H2AcceptedDispatch {
  162    167   
                    connection,
  163    168   
                    close,
  164    169   
                    captured_metadata,
  165    170   
                    response_guard: response,
  166    171   
                    reused,
         172  +
                    connection_selection,
  167    173   
                },
  168    174   
            )
  169    175   
        }
  170    176   
        Poll::Pending => {
  171    177   
            activation.accept(dispatch);
         178  +
            let connection_selection =
         179  +
                context.freeze_connection_selection(reused, captured_metadata.as_ref());
  172    180   
            let result = send.await;
  173    181   
            let sender_closed = sender.is_closed();
  174    182   
            resolve_h2_send(
  175    183   
                sender_closed,
  176    184   
                result,
  177    185   
                H2AcceptedDispatch {
  178    186   
                    connection,
  179    187   
                    close,
  180    188   
                    captured_metadata,
  181    189   
                    response_guard: response,
  182    190   
                    reused,
         191  +
                    connection_selection,
  183    192   
                },
  184    193   
            )
  185    194   
        }
  186    195   
    }
  187    196   
}
  188    197   
  189         -
#[allow(
  190         -
    clippy::result_large_err,
  191         -
    reason = "ConnectorError preserves SDK classification and connection metadata"
  192         -
)]
  193    198   
fn resolve_h2_send(
  194    199   
    sender_closed: bool,
  195    200   
    result: Result<
  196    201   
        Response<hyper::body::Incoming>,
  197    202   
        hyper::client::conn::TrySendError<Request<SdkBody>>,
  198    203   
    >,
  199    204   
    accepted: H2AcceptedDispatch,
  200    205   
) -> Result<H2DispatchOutcome, ConnectorError> {
  201    206   
    let H2AcceptedDispatch {
  202    207   
        connection,
  203    208   
        close,
  204    209   
        captured_metadata,
  205    210   
        response_guard,
  206    211   
        reused,
         212  +
        connection_selection,
  207    213   
    } = accepted;
  208    214   
    match result {
  209    215   
        Ok(mut response) => {
         216  +
            if let Some(selection) = connection_selection {
         217  +
                selection.record();
         218  +
            }
  210    219   
            connection
  211    220   
                .info()
  212    221   
                .apply_connector_extras(response.extensions_mut());
  213    222   
            let (parts, body) = response.into_parts();
  214    223   
            let body = H2ResponseBody::new(body, response_guard);
  215    224   
            Ok(H2DispatchOutcome::Response(Response::from_parts(
  216    225   
                parts,
  217    226   
                SdkBody::from_body_1_x(body),
  218    227   
            )))
  219    228   
        }
  220    229   
        Err(mut error) => {
  221    230   
            // Hyper may return a queued envelope when its connection task
  222    231   
            // drops after the first poll. The returned request is the
  223    232   
            // authority for reacquisition; an error without it is terminal.
  224    233   
            if let Some(request) = error.take_message() {
  225    234   
                if let Some(body) = request.extensions().get::<H2RequestBodyHandle>() {
  226    235   
                    body.clear();
  227    236   
                }
  228    237   
                drop(response_guard);
  229    238   
                close.close(CloseReason::ProtocolClosed);
  230    239   
                let metadata =
  231    240   
                    captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  232    241   
                return resolve_unaccepted_request(
  233    242   
                    request,
  234    243   
                    reused,
  235    244   
                    UnacceptedStage::ReturnedByHyper,
  236    245   
                    downcast_error(Box::new(error.into_error())).with_connection(metadata),
  237    246   
                );
  238    247   
            }
  239    248   
            if sender_closed {
  240    249   
                close.close(CloseReason::ProtocolClosed);
  241    250   
            }
  242    251   
            drop(response_guard);
         252  +
            if let Some(selection) = connection_selection {
         253  +
                selection.record();
         254  +
            }
  243    255   
            let metadata =
  244    256   
                captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  245    257   
            Err(downcast_error(Box::new(error.into_error())).with_connection(metadata))
  246    258   
        }
  247    259   
    }
  248    260   
}
  249    261   
  250    262   
/// Restores the absolute URI and removes only a pool-synthesized H1 `Host`.
  251    263   
fn prepare_h2_request(request: &mut Request<SdkBody>, absolute_uri: &http_1x::Uri) {
  252    264   
    *request.uri_mut() = absolute_uri.clone();
  253    265   
    if request
  254    266   
        .extensions_mut()
  255    267   
        .remove::<H1HostHeaderInserted>()
  256    268   
        .is_some()
  257    269   
    {
  258    270   
        request.headers_mut().remove(http_1x::header::HOST);
  259    271   
    }
  260    272   
}
  261    273   
  262    274   
/// Point at which the pool learned that Hyper had not accepted the request.
  263    275   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
  264    276   
enum UnacceptedStage {
  265    277   
    /// The pool rejected a stale generation before calling Hyper.
  266    278   
    BeforeHyper,
  267    279   
    /// Hyper returned the original request envelope.
  268    280   
    ReturnedByHyper,
  269    281   
}
  270    282   
  271    283   
/// Applies retry authority to a request that Hyper did not accept.
  272    284   
///
  273    285   
/// Pool-side checks always reacquire because no protocol code observed the
  274    286   
/// request. A returned Hyper envelope reacquires only after prior successful
  275    287   
/// use proves that replacing a stale pooled generation is appropriate.
  276         -
#[allow(
  277         -
    clippy::result_large_err,
  278         -
    reason = "ConnectorError preserves SDK classification and connection metadata"
  279         -
)]
  280    288   
fn resolve_unaccepted_request(
  281    289   
    request: Request<SdkBody>,
  282    290   
    reused: bool,
  283    291   
    stage: UnacceptedStage,
  284    292   
    error: ConnectorError,
  285    293   
) -> Result<H2DispatchOutcome, ConnectorError> {
  286    294   
    if stage == UnacceptedStage::BeforeHyper || reused {
  287    295   
        Ok(H2DispatchOutcome::NotAccepted(Box::new(
  288    296   
            H2UnacceptedRequest {
  289    297   
                request,

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

@@ -56,56 +154,155 @@
   76     76   
async fn run_h1_handshake(
   77     77   
    context: AcquisitionContext,
   78     78   
    permit: EstablishmentPermit,
   79     79   
    transport: ConnectedTransport,
   80     80   
    mut establishment: ConnectionEstablishment,
   81     81   
) -> Result<H1Selection, ConnectorError> {
   82     82   
    let AcquisitionContext {
   83     83   
        pool,
   84     84   
        partition,
   85     85   
        cell,
   86         -
        absolute_uri: _,
   87     86   
        owner_spawner,
   88         -
        connect_timeout: _,
          87  +
        ..
   89     88   
    } = context;
   90     89   
   91     90   
    let id = match next_connection_id(&pool) {
   92     91   
        Ok(id) => id,
   93     92   
        Err(error) => {
   94     93   
            let error = ConnectorError::other(error.into(), None);
   95     94   
            establishment.failed(&error);
   96     95   
            return Err(error);
   97     96   
        }
   98     97   
    };
   99     98   
    let info = ConnectionInfo::new(
  100     99   
        id,
  101    100   
        cell.id().origin().clone(),
  102    101   
        partition.id(),
  103    102   
        ConnectionProtocol::Http1,
  104    103   
        transport.metadata,
  105    104   
    );
  106         -
    let (connection, physical) = ConnectionState::pending_open(info, cell.connection_stats());
         105  +
    let (connection, physical) =
         106  +
        ConnectionState::pending_open(info, owner_spawner, cell.connection_stats());
  107    107   
    let io = ConnectionIo::new(transport.io, physical);
  108    108   
  109    109   
    establishment.protocol_handshake_started();
  110    110   
    let (sender, driver) = match hyper::client::conn::http1::Builder::new()
  111    111   
        .handshake::<_, SdkBody>(io)
  112    112   
        .await
  113    113   
    {
  114    114   
        Ok(established) => established,
  115    115   
        Err(error) => {
  116    116   
            let error = downcast_error(Box::new(error));
  117    117   
            establishment.protocol_handshake_failed();
  118    118   
            connection.logical_close(CloseReason::ProtocolClosed);
  119    119   
            establishment.failed(&error);
  120    120   
            return Err(error);
  121    121   
        }
  122    122   
    };
  123    123   
    establishment.protocol_handshake_completed();
  124    124   
  125    125   
    if let Err(lease) = connection.open(permit.into_lease()) {
  126    126   
        drop(lease);
  127    127   
        connection.logical_close(CloseReason::ProtocolClosed);
  128    128   
        let error = ConnectorError::io("HTTP/1 connection closed before installation".into());
  129    129   
        establishment.failed(&error);
  130    130   
        return Err(error);
  131    131   
    }
         132  +
    establishment.installed(&connection);
  132    133   
  133    134   
    let selection =
  134    135   
        OriginCell::insert_selected_h1(&cell, connection.clone(), H1Sender::from_hyper(sender));
  135    136   
    let driver_guard = H1DriverGuard::new(H1CloseHandle::new(&cell, &connection));
  136    137   
    let driver_info = connection.info().clone();
  137         -
    owner_spawner.spawn(Box::pin(async move {
         138  +
    connection.owner_spawner().spawn(Box::pin(async move {
  138    139   
        let result = driver.with_upgrades().await;
  139    140   
        if let Err(error) = result {
  140    141   
            tracing::debug!(
  141    142   
                connection_id = %driver_info.id(),
  142    143   
                connection_partition = ?driver_info.owner_partition(),
  143    144   
                origin_scheme = %driver_info.origin().scheme(),
  144    145   
                origin_host = driver_info.origin().host(),
  145    146   
                origin_port = ?driver_info.origin().port(),
  146    147   
                error = ?error,
  147    148   
                "HTTP/1 connection driver failed"