AWS SDK

AWS SDK

rev. 1ed3d5371c342b375a8e2197cd2254b0dc08a51c

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.
@@ -423,457 +508,553 @@
  443    477   
    /// For TLS transports, the connector has completed TLS and ALPN before this
  444    478   
    /// call. Hyper protocol setup and cell installation have not occurred.
  445    479   
    ///
  446    480   
    /// The returned guard uniquely tracks the client's physical connection
  447    481   
    /// handle.
  448    482   
    /// After Hyper returns a request handle, [`Self::open`] attaches optional
  449    483   
    /// bounded capacity before cell installation makes the connection
  450    484   
    /// discoverable.
  451    485   
    pub(super) fn pending_open(
  452    486   
        info: Arc<ConnectionInfo>,
         487  +
        owner_spawner: StdArc<dyn DriverSpawner>,
  453    488   
        stats: Arc<CellConnectionStats>,
  454    489   
    ) -> (Arc<Self>, PhysicalConnectionGuard) {
  455    490   
        stats.physical_connection_started();
  456    491   
        let connection = Arc::new(Self {
  457    492   
            info,
         493  +
            owner_spawner,
  458    494   
            stats,
  459    495   
            lifecycle: Mutex::new(ConnectionLifecycle {
  460    496   
                logical: LogicalState::PendingOpen,
  461    497   
                in_flight: 0,
  462    498   
                physical_connection_complete: false,
  463    499   
                counted_close: None,
  464    500   
                events: LifecycleEventProgress::OpenedPending,
  465    501   
            }),
  466    502   
        });
  467    503   
        let physical = PhysicalConnectionGuard {
  468    504   
            connection: connection.clone(),
  469    505   
            active: true,
  470    506   
        };
  471    507   
        (connection, physical)
  472    508   
    }
  473    509   
  474    510   
    #[cfg(test)]
  475    511   
    pub(super) fn pending_open_for_test(
  476    512   
        info: Arc<ConnectionInfo>,
  477    513   
    ) -> (Arc<Self>, PhysicalConnectionGuard) {
  478         -
        Self::pending_open(info, Arc::new(CellConnectionStats::default()))
         514  +
        Self::pending_open(
         515  +
            info,
         516  +
            StdArc::new(TestDriverSpawner),
         517  +
            Arc::new(CellConnectionStats::default()),
         518  +
        )
         519  +
    }
         520  +
         521  +
    /// Returns the runtime that owns this installed connection.
         522  +
    pub(super) fn owner_spawner(&self) -> StdArc<dyn DriverSpawner> {
         523  +
        self.owner_spawner.clone()
  479    524   
    }
  480    525   
  481    526   
    /// Opens dispatch commitment and transfers optional bounded capacity.
  482    527   
    ///
  483    528   
    /// The caller performs this transition after Hyper protocol setup and
  484    529   
    /// before publishing the connection through its cell. Opening first
  485    530   
    /// ensures that a newly visible request handle can commit dispatch.
  486    531   
    ///
  487    532   
    /// Returns `lease` when logical close won before opening.
  488    533   
    pub(super) fn open(&self, lease: Option<CapacityLease>) -> Result<(), Option<CapacityLease>> {
@@ -1130,1175 +1190,1236 @@
 1150   1195   
                }
 1151   1196   
            })),
 1152   1197   
            SharedTimeSource::default(),
 1153   1198   
        );
 1154   1199   
        let mut establishment = events.establishment_started(
 1155   1200   
            test_info(1).origin(),
 1156   1201   
            PartitionId::from_index(0),
 1157   1202   
            stats.clone(),
 1158   1203   
        );
 1159   1204   
        establishment.protocol_selected(ConnectionProtocol::Http1);
 1160         -
        let (connection, physical) = ConnectionState::pending_open(test_info(1), stats);
        1205  +
        let (connection, physical) =
        1206  +
            ConnectionState::pending_open(test_info(1), StdArc::new(TestDriverSpawner), stats);
 1161   1207   
        connection.open(None).unwrap();
 1162   1208   
 1163   1209   
        establishment.opened(&connection);
 1164   1210   
        assert!(connection.logical_close(CloseReason::ProtocolClosed));
 1165   1211   
        physical.release();
 1166   1212   
 1167   1213   
        assert_eq!(
 1168   1214   
            &[
 1169   1215   
                (EventKind::Opened, 0, 0, 1),
 1170   1216   
                (EventKind::LogicalClose, 0, 1, 1),
@@ -1444,1490 +1634,1741 @@
 1464   1510   
        );
 1465   1511   
        let (connection, _physical) = ConnectionState::unbounded(info);
 1466   1512   
 1467   1513   
        assert_eq!(ConnectionId::new(7), connection.info().id());
 1468   1514   
        assert_eq!(&origin, connection.info().origin());
 1469   1515   
        assert_eq!(PartitionId::from_index(2), connection.owner_partition());
 1470   1516   
        assert_eq!(ConnectionProtocol::Http1, connection.info().protocol());
 1471   1517   
        assert_eq!(None, connection.info().local_addr());
 1472   1518   
        assert_eq!(None, connection.info().remote_addr());
 1473   1519   
        assert_eq!(ConnectPath::ForwardProxy, connection.info().connect_path());
        1520  +
        assert_eq!(None, connection.info().establishment());
 1474   1521   
        let mut extensions = Extensions::new();
 1475   1522   
        connection.info().apply_connector_extras(&mut extensions);
 1476   1523   
        assert_eq!(
 1477   1524   
            Some(&ConnectorMarker("connector-extra")),
 1478   1525   
            extensions.get::<ConnectorMarker>()
 1479   1526   
        );
 1480   1527   
    }
 1481   1528   
 1482   1529   
    #[test]
 1483   1530   
    fn dropping_connection_io_completes_physical_connection_ownership() {
 1484   1531   
        let (connection, physical) = ConnectionState::unbounded(test_info(1));
 1485   1532   
        let io = ConnectionIo::new("transport", physical);
 1486   1533   
        assert_eq!(&"transport", io.get_ref());
 1487   1534   
        assert!(!connection.probe().physical_connection_complete);
 1488   1535   
 1489   1536   
        drop(io);
 1490   1537   
 1491   1538   
        assert!(connection.probe().physical_connection_complete);
 1492   1539   
    }
 1493   1540   
}
 1494   1541   
 1495   1542   
#[cfg(all(test, smithy_http_client_loom))]
 1496   1543   
mod loom_tests {
 1497   1544   
    use super::*;
 1498   1545   
    use crate::client::pool::admission::OriginAdmission;
 1499   1546   
    use crate::client::pool::events::{
 1500   1547   
        ConnectionEvent, ConnectionEvents, SharedConnectionEventListener,
 1501   1548   
    };
 1502   1549   
    use aws_smithy_async::time::SharedTimeSource;
 1503   1550   
    use loom::sync::atomic::{AtomicUsize, Ordering};
 1504   1551   
    use std::num::NonZeroUsize;
 1505   1552   
 1506   1553   
    fn test_info(id: u64) -> Arc<ConnectionInfo> {
 1507   1554   
        ConnectionInfo::for_test(ConnectionId::new(id), PartitionId::from_index(0))
 1508   1555   
    }
 1509   1556   
        1557  +
    /// Races request dispatch commitment with logical close.
        1558  +
    ///
        1559  +
    /// Dispatch either commits before close and drains afterward, or close
        1560  +
    /// rejects it without incrementing the in-flight count.
 1510   1561   
    #[test]
 1511   1562   
    fn dispatch_commit_linearizes_against_close() {
 1512   1563   
        loom::model(|| {
 1513   1564   
            let (connection, _physical) = ConnectionState::unbounded(test_info(1));
 1514   1565   
 1515   1566   
            let dispatch_connection = connection.clone();
 1516   1567   
            let dispatch = loom::thread::spawn(move || {
 1517   1568   
                ConnectionState::try_commit_dispatch(&dispatch_connection)
 1518   1569   
            });
 1519   1570   
            let close_connection = connection.clone();
 1520   1571   
            let close = loom::thread::spawn(move || {
 1521   1572   
                close_connection.logical_close(CloseReason::ProtocolClosed)
 1522   1573   
            });
 1523   1574   
 1524   1575   
            let dispatch = dispatch.join().unwrap();
 1525   1576   
            assert!(close.join().unwrap());
 1526   1577   
            assert!(ConnectionState::try_commit_dispatch(&connection).is_none());
 1527   1578   
            match dispatch {
 1528   1579   
                Some(dispatch) => {
 1529   1580   
                    let probe = connection.probe();
 1530   1581   
                    assert_eq!(None, probe.close_reason);
 1531   1582   
                    assert!(probe.awaiting_h1_exchange);
 1532   1583   
                    assert_eq!(1, probe.in_flight);
 1533   1584   
                    drop(dispatch);
 1534   1585   
                    let probe = connection.probe();
 1535   1586   
                    assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
 1536   1587   
                    assert!(!probe.awaiting_h1_exchange);
 1537   1588   
                    assert_eq!(0, probe.in_flight);
 1538   1589   
                }
 1539   1590   
                None => {
 1540   1591   
                    let probe = connection.probe();
 1541   1592   
                    assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
 1542   1593   
                    assert!(!probe.awaiting_h1_exchange);
 1543   1594   
                    assert_eq!(0, probe.in_flight);
 1544   1595   
                }
 1545   1596   
            }
 1546   1597   
        });
 1547   1598   
    }
 1548   1599   
        1600  +
    /// Races two independent logical-close signals for one bounded connection.
        1601  +
    ///
        1602  +
    /// Exactly one reason becomes authoritative and its capacity lease returns once.
 1549   1603   
    #[test]
 1550   1604   
    fn concurrent_logical_close_releases_one_capacity_lease() {
 1551   1605   
        loom::model(|| {
 1552   1606   
            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
 1553   1607   
            let lease = OriginAdmission::lease_for_test(&origin);
 1554   1608   
            let (connection, _physical) = ConnectionState::bounded(test_info(1), lease);
 1555   1609   
            let first_connection = connection.clone();
 1556   1610   
            let first =
 1557   1611   
                loom::thread::spawn(move || first_connection.logical_close(CloseReason::Poisoned));
 1558   1612   
            let second_connection = connection.clone();
 1559   1613   
            let second = loom::thread::spawn(move || {
 1560   1614   
                second_connection.logical_close(CloseReason::PoolDropped)
 1561   1615   
            });
 1562   1616   
 1563   1617   
            let first = first.join().unwrap();
 1564   1618   
            let second = second.join().unwrap();
 1565   1619   
            assert_ne!(first, second);
 1566   1620   
            assert_eq!(1, origin.available_capacity_for_test());
 1567   1621   
            let reason = connection.probe().close_reason.unwrap();
 1568   1622   
            assert!(matches!(
 1569   1623   
                reason,
 1570   1624   
                CloseReason::Poisoned | CloseReason::PoolDropped
 1571   1625   
            ));
 1572   1626   
        });
 1573   1627   
    }
 1574   1628   
 1575         -
    /// Races `Opened` completion with logical and physical close.
        1629  +
    /// Races HTTP/1 upgrade classification with physical connection completion.
        1630  +
    ///
        1631  +
    /// The winning transition determines the final reason while both paths
        1632  +
    /// converge on one capacity return and zero retained lifetime counts.
        1633  +
    #[test]
        1634  +
    fn h1_upgrade_classification_linearizes_against_physical_completion() {
        1635  +
        loom::model(|| {
        1636  +
            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
        1637  +
            let lease = OriginAdmission::lease_for_test(&origin);
        1638  +
            let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
        1639  +
            let dispatch = ConnectionState::try_commit_dispatch(&connection)
        1640  +
                .expect("open HTTP/1 connection rejected dispatch");
        1641  +
            assert!(connection.logical_close(CloseReason::ProtocolClosed));
        1642  +
        1643  +
            let classify_connection = connection.clone();
        1644  +
            let classify = loom::thread::spawn(move || {
        1645  +
                classify_connection.complete_h1_exchange(CloseReason::Upgraded)
        1646  +
            });
        1647  +
            let complete_physical = loom::thread::spawn(move || physical.release());
        1648  +
        1649  +
            let classified_as_upgrade = classify.join().unwrap();
        1650  +
            complete_physical.join().unwrap();
        1651  +
            dispatch.release();
        1652  +
        1653  +
            let probe = connection.probe();
        1654  +
            assert_eq!(
        1655  +
                Some(if classified_as_upgrade {
        1656  +
                    CloseReason::Upgraded
        1657  +
                } else {
        1658  +
                    CloseReason::ProtocolClosed
        1659  +
                }),
        1660  +
                probe.close_reason
        1661  +
            );
        1662  +
            assert!(!probe.awaiting_h1_exchange);
        1663  +
            assert_eq!(0, probe.in_flight);
        1664  +
            assert!(probe.physical_connection_complete);
        1665  +
            assert_eq!(1, origin.available_capacity_for_test());
        1666  +
        1667  +
            let stats = connection.stats.snapshot(0, 0, 0, 0, 0);
        1668  +
            assert_eq!(0, stats.h1().draining());
        1669  +
            assert_eq!(0, stats.h1().upgraded());
        1670  +
            assert_eq!(0, stats.physically_live_connections());
        1671  +
        });
        1672  +
    }
        1673  +
        1674  +
    /// Races `Opened`, accepted HTTP/1 completion, and both close transitions.
 1576   1675   
    ///
 1577   1676   
    /// Each installed event must be delivered once in
 1578   1677   
    /// `Opened -> LogicalClose -> PhysicalClose` order.
 1579   1678   
    #[test]
 1580   1679   
    fn installed_events_remain_ordered_across_concurrent_close() {
 1581         -
        loom::model(|| {
        1680  +
        let mut model = loom::model::Builder::new();
        1681  +
        model.preemption_bound.get_or_insert(3);
        1682  +
        model.check(|| {
 1582   1683   
            let event_sequence = Arc::new(AtomicUsize::new(0));
 1583   1684   
            let observed = event_sequence.clone();
 1584   1685   
            let events = ConnectionEvents::new(
 1585   1686   
                Some(SharedConnectionEventListener::new(
 1586   1687   
                    move |event: &ConnectionEvent<'_>| {
 1587   1688   
                        let code = match event {
 1588   1689   
                            ConnectionEvent::Opened(_) => 1,
 1589   1690   
                            ConnectionEvent::LogicalClose(_) => 2,
 1590   1691   
                            ConnectionEvent::PhysicalClose(_) => 3,
 1591   1692   
                            ConnectionEvent::EstablishmentFailed(_) => 4,
 1592   1693   
                        };
 1593   1694   
                        observed
 1594   1695   
                            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |sequence| {
 1595   1696   
                                Some(sequence * 4 + code)
 1596   1697   
                            })
 1597   1698   
                            .expect("event sequence update is infallible");
 1598   1699   
                    },
 1599   1700   
                )),
 1600   1701   
                SharedTimeSource::default(),
 1601   1702   
            );
 1602   1703   
            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
 1603   1704   
            let lease = OriginAdmission::lease_for_test(&origin);
 1604   1705   
            let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
        1706  +
            let dispatch = ConnectionState::try_commit_dispatch(&connection)
        1707  +
                .expect("open HTTP/1 connection rejected dispatch");
 1605   1708   
            let establishment = events.establishment_started(
 1606   1709   
                connection.info().origin(),
 1607   1710   
                connection.owner_partition(),
 1608   1711   
                connection.stats.clone(),
 1609   1712   
            );
 1610   1713   
 1611   1714   
            let opened_connection = connection.clone();
 1612   1715   
            let opened = loom::thread::spawn(move || establishment.opened(&opened_connection));
 1613   1716   
            let close_connection = connection.clone();
 1614   1717   
            let close = loom::thread::spawn(move || {
 1615         -
                close_connection.logical_close(CloseReason::PoolDropped)
        1718  +
                close_connection.logical_close(CloseReason::ProtocolClosed)
 1616   1719   
            });
 1617   1720   
            let complete_physical = loom::thread::spawn(move || physical.release());
        1721  +
            let complete_dispatch = loom::thread::spawn(move || dispatch.release());
 1618   1722   
 1619   1723   
            opened.join().unwrap();
 1620   1724   
            assert!(close.join().unwrap());
 1621   1725   
            complete_physical.join().unwrap();
        1726  +
            complete_dispatch.join().unwrap();
 1622   1727   
 1623   1728   
            assert_eq!(
 1624   1729   
                27,
 1625   1730   
                event_sequence.load(Ordering::SeqCst),
 1626   1731   
                "installed connection events were missing, duplicated, or reordered"
 1627   1732   
            );
 1628   1733   
            assert_eq!(1, origin.available_capacity_for_test());
 1629   1734   
            let probe = connection.probe();
 1630         -
            assert_eq!(Some(CloseReason::PoolDropped), probe.close_reason);
        1735  +
            assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
        1736  +
            assert!(!probe.awaiting_h1_exchange);
        1737  +
            assert_eq!(0, probe.in_flight);
 1631   1738   
            assert!(probe.physical_connection_complete);
 1632   1739   
        });
 1633   1740   
    }
 1634   1741   
}

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

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

@@ -176,176 +289,290 @@
  196    196   
        }
  197    197   
    };
  198    198   
    let info = ConnectionInfo::new(
  199    199   
        id,
  200    200   
        context.cell.id().origin().clone(),
  201    201   
        context.partition.id(),
  202    202   
        ConnectionProtocol::Http2,
  203    203   
        transport.metadata,
  204    204   
    );
  205    205   
    let (connection, physical) =
  206         -
        ConnectionState::pending_open(info, context.cell.connection_stats());
         206  +
        ConnectionState::pending_open(info, context.owner_spawner, context.cell.connection_stats());
  207    207   
    let io = ConnectionIo::new(transport.io, physical);
  208    208   
    let executor = PartitionExecutor {
  209         -
        spawner: context.owner_spawner.clone(),
         209  +
        spawner: connection.owner_spawner(),
  210    210   
    };
  211    211   
    establishment.protocol_handshake_started();
  212    212   
    let (sender, driver) = match hyper::client::conn::http2::Builder::new(executor)
  213    213   
        .handshake::<_, SdkBody>(io)
  214    214   
        .await
  215    215   
    {
  216    216   
        Ok(established) => established,
  217    217   
        Err(error) => {
  218    218   
            let error = downcast_error(Box::new(error));
  219    219   
            establishment.protocol_handshake_failed();
  220    220   
            connection.logical_close(CloseReason::ProtocolClosed);
  221    221   
            fail_flight_establishment(&mut completion, establishment, error);
  222    222   
            return;
  223    223   
        }
  224    224   
    };
  225    225   
    establishment.protocol_handshake_completed();
  226    226   
  227    227   
    if let Err(lease) = connection.open(permit.into_lease()) {
  228    228   
        drop(lease);
  229    229   
        connection.logical_close(CloseReason::ProtocolClosed);
  230    230   
        let error = ConnectorError::io(Box::new(std::io::Error::other(
  231    231   
            "HTTP/2 connection closed before installation",
  232    232   
        )));
  233    233   
        fail_flight_establishment(&mut completion, establishment, error);
  234    234   
        return;
  235    235   
    }
         236  +
    establishment.installed(&connection);
  236    237   
  237    238   
    let installed = OriginCell::complete_h2_flight(
  238    239   
        &context.cell,
  239    240   
        flight,
  240    241   
        connection.clone(),
  241    242   
        H2Sender::from_hyper(sender),
  242    243   
        context.cell.idle_deadline(),
  243    244   
    );
  244    245   
    let installed = match installed {
  245    246   
        Ok(installed) => installed,
  246    247   
        Err((_connection, _sender)) => {
  247    248   
            connection.logical_close(CloseReason::ProtocolClosed);
  248    249   
            let error = ConnectorError::io(Box::new(std::io::Error::other(
  249    250   
                "HTTP/2 flight became stale before installation",
  250    251   
            )));
  251    252   
            fail_flight_establishment(&mut completion, establishment, error);
  252    253   
            return;
  253    254   
        }
  254    255   
    };
  255    256   
  256    257   
    let generation = installed;
  257    258   
    let driver_guard = H2DriverGuard::new(H2CloseHandle::new(&context.cell, generation));
  258    259   
    let driver_info = connection.info().clone();
  259         -
    context.owner_spawner.spawn(Box::pin(async move {
         260  +
    connection.owner_spawner().spawn(Box::pin(async move {
  260    261   
        if let Err(error) = driver.await {
  261    262   
            tracing::debug!(
  262    263   
                connection_id = %driver_info.id(),
  263    264   
                connection_partition = ?driver_info.owner_partition(),
  264    265   
                origin_scheme = %driver_info.origin().scheme(),
  265    266   
                origin_host = driver_info.origin().host(),
  266    267   
                origin_port = ?driver_info.origin().port(),
  267    268   
                error = ?error,
  268    269   
                "HTTP/2 connection driver failed"
  269    270   
            );

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

@@ -148,148 +226,240 @@
  168    168   
/// measurements.
  169    169   
#[derive(Clone, Copy, Debug)]
  170    170   
#[non_exhaustive]
  171    171   
pub struct ConnectionEstablishmentStats {
  172    172   
    total_duration: Duration,
  173    173   
    transport_duration: Duration,
  174    174   
    protocol_handshake_duration: Option<Duration>,
  175    175   
}
  176    176   
  177    177   
impl ConnectionEstablishmentStats {
  178         -
    /// Returns elapsed time from establishment start through its terminal event.
         178  +
    /// Returns elapsed time through successful protocol installation or terminal failure.
         179  +
    ///
         180  +
    /// Successful measurements are frozen before the connection is published
         181  +
    /// to waiting pool demand.
  179    182   
    pub fn total_duration(&self) -> Duration {
  180    183   
        self.total_duration
  181    184   
    }
  182    185   
  183    186   
    /// Returns elapsed time spent in the configured transport connector.
  184    187   
    ///
  185    188   
    /// This may include DNS, socket, proxy, and TLS work. Custom connectors may
  186    189   
    /// perform a different set of transport operations.
  187    190   
    pub fn transport_duration(&self) -> Duration {
  188    191   
        self.transport_duration
  189    192   
    }
  190    193   
  191    194   
    /// Returns elapsed time spent in the Hyper protocol handshake, when started.
  192    195   
    ///
  193    196   
    /// Failures before protocol handshake return `None`.
  194    197   
    pub fn protocol_handshake_duration(&self) -> Option<Duration> {
  195    198   
        self.protocol_handshake_duration
  196    199   
    }
         200  +
         201  +
    fn connection_metadata(
         202  +
        &self,
         203  +
    ) -> aws_smithy_runtime_api::client::connection::ConnectionEstablishmentMetadata {
         204  +
        let mut builder =
         205  +
            aws_smithy_runtime_api::client::connection::ConnectionEstablishmentMetadata::builder()
         206  +
                .total_duration(self.total_duration)
         207  +
                .transport_duration(self.transport_duration);
         208  +
        builder.set_protocol_handshake_duration(self.protocol_handshake_duration);
         209  +
        builder.build()
         210  +
    }
  197    211   
}
  198    212   
  199    213   
/// Stage that terminated one failed connection establishment.
  200    214   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
  201    215   
#[non_exhaustive]
  202    216   
pub enum ConnectionEstablishmentStage {
  203    217   
    /// Work performed by the configured transport connector.
  204    218   
    Transport,
  205    219   
    /// Selection of an HTTP protocol compatible with the request.
  206    220   
    ProtocolSelection,
@@ -241,255 +301,315 @@
  261    275   
    stats: &'a ConnectionEstablishmentStats,
  262    276   
    connection: &'a PoolArc<ConnectionInfo>,
  263    277   
}
  264    278   
  265    279   
impl<'a> ConnectionOpened<'a> {
  266    280   
    /// Returns the establishment that created this connection.
  267    281   
    pub fn establishment(&self) -> &'a ConnectionEstablishmentInfo {
  268    282   
        self.establishment
  269    283   
    }
  270    284   
  271         -
    /// Returns measurements from the successful establishment.
         285  +
    /// Returns measurements frozen after protocol installation and before pool publication.
  272    286   
    pub fn stats(&self) -> &'a ConnectionEstablishmentStats {
  273    287   
        self.stats
  274    288   
    }
  275    289   
  276    290   
    /// Returns immutable identity and transport facts for the connection.
  277    291   
    pub fn connection(&self) -> &'a PoolArc<ConnectionInfo> {
  278    292   
        self.connection
  279    293   
    }
  280    294   
}
  281    295   
@@ -335,349 +611,655 @@
  355    369   
    }
  356    370   
  357    371   
    /// Returns the final protocol and ownership close classification.
  358    372   
    pub fn reason(&self) -> CloseReason {
  359    373   
        self.reason
  360    374   
    }
  361    375   
}
  362    376   
  363    377   
/// Pool-owned connection event source.
  364    378   
///
  365         -
/// This owner retains callback delivery, lifecycle timing, and establishment
  366         -
/// identity allocation. Starting an establishment performs no timing or
  367         -
/// identity work when no listener is configured.
         379  +
/// This owner retains callback delivery, establishment timing, and public
         380  +
/// establishment identity allocation. Timing is collected for successful
         381  +
/// connection metadata even when callback delivery is disabled.
  368    382   
#[derive(Debug)]
  369    383   
pub(super) struct ConnectionEvents {
  370    384   
    listener: Option<SharedConnectionEventListener>,
  371    385   
    time_source: SharedTimeSource,
  372    386   
    next_establishment_id: AtomicU64,
  373    387   
}
  374    388   
  375    389   
impl ConnectionEvents {
  376    390   
    /// Creates the event source retained by one connection pool.
  377    391   
    pub(super) fn new(
  378    392   
        listener: Option<SharedConnectionEventListener>,
  379    393   
        time_source: SharedTimeSource,
  380    394   
    ) -> Self {
  381    395   
        Self {
  382    396   
            listener,
  383    397   
            time_source,
  384    398   
            next_establishment_id: AtomicU64::new(0),
  385    399   
        }
  386    400   
    }
  387    401   
  388         -
    /// Records the start of an establishment when observation is enabled.
         402  +
    /// Records the start of one connection establishment.
  389    403   
    pub(super) fn establishment_started(
  390    404   
        &self,
  391    405   
        origin: &OriginKey,
  392    406   
        partition: PartitionId,
  393    407   
        connection_stats: PoolArc<CellConnectionStats>,
  394    408   
    ) -> ConnectionEstablishment {
  395    409   
        connection_stats.establishment_started();
         410  +
        let timing = EstablishmentTiming {
         411  +
            time_source: self.time_source.clone(),
         412  +
            started_at: self.time_source.now(),
         413  +
            protocol_handshake_started_at: None,
         414  +
            transport_duration: None,
         415  +
            protocol_handshake_duration: None,
         416  +
        };
  396    417   
        let observation = self
  397    418   
            .listener
  398    419   
            .as_ref()
  399    420   
            .map(|listener| EstablishmentObservation {
  400    421   
                listener: listener.clone(),
  401    422   
                info: ConnectionEstablishmentInfo {
  402    423   
                    id: ConnectionEstablishmentId(
  403    424   
                        self.next_establishment_id.fetch_add(1, Ordering::Relaxed),
  404    425   
                    ),
  405    426   
                    origin: origin.clone(),
  406    427   
                    partition,
  407    428   
                },
  408         -
                time_source: self.time_source.clone(),
  409         -
                started_at: self.time_source.now(),
  410         -
                protocol_handshake_started_at: None,
  411         -
                transport_duration: None,
  412         -
                protocol_handshake_duration: None,
  413    429   
                stage: ConnectionEstablishmentStage::Transport,
  414    430   
                remote_addr: None,
  415    431   
                protocol: None,
  416    432   
            });
  417    433   
        ConnectionEstablishment {
         434  +
            timing,
  418    435   
            observation,
         436  +
            successful_stats: None,
  419    437   
            connection_stats: Some(connection_stats),
  420    438   
        }
  421    439   
    }
  422    440   
}
  423    441   
  424         -
/// Tracks observations collected during one connection establishment.
         442  +
/// Tracks measurements and optional callback state for one establishment.
  425    443   
pub(super) struct ConnectionEstablishment {
         444  +
    timing: EstablishmentTiming,
  426    445   
    observation: Option<EstablishmentObservation>,
         446  +
    successful_stats: Option<ConnectionEstablishmentStats>,
  427    447   
    connection_stats: Option<PoolArc<CellConnectionStats>>,
  428    448   
}
  429    449   
  430         -
/// Data collected while one observed establishment is active.
  431         -
struct EstablishmentObservation {
  432         -
    listener: SharedConnectionEventListener,
  433         -
    info: ConnectionEstablishmentInfo,
         450  +
/// Timing collected for every connection establishment.
         451  +
struct EstablishmentTiming {
  434    452   
    time_source: SharedTimeSource,
  435    453   
    started_at: SystemTime,
  436    454   
    protocol_handshake_started_at: Option<SystemTime>,
  437    455   
    transport_duration: Option<Duration>,
  438    456   
    protocol_handshake_duration: Option<Duration>,
         457  +
}
         458  +
         459  +
/// Callback state retained only when a listener is configured.
         460  +
struct EstablishmentObservation {
         461  +
    listener: SharedConnectionEventListener,
         462  +
    info: ConnectionEstablishmentInfo,
  439    463   
    stage: ConnectionEstablishmentStage,
  440    464   
    remote_addr: Option<SocketAddr>,
  441    465   
    protocol: Option<ConnectionProtocol>,
  442    466   
}
  443    467   
  444    468   
impl ConnectionEstablishment {
  445    469   
    /// Records completion of the transport stage.
  446    470   
    pub(super) fn transport_completed(&mut self, remote_addr: Option<SocketAddr>) {
         471  +
        self.timing.transport_duration = Some(self.timing.elapsed_since(self.timing.started_at));
  447    472   
        if let Some(observation) = &mut self.observation {
  448         -
            observation.transport_duration =
  449         -
                Some(observation.elapsed_since(observation.started_at));
  450    473   
            observation.remote_addr = remote_addr;
  451    474   
            observation.stage = ConnectionEstablishmentStage::ProtocolSelection;
  452    475   
        }
  453    476   
    }
  454    477   
  455    478   
    /// Records the protocol selected from connector metadata.
  456    479   
    pub(super) fn protocol_selected(&mut self, protocol: ConnectionProtocol) {
  457    480   
        if let Some(observation) = &mut self.observation {
  458    481   
            observation.protocol = Some(protocol);
  459    482   
        }
  460    483   
    }
  461    484   
  462    485   
    /// Records entry into Hyper's protocol handshake.
  463    486   
    pub(super) fn protocol_handshake_started(&mut self) {
         487  +
        self.timing.protocol_handshake_started_at = Some(self.timing.time_source.now());
  464    488   
        if let Some(observation) = &mut self.observation {
  465         -
            observation.protocol_handshake_started_at = Some(observation.time_source.now());
  466    489   
            observation.stage = ConnectionEstablishmentStage::ProtocolHandshake;
  467    490   
        }
  468    491   
    }
  469    492   
  470    493   
    /// Records a failed Hyper handshake before emitting its terminal event.
  471    494   
    pub(super) fn protocol_handshake_failed(&mut self) {
  472         -
        if let Some(observation) = &mut self.observation {
  473         -
            observation.finish_protocol_handshake();
  474         -
        }
         495  +
        self.timing.finish_protocol_handshake();
  475    496   
    }
  476    497   
  477    498   
    /// Records successful Hyper handshake before pool installation.
  478    499   
    pub(super) fn protocol_handshake_completed(&mut self) {
         500  +
        self.timing.finish_protocol_handshake();
  479    501   
        if let Some(observation) = &mut self.observation {
  480         -
            observation.finish_protocol_handshake();
  481    502   
            observation.stage = ConnectionEstablishmentStage::PoolInstallation;
  482    503   
        }
  483    504   
    }
  484    505   
         506  +
    /// Freezes successful measurements before the connection becomes visible.
         507  +
    pub(super) fn installed(&mut self, connection: &PoolArc<ConnectionState>) {
         508  +
        let stats = self.timing.stats();
         509  +
        connection
         510  +
            .info()
         511  +
            .set_establishment(stats.connection_metadata());
         512  +
        assert!(
         513  +
            self.successful_stats.replace(stats).is_none(),
         514  +
            "connection establishment was installed more than once"
         515  +
        );
         516  +
    }
         517  +
  485    518   
    /// Emits the terminal failure for this establishment.
  486    519   
    pub(super) fn failed(mut self, error: &ConnectorError) {
         520  +
        let stats = self.timing.stats();
  487    521   
        self.finish_connection_stats();
  488    522   
        let Some(observation) = self.observation.take() else {
  489    523   
            return;
  490    524   
        };
  491         -
        observation.notify_failure(error);
         525  +
        observation.notify_failure(error, &stats);
  492    526   
    }
  493    527   
  494    528   
    /// Reports successful installation and enables ordered close observations.
  495    529   
    pub(super) fn opened(mut self, connection: &PoolArc<ConnectionState>) {
         530  +
        let stats = self.successful_stats.take().unwrap_or_else(|| {
         531  +
            let stats = self.timing.stats();
         532  +
            connection
         533  +
                .info()
         534  +
                .set_establishment(stats.connection_metadata());
         535  +
            stats
         536  +
        });
  496    537   
        self.finish_connection_stats();
  497    538   
        let listener = self.observation.take().map(|observation| {
  498         -
            let stats = observation.stats();
  499    539   
            observation
  500    540   
                .listener
  501    541   
                .notify(&ConnectionEvent::Opened(ConnectionOpened {
  502    542   
                    establishment: &observation.info,
  503    543   
                    stats: &stats,
  504    544   
                    connection: connection.info(),
  505    545   
                }));
  506    546   
            observation.listener
  507    547   
        });
  508    548   
        connection.complete_opened_event(listener.as_ref());
  509    549   
    }
  510    550   
  511    551   
    /// Ends an establishment whose transport lost to existing HTTP/2 supply.
  512    552   
    pub(super) fn superseded(mut self) {
  513    553   
        self.finish_connection_stats();
  514    554   
        self.observation.take();
  515    555   
    }
  516    556   
  517    557   
    fn finish_connection_stats(&mut self) {
  518    558   
        if let Some(stats) = self.connection_stats.take() {
  519    559   
            stats.establishment_finished();
  520    560   
        }
  521    561   
    }
  522    562   
}
  523    563   
  524    564   
impl Drop for ConnectionEstablishment {
  525    565   
    fn drop(&mut self) {
         566  +
        let stats = self.successful_stats.unwrap_or_else(|| self.timing.stats());
  526    567   
        self.finish_connection_stats();
  527    568   
        let Some(observation) = self.observation.take() else {
  528    569   
            return;
  529    570   
        };
  530    571   
        let error = ConnectorError::io("connection establishment task was dropped".into());
  531         -
        observation.notify_failure(&error);
         572  +
        observation.notify_failure(&error, &stats);
  532    573   
    }
  533    574   
}
  534    575   
  535    576   
impl EstablishmentObservation {
  536         -
    fn notify_failure(&self, error: &ConnectorError) {
  537         -
        let stats = self.stats();
         577  +
    fn notify_failure(&self, error: &ConnectorError, stats: &ConnectionEstablishmentStats) {
  538    578   
        self.listener.notify(&ConnectionEvent::EstablishmentFailed(
  539    579   
            ConnectionEstablishmentFailed {
  540    580   
                establishment: &self.info,
  541         -
                stats: &stats,
         581  +
                stats,
  542    582   
                stage: self.stage,
  543    583   
                remote_addr: self.remote_addr,
  544    584   
                protocol: self.protocol,
  545    585   
                error,
  546    586   
            },
  547    587   
        ));
  548    588   
    }
         589  +
}
  549    590   
         591  +
impl EstablishmentTiming {
  550    592   
    fn stats(&self) -> ConnectionEstablishmentStats {
  551    593   
        let total_duration = self.elapsed_since(self.started_at);
  552    594   
        ConnectionEstablishmentStats {
  553    595   
            total_duration,
  554    596   
            transport_duration: self.transport_duration.unwrap_or(total_duration),
  555    597   
            protocol_handshake_duration: self.protocol_handshake_duration.or_else(|| {
  556    598   
                self.protocol_handshake_started_at
  557    599   
                    .map(|started_at| self.elapsed_since(started_at))
  558    600   
            }),
  559    601   
        }
  560    602   
    }
  561    603   
  562    604   
    fn finish_protocol_handshake(&mut self) {
  563    605   
        self.protocol_handshake_duration = self
  564    606   
            .protocol_handshake_started_at
  565    607   
            .take()
  566    608   
            .map(|started_at| self.elapsed_since(started_at));
  567    609   
    }
  568    610   
  569    611   
    fn elapsed_since(&self, started_at: SystemTime) -> Duration {
  570    612   
        self.time_source
  571    613   
            .now()
  572    614   
            .duration_since(started_at)
  573    615   
            .unwrap_or_default()
  574    616   
    }
  575    617   
}
  576    618   
  577    619   
#[cfg(all(test, not(smithy_http_client_loom)))]
  578    620   
mod tests {
  579    621   
    use super::*;
         622  +
    use aws_smithy_async::test_util::ManualTimeSource;
  580    623   
    use std::sync::atomic::AtomicUsize;
  581    624   
    use std::sync::Mutex;
         625  +
    use std::time::UNIX_EPOCH;
  582    626   
  583    627   
    fn origin() -> OriginKey {
  584    628   
        OriginKey::from_parts(http_1x::uri::Scheme::HTTPS, "example.com", None).unwrap()
  585    629   
    }
  586    630   
  587    631   
    fn events(listener: Option<SharedConnectionEventListener>) -> ConnectionEvents {
  588    632   
        ConnectionEvents::new(listener, SharedTimeSource::default())
  589    633   
    }
  590    634   
  591    635   
    fn establishment(events: &ConnectionEvents) -> ConnectionEstablishment {
@@ -675,719 +734,821 @@
  695    739   
            Some((
  696    740   
                ConnectionEstablishmentId(0),
  697    741   
                ConnectionEstablishmentStage::ProtocolHandshake,
  698    742   
                Some(ConnectionProtocol::Http2),
  699    743   
                true,
  700    744   
            )),
  701    745   
            *observed.lock().unwrap()
  702    746   
        );
  703    747   
    }
  704    748   
         749  +
    #[test]
         750  +
    fn establishment_stats_measure_recorded_phases() {
         751  +
        let time = ManualTimeSource::new(UNIX_EPOCH);
         752  +
        let observed = Arc::new(Mutex::new(None));
         753  +
        let events = ConnectionEvents::new(
         754  +
            Some(SharedConnectionEventListener::new({
         755  +
                let observed = observed.clone();
         756  +
                move |event: &ConnectionEvent<'_>| {
         757  +
                    let ConnectionEvent::EstablishmentFailed(failed) = event else {
         758  +
                        panic!("unexpected event: {event:?}");
         759  +
                    };
         760  +
                    *observed.lock().unwrap() = Some(*failed.stats());
         761  +
                }
         762  +
            })),
         763  +
            SharedTimeSource::new(time.clone()),
         764  +
        );
         765  +
        let mut establishment = establishment(&events);
         766  +
         767  +
        time.advance(Duration::from_secs(2));
         768  +
        establishment.transport_completed(None);
         769  +
        establishment.protocol_selected(ConnectionProtocol::Http2);
         770  +
        establishment.protocol_handshake_started();
         771  +
        time.advance(Duration::from_secs(3));
         772  +
        establishment.protocol_handshake_completed();
         773  +
        time.advance(Duration::from_secs(2));
         774  +
        establishment.failed(&ConnectorError::io("synthetic installation failure".into()));
         775  +
         776  +
        let stats = observed.lock().unwrap().expect("establishment stats");
         777  +
        assert_eq!(stats.total_duration(), Duration::from_secs(7));
         778  +
        assert_eq!(stats.transport_duration(), Duration::from_secs(2));
         779  +
        assert_eq!(
         780  +
            stats.protocol_handshake_duration(),
         781  +
            Some(Duration::from_secs(3))
         782  +
        );
         783  +
        let metadata = stats.connection_metadata();
         784  +
        assert_eq!(metadata.total_duration(), Duration::from_secs(7));
         785  +
        assert_eq!(metadata.transport_duration(), Duration::from_secs(2));
         786  +
        assert_eq!(
         787  +
            metadata.protocol_handshake_duration(),
         788  +
            Some(Duration::from_secs(3))
         789  +
        );
         790  +
    }
         791  +
  705    792   
    #[test]
  706    793   
    fn superseded_establishment_emits_no_public_event() {
  707    794   
        let observed = Arc::new(AtomicUsize::new(0));
  708    795   
        let events = events(Some(SharedConnectionEventListener::new({
  709    796   
            let observed = observed.clone();
  710    797   
            move |_: &ConnectionEvent<'_>| {
  711    798   
                observed.fetch_add(1, Ordering::Relaxed);
  712    799   
            }
  713    800   
        })));
  714    801   

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

@@ -39,39 +104,98 @@
   59     59   
    }
   60     60   
   61     61   
    /// Returns the deadline for a sender becoming idle now.
   62     62   
    pub(super) fn idle_deadline(&self) -> Option<SystemTime> {
   63     63   
        self.idle_timeout
   64     64   
            .and_then(|timeout| self.time_source.now().checked_add(timeout))
   65     65   
    }
   66     66   
   67     67   
    /// Registers a retained cell without waking an idle task.
   68     68   
    pub(super) fn register(&self, cell: &Arc<OriginCell>) {
   69         -
        // Loom has no modeled Weak. Skip this production-only index to avoid a
   70         -
        // model-only maintenance -> cell -> maintenance cycle.
   71         -
        #[cfg(all(test, smithy_http_client_loom))]
   72         -
        let _ = cell;
   73         -
   74         -
        #[cfg(not(all(test, smithy_http_client_loom)))]
   75     69   
        self.state
   76     70   
            .lock()
   77     71   
            .cells
   78     72   
            .insert(cell.id().clone(), Weak::from_arc(cell));
   79     73   
    }
   80     74   
   81     75   
    /// Publishes newly idle work when it precedes the current wakeup.
   82     76   
    pub(super) fn notify_deadline(&self, deadline: Option<SystemTime>) {
   83     77   
        let Some(deadline) = deadline else {
   84     78   
            return;
@@ -164,158 +223,225 @@
  184    178   
        if state
  185    179   
            .waker
  186    180   
            .as_ref()
  187    181   
            .is_none_or(|registered| !registered.will_wake(cx.waker()))
  188    182   
        {
  189    183   
            state.waker = Some(cx.waker().clone());
  190    184   
        }
  191    185   
        Poll::Pending
  192    186   
    }
  193    187   
         188  +
    #[cfg(all(test, smithy_http_client_loom))]
         189  +
    pub(super) fn clear_modeled_cells_for_test(&self) {
         190  +
        // Loom has no modeled Weak, so its synchronization facade retains
         191  +
        // registered cells strongly. Explicit teardown prevents that model-only
         192  +
        // substitution from creating a maintenance -> cell -> maintenance cycle.
         193  +
        self.state.lock().cells.clear();
         194  +
    }
         195  +
  194    196   
    #[cfg(all(test, feature = "rt-tokio", not(smithy_http_client_loom)))]
  195    197   
    fn probe(&self) -> MaintenanceProbe {
  196    198   
        let state = self.state.lock();
  197    199   
        MaintenanceProbe {
  198    200   
            started: self.started.load(Ordering::Acquire),
  199    201   
            shutdown: state.shutdown,
  200    202   
            scheduled_deadline: state.scheduled_deadline,
  201    203   
        }
  202    204   
    }
  203    205   
}
@@ -360,362 +511,599 @@
  380    382   
#[cfg(all(test, feature = "rt-tokio", not(smithy_http_client_loom)))]
  381    383   
struct MaintenanceProbe {
  382    384   
    started: bool,
  383    385   
    shutdown: bool,
  384    386   
    scheduled_deadline: Option<SystemTime>,
  385    387   
}
  386    388   
  387    389   
#[cfg(all(test, smithy_http_client_loom))]
  388    390   
mod loom_tests {
  389    391   
    use super::*;
  390         -
    use std::sync::atomic::{AtomicUsize, Ordering as StdOrdering};
         392  +
    use crate::client::pool::cell::h1::{H1CloseHandle, H1Sender};
         393  +
    use crate::client::pool::connection::{CloseReason, ConnectionInfo, ConnectionState};
         394  +
    use crate::client::pool::origin::OriginKey;
         395  +
    use crate::client::pool::partition::{EligibilityGroup, PartitionId};
         396  +
    use crate::sync::AtomicUsize;
         397  +
    use aws_smithy_async::test_util::ManualTimeSource;
         398  +
    use aws_smithy_runtime_api::client::connection::ConnectionId;
         399  +
    use http_1x::uri::Scheme;
  391    400   
    use std::sync::Arc as StdArc;
  392    401   
    use std::task::{Context, Wake, Waker};
  393    402   
    use std::time::UNIX_EPOCH;
  394    403   
  395    404   
    struct WakeCounter(AtomicUsize);
  396    405   
  397    406   
    impl Wake for WakeCounter {
  398    407   
        fn wake(self: StdArc<Self>) {
  399         -
            self.0.fetch_add(1, StdOrdering::SeqCst);
         408  +
            self.0.fetch_add(1, Ordering::SeqCst);
  400    409   
        }
  401    410   
  402    411   
        fn wake_by_ref(self: &StdArc<Self>) {
  403         -
            self.0.fetch_add(1, StdOrdering::SeqCst);
         412  +
            self.0.fetch_add(1, Ordering::SeqCst);
  404    413   
        }
  405    414   
    }
  406    415   
         416  +
    /// Races deadline publication with registration of the maintenance waker.
         417  +
    ///
         418  +
    /// A task that observes the old revision must either retry immediately or
         419  +
    /// receive exactly one wake after it parks.
  407    420   
    #[test]
  408    421   
    fn deadline_publication_wakes_a_registered_task() {
  409    422   
        loom::model(|| {
  410    423   
            let maintenance = PartitionMaintenance::new(MaintenanceConfig::default());
  411    424   
            let observed = maintenance
  412    425   
                .begin_scan()
  413    426   
                .expect("new maintenance scheduler was shut down");
  414    427   
            let counter = StdArc::new(WakeCounter(AtomicUsize::new(0)));
  415    428   
            let waker = Waker::from(counter.clone());
  416    429   
            let mut context = Context::from_waker(&waker);
  417    430   
            let publisher = maintenance.clone();
  418    431   
            let publish = loom::thread::spawn(move || {
  419    432   
                publisher.notify_deadline(Some(UNIX_EPOCH + Duration::from_secs(1)));
  420    433   
            });
  421    434   
  422    435   
            let before_join = maintenance.poll_revision(observed, &mut context);
  423    436   
            publish.join().unwrap();
  424    437   
            if before_join.is_pending() {
  425         -
                assert_eq!(1, counter.0.load(StdOrdering::SeqCst));
         438  +
                assert_eq!(1, counter.0.load(Ordering::SeqCst));
  426    439   
            }
  427    440   
            assert!(maintenance.poll_revision(observed, &mut context).is_ready());
  428    441   
        });
  429    442   
    }
  430    443   
         444  +
    /// Publishes an earlier maintenance deadline while a scan is being committed.
         445  +
    ///
         446  +
    /// The scan must retry or retain a revision that forces the next poll to
         447  +
    /// observe the concurrent publication.
  431    448   
    #[test]
  432    449   
    fn deadline_published_during_a_scan_forces_retry_or_wake() {
  433    450   
        loom::model(|| {
  434    451   
            let maintenance = PartitionMaintenance::new(MaintenanceConfig::default());
  435    452   
            let elapsed = UNIX_EPOCH + Duration::from_secs(1);
  436    453   
            let later = UNIX_EPOCH + Duration::from_secs(2);
  437    454   
            maintenance.notify_deadline(Some(elapsed));
  438    455   
            let observed = maintenance
  439    456   
                .begin_scan()
  440    457   
                .expect("new maintenance scheduler was shut down");
  441    458   
            assert_eq!(None, maintenance.state.lock().scheduled_deadline);
  442    459   
  443    460   
            let publisher = maintenance.clone();
  444    461   
            let publish = loom::thread::spawn(move || publisher.notify_deadline(Some(later)));
  445    462   
            let result = maintenance.schedule(observed, None);
  446    463   
            publish.join().unwrap();
  447    464   
  448    465   
            match result {
  449    466   
                ScheduleResult::Retry => {}
  450    467   
                ScheduleResult::Wait { revision, deadline } => {
  451    468   
                    assert_eq!(None, deadline);
  452    469   
                    let state = maintenance.state.lock();
  453    470   
                    assert_ne!(revision, state.revision);
  454    471   
                    assert_eq!(Some(later), state.scheduled_deadline);
  455    472   
                }
  456    473   
                ScheduleResult::Shutdown => panic!("maintenance unexpectedly shut down"),
  457    474   
            }
  458    475   
        });
  459    476   
    }
  460    477   
         478  +
    /// Races shutdown with registration of the maintenance waker.
         479  +
    ///
         480  +
    /// A task that parks before shutdown must be woken and all later polls must
         481  +
    /// observe terminal shutdown.
  461    482   
    #[test]
  462    483   
    fn shutdown_wakes_a_registered_task() {
  463    484   
        loom::model(|| {
  464    485   
            let maintenance = PartitionMaintenance::new(MaintenanceConfig::default());
  465    486   
            let observed = maintenance
  466    487   
                .begin_scan()
  467    488   
                .expect("new maintenance scheduler was shut down");
  468    489   
            let counter = StdArc::new(WakeCounter(AtomicUsize::new(0)));
  469    490   
            let waker = Waker::from(counter.clone());
  470    491   
            let mut context = Context::from_waker(&waker);
  471    492   
            let shutdown = maintenance.clone();
  472    493   
            let stop = loom::thread::spawn(move || shutdown.shutdown());
  473    494   
  474    495   
            let before_join = maintenance.poll_revision(observed, &mut context);
  475    496   
            stop.join().unwrap();
  476    497   
            if before_join.is_pending() {
  477         -
                assert_eq!(1, counter.0.load(StdOrdering::SeqCst));
         498  +
                assert_eq!(1, counter.0.load(Ordering::SeqCst));
  478    499   
            }
  479    500   
            assert!(maintenance.poll_revision(observed, &mut context).is_ready());
  480    501   
        });
  481    502   
    }
         503  +
         504  +
    /// Races an expired idle HTTP/1 record with local sender selection.
         505  +
    ///
         506  +
    /// Selection either takes the sender before the scan observes it or expiry
         507  +
    /// closes the exact record. The sender and record must have one final owner.
         508  +
    #[test]
         509  +
    fn idle_expiry_linearizes_against_h1_selection() {
         510  +
        loom::model(|| {
         511  +
            let maintenance = PartitionMaintenance::new(MaintenanceConfig {
         512  +
                idle_timeout: Some(Duration::ZERO),
         513  +
                time_source: SharedTimeSource::new(ManualTimeSource::new(UNIX_EPOCH)),
         514  +
                sleep: None,
         515  +
            });
         516  +
            let cell = Arc::new(OriginCell::new(
         517  +
                PartitionId::from_index(1),
         518  +
                OriginKey::from_parts(Scheme::HTTP, "example.com", None).unwrap(),
         519  +
                EligibilityGroup::Pool,
         520  +
                None,
         521  +
                Some(maintenance.clone()),
         522  +
            ));
         523  +
            maintenance.register(&cell);
         524  +
            let (connection, physical) = ConnectionState::unbounded(ConnectionInfo::for_test(
         525  +
                ConnectionId::new(1),
         526  +
                cell.id().partition(),
         527  +
            ));
         528  +
            OriginCell::insert_idle_h1(&cell, connection.clone(), H1Sender::test(1));
         529  +
         530  +
            let selecting_cell = cell.clone();
         531  +
            let selecting = loom::thread::spawn(move || OriginCell::select_h1(&selecting_cell));
         532  +
            let scanning = maintenance.clone();
         533  +
            let expiring = loom::thread::spawn(move || {
         534  +
                let observed = scanning
         535  +
                    .begin_scan()
         536  +
                    .expect("maintenance shut down before its scan");
         537  +
                let cells = scanning.cells();
         538  +
                for cell in &cells {
         539  +
                    OriginCell::expire_idle(cell, UNIX_EPOCH);
         540  +
                }
         541  +
                let nearest = cells
         542  +
                    .iter()
         543  +
                    .filter_map(|cell| cell.nearest_idle_deadline())
         544  +
                    .min();
         545  +
                drop(cells);
         546  +
                scanning.schedule(observed, nearest)
         547  +
            });
         548  +
         549  +
            let selected = selecting.join().unwrap();
         550  +
            assert!(matches!(
         551  +
                expiring.join().unwrap(),
         552  +
                ScheduleResult::Wait { .. }
         553  +
            ));
         554  +
            drop(selected);
         555  +
         556  +
            if connection.probe().close_reason.is_none() {
         557  +
                assert!(H1CloseHandle::new(&cell, &connection).close(CloseReason::PoolDropped));
         558  +
            }
         559  +
            let stats = cell.connection_stats_snapshot();
         560  +
            assert_eq!(0, stats.h1().idle());
         561  +
            assert_eq!(0, stats.h1().active());
         562  +
            assert!(matches!(
         563  +
                connection.probe().close_reason,
         564  +
                Some(CloseReason::IdleTimeout | CloseReason::PoolDropped)
         565  +
            ));
         566  +
            drop(physical);
         567  +
            maintenance.clear_modeled_cells_for_test();
         568  +
        });
         569  +
    }
  482    570   
}
  483    571   
  484    572   
#[cfg(all(test, feature = "rt-tokio", not(smithy_http_client_loom)))]
  485    573   
mod tests {
  486    574   
    use super::*;
  487    575   
    use crate::client::pool::cell::h1::H1Sender;
  488    576   
    use crate::client::pool::connection::{ConnectionInfo, ConnectionProtocol, ConnectionState};
  489    577   
    use crate::client::pool::origin::OriginKey;
  490    578   
    use crate::client::pool::partition::{EligibilityGroup, PartitionId, TokioDriverSpawner};
  491    579   
    use aws_smithy_async::test_util::controlled_time_and_sleep;