AWS SDK

AWS SDK

rev. 7a81c738582b03212f62160d093a63f8547fc0f5 (ignoring whitespace)

Files changed:

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

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

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

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

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

@@ -1,1 +155,0 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! HTTP/1 connection establishment.
    7      7   
//!
    8         -
//! Establishment connects a transport, performs Hyper's client handshake,
    9         -
//! installs the resulting exclusive sender in the origin cell, and submits
   10         -
//! the protocol driver to the connection-owning partition.
           8  +
//! Establishment receives an already connected transport, performs Hyper's
           9  +
//! client handshake, installs the resulting exclusive sender in the origin
          10  +
//! cell, and submits the protocol driver to the connection-owning partition.
   11     11   
   12     12   
use super::super::cell::h1::{H1CloseHandle, H1DriverGuard, H1Selection, H1Sender};
   13     13   
use super::super::cell::{EstablishmentPermit, OriginCell};
   14     14   
use super::super::connection::{
   15     15   
    CloseReason, ConnectionInfo, ConnectionIo, ConnectionState, NegotiatedProtocol,
   16     16   
};
   17     17   
use super::super::dispatch::AcquisitionContext;
   18     18   
use super::next_connection_id;
          19  +
use crate::client::connect::BoxConn;
   19     20   
use aws_smithy_runtime_api::client::result::ConnectorError;
   20     21   
use aws_smithy_types::body::SdkBody;
   21         -
use std::error::Error;
   22         -
use std::fmt;
          22  +
use hyper_util::client::legacy::connect::Connected;
   23     23   
   24         -
/// Connects, handshakes, installs, and starts the owner-partition driver.
          24  +
/// Handshakes, installs, and starts the owner-partition driver.
   25     25   
///
   26     26   
/// `permit` remains the sole owner of optional bounded capacity until the
   27     27   
/// connection state is created. An earlier failure returns that capacity
   28     28   
/// through the permit's fallback. A handshake failure logically closes the
   29     29   
/// new connection state, while success transfers driver ownership to the
   30     30   
/// partition runtime and returns the installed sender selection.
   31     31   
pub(in crate::client::pool) async fn establish_h1(
   32     32   
    context: AcquisitionContext,
   33     33   
    permit: EstablishmentPermit,
          34  +
    io: BoxConn,
          35  +
    connected: Connected,
   34     36   
) -> Result<H1Selection, ConnectorError> {
   35     37   
    let request_partition = context.partition.id();
   36     38   
    let connection_cell = context.cell.clone();
   37     39   
    tracing::debug!(
   38     40   
        request_partition = ?request_partition,
   39     41   
        connection_partition = ?connection_cell.id().partition(),
   40     42   
        origin_scheme = %connection_cell.id().origin().scheme(),
   41     43   
        origin_host = connection_cell.id().origin().host(),
   42     44   
        origin_port = ?connection_cell.id().origin().port(),
   43     45   
        "HTTP/1 connection establishment started"
   44     46   
    );
   45     47   
   46         -
    let result = connect_handshake_and_install_h1(context, permit).await;
          48  +
    let result = handshake_and_install_h1(context, permit, io, connected).await;
   47     49   
    match &result {
   48     50   
        Ok(selection) => {
   49     51   
            let connection = selection.connection();
   50     52   
            tracing::debug!(
   51     53   
                connection_id = %connection.id(),
   52     54   
                request_partition = ?request_partition,
   53     55   
                connection_partition = ?connection.owner_partition(),
   54     56   
                origin_scheme = %connection.info().origin().scheme(),
   55     57   
                origin_host = connection.info().origin().host(),
   56     58   
                origin_port = ?connection.info().origin().port(),
   57     59   
                "HTTP/1 connection established"
   58     60   
            );
   59     61   
        }
   60     62   
        Err(error) => tracing::debug!(
   61     63   
            request_partition = ?request_partition,
   62     64   
            connection_partition = ?connection_cell.id().partition(),
   63     65   
            origin_scheme = %connection_cell.id().origin().scheme(),
   64     66   
            origin_host = connection_cell.id().origin().host(),
   65     67   
            origin_port = ?connection_cell.id().origin().port(),
   66     68   
            error = ?error,
   67     69   
            "HTTP/1 connection establishment failed"
   68     70   
        ),
   69     71   
    }
   70     72   
    result
   71     73   
}
   72     74   
   73         -
/// Connects, handshakes, and installs one HTTP/1 connection.
   74         -
async fn connect_handshake_and_install_h1(
          75  +
/// Handshakes and installs one already connected HTTP/1 transport.
          76  +
async fn handshake_and_install_h1(
   75     77   
    context: AcquisitionContext,
   76     78   
    permit: EstablishmentPermit,
          79  +
    io: BoxConn,
          80  +
    connected: Connected,
   77     81   
) -> Result<H1Selection, ConnectorError> {
   78     82   
    let AcquisitionContext {
   79     83   
        pool,
   80     84   
        partition,
   81     85   
        cell,
   82         -
        absolute_uri,
          86  +
        absolute_uri: _,
   83     87   
        owner_spawner,
   84         -
        connect_timeout,
          88  +
        connect_timeout: _,
   85     89   
    } = context;
   86     90   
   87         -
    let io = pool
   88         -
        .transport
   89         -
        .connect(&partition, absolute_uri, connect_timeout)
   90         -
        .await
   91         -
        .map_err(super::super::super::downcast_error)?;
   92         -
    let connected = io.connected();
   93         -
    if connected.is_negotiated_h2() {
   94         -
        return Err(ConnectorError::user(UnsupportedNegotiatedProtocol.into()));
   95         -
    }
   96         -
   97     91   
    let id =
   98     92   
        next_connection_id(&pool).map_err(|error| ConnectorError::other(error.into(), None))?;
   99     93   
    let info = ConnectionInfo::new(
  100     94   
        id,
  101     95   
        cell.id().origin().clone(),
  102     96   
        partition.id(),
  103     97   
        NegotiatedProtocol::Http1,
  104     98   
        connected,
  105     99   
    );
  106         -
    let (connection, physical) = match permit.into_lease() {
  107         -
        Some(lease) => ConnectionState::bounded(info, lease),
  108         -
        None => ConnectionState::unbounded(info),
  109         -
    };
         100  +
    let (connection, physical) = ConnectionState::pending_open(info);
  110    101   
    let io = ConnectionIo::new(io, physical);
  111    102   
  112    103   
    let (sender, driver) = match hyper::client::conn::http1::Builder::new()
  113    104   
        .handshake::<_, SdkBody>(io)
  114    105   
        .await
  115    106   
    {
  116    107   
        Ok(established) => established,
  117    108   
        Err(error) => {
  118    109   
            connection.logical_close(CloseReason::ProtocolClosed);
  119    110   
            return Err(super::super::super::downcast_error(Box::new(error)));
  120    111   
        }
  121    112   
    };
  122    113   
         114  +
    if let Err(lease) = connection.open(permit.into_lease()) {
         115  +
        drop(lease);
         116  +
        connection.logical_close(CloseReason::ProtocolClosed);
         117  +
        return Err(ConnectorError::io(
         118  +
            "HTTP/1 connection closed before installation".into(),
         119  +
        ));
         120  +
    }
         121  +
  123    122   
    let selection =
  124    123   
        OriginCell::install_selected_h1(&cell, connection.clone(), H1Sender::from_hyper(sender));
  125    124   
    let driver_guard = H1DriverGuard::new(H1CloseHandle::new(&cell, &connection));
  126    125   
    let driver_info = connection.info().clone();
  127    126   
    owner_spawner.spawn(Box::pin(async move {
  128    127   
        let result = driver.with_upgrades().await;
  129    128   
        if let Err(error) = result {
  130    129   
            tracing::debug!(
  131    130   
                connection_id = %driver_info.id(),
  132    131   
                connection_partition = ?driver_info.owner_partition(),
  133    132   
                origin_scheme = %driver_info.origin().scheme(),
  134    133   
                origin_host = driver_info.origin().host(),
  135    134   
                origin_port = ?driver_info.origin().port(),
  136    135   
                error = ?error,
  137    136   
                "HTTP/1 connection driver failed"
  138    137   
            );
  139    138   
        }
  140    139   
        driver_guard.protocol_closed();
  141    140   
    }));
  142    141   
    Ok(selection)
  143    142   
}
  144         -
  145         -
/// The cleartext HTTP/1 path rejected a connector-selected HTTP/2 transport.
  146         -
#[derive(Debug)]
  147         -
struct UnsupportedNegotiatedProtocol;
  148         -
  149         -
impl fmt::Display for UnsupportedNegotiatedProtocol {
  150         -
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  151         -
        f.write_str("the HTTP/1 pool path cannot install an HTTP/2 transport")
  152         -
    }
  153         -
}
  154         -
  155         -
impl Error for UnsupportedNegotiatedProtocol {}

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

@@ -0,1 +0,475 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
//! HTTP/2 flight convergence, handshake, and driver installation.
           7  +
//!
           8  +
//! ALPN-selected attempts first enter one cell-local transition: use an
           9  +
//! accepting generation, join the current flight, or become the flight's
          10  +
//! owner task. Only the owner performs Hyper's HTTP/2 handshake. It opens the
          11  +
//! connection, installs the generation, submits the driver on the partition
          12  +
//! spawner, and then disarms flight cleanup. Participants own only their
          13  +
//! waiter entries; losing attempts close their unused transport and return
          14  +
//! their capacity before this function reports transfer. The connector timeout
          15  +
//! ends before this phase; no separate pool timeout covers the HTTP/2
          16  +
//! handshake or driver submission.
          17  +
          18  +
use super::super::cell::h2::{
          19  +
    H2CloseHandle, H2DriverGuard, H2FlightId, H2FlightInstall, H2GenerationJoin, H2Sender,
          20  +
};
          21  +
use super::super::cell::{AcquisitionResult, EstablishmentPermit, OriginCell, WaiterId};
          22  +
use super::super::connection::{
          23  +
    CloseReason, ConnectionInfo, ConnectionIo, ConnectionState, NegotiatedProtocol,
          24  +
};
          25  +
use super::super::dispatch::AcquisitionContext;
          26  +
use super::super::partition::DriverSpawner;
          27  +
use super::{next_connection_id, EstablishmentOutcome};
          28  +
use crate::client::connect::BoxConn;
          29  +
use aws_smithy_runtime_api::box_error::BoxError;
          30  +
use aws_smithy_runtime_api::client::result::ConnectorError;
          31  +
use aws_smithy_types::body::SdkBody;
          32  +
use aws_smithy_types::retry::ErrorKind;
          33  +
use hyper::rt::Executor;
          34  +
use hyper_util::client::legacy::connect::Connected;
          35  +
use std::future::Future;
          36  +
use std::sync::Arc as StdArc;
          37  +
          38  +
/// Hyper executor that keeps spawned HTTP/2 work on the connection partition.
          39  +
#[derive(Clone, Debug)]
          40  +
struct PartitionExecutor {
          41  +
    /// Runtime placement inherited from the connection-owning partition.
          42  +
    spawner: StdArc<dyn DriverSpawner>,
          43  +
}
          44  +
          45  +
impl<F> Executor<F> for PartitionExecutor
          46  +
where
          47  +
    F: Future<Output = ()> + Send + 'static,
          48  +
{
          49  +
    fn execute(&self, future: F) {
          50  +
        self.spawner.spawn(Box::pin(future));
          51  +
    }
          52  +
}
          53  +
          54  +
/// Settles a flight if its owner-runtime task is discarded.
          55  +
struct FlightCompletionGuard {
          56  +
    /// Cell that owns the exact flight.
          57  +
    cell: crate::sync::Arc<OriginCell>,
          58  +
    /// Flight identity protected from stale task completion.
          59  +
    flight: H2FlightId,
          60  +
    /// Whether drop must fail remaining participants.
          61  +
    active: bool,
          62  +
}
          63  +
          64  +
impl FlightCompletionGuard {
          65  +
    fn new(cell: crate::sync::Arc<OriginCell>, flight: H2FlightId) -> Self {
          66  +
        Self {
          67  +
            cell,
          68  +
            flight,
          69  +
            active: true,
          70  +
        }
          71  +
    }
          72  +
          73  +
    fn disarm(&mut self) {
          74  +
        self.active = false;
          75  +
    }
          76  +
          77  +
    fn fail(&mut self, error: ConnectorError) {
          78  +
        self.active = false;
          79  +
        fail_participants(&self.cell, self.flight, error);
          80  +
    }
          81  +
}
          82  +
          83  +
impl Drop for FlightCompletionGuard {
          84  +
    fn drop(&mut self) {
          85  +
        if self.active {
          86  +
            fail_participants(
          87  +
                &self.cell,
          88  +
                self.flight,
          89  +
                ConnectorError::io(Box::new(std::io::Error::other(
          90  +
                    "HTTP/2 establishment task was dropped",
          91  +
                ))),
          92  +
            );
          93  +
        }
          94  +
    }
          95  +
}
          96  +
          97  +
/// Converges after ALPN and performs at most one HTTP/2 handshake per cell.
          98  +
pub(super) async fn establish_h2(
          99  +
    context: AcquisitionContext,
         100  +
    waiter: WaiterId,
         101  +
    permit: EstablishmentPermit,
         102  +
    io: BoxConn,
         103  +
    connected: Connected,
         104  +
) -> EstablishmentOutcome {
         105  +
    loop {
         106  +
        match context.cell.install_or_join_h2_flight(waiter) {
         107  +
            H2FlightInstall::Accepting(generation) => {
         108  +
                tracing::trace!(
         109  +
                    request_partition = ?context.partition.id(),
         110  +
                    connection_partition = ?context.cell.id().partition(),
         111  +
                    origin_scheme = %context.cell.id().origin().scheme(),
         112  +
                    origin_host = context.cell.id().origin().host(),
         113  +
                    origin_port = ?context.cell.id().origin().port(),
         114  +
                    h2_generation = ?generation,
         115  +
                    "HTTP/2 establishment found an accepting generation"
         116  +
                );
         117  +
                match OriginCell::join_h2_generation(&context.cell, waiter, generation) {
         118  +
                    H2GenerationJoin::GenerationChanged => continue,
         119  +
                    H2GenerationJoin::Joined | H2GenerationJoin::WaiterCompleted => {
         120  +
                        drop(io);
         121  +
                        drop(permit);
         122  +
                        return EstablishmentOutcome::Transferred;
         123  +
                    }
         124  +
                }
         125  +
            }
         126  +
            H2FlightInstall::Joined => {
         127  +
                tracing::trace!(
         128  +
                    request_partition = ?context.partition.id(),
         129  +
                    connection_partition = ?context.cell.id().partition(),
         130  +
                    origin_scheme = %context.cell.id().origin().scheme(),
         131  +
                    origin_host = context.cell.id().origin().host(),
         132  +
                    origin_port = ?context.cell.id().origin().port(),
         133  +
                    "HTTP/2 establishment joined the active flight"
         134  +
                );
         135  +
                drop(io);
         136  +
                drop(permit);
         137  +
                return EstablishmentOutcome::Transferred;
         138  +
            }
         139  +
            H2FlightInstall::WaiterCompleted => {
         140  +
                tracing::trace!(
         141  +
                    request_partition = ?context.partition.id(),
         142  +
                    connection_partition = ?context.cell.id().partition(),
         143  +
                    origin_scheme = %context.cell.id().origin().scheme(),
         144  +
                    origin_host = context.cell.id().origin().host(),
         145  +
                    origin_port = ?context.cell.id().origin().port(),
         146  +
                    "HTTP/2 establishment waiter already completed"
         147  +
                );
         148  +
                drop(io);
         149  +
                drop(permit);
         150  +
                return EstablishmentOutcome::Transferred;
         151  +
            }
         152  +
            H2FlightInstall::Driver(flight) => {
         153  +
                tracing::trace!(
         154  +
                    request_partition = ?context.partition.id(),
         155  +
                    connection_partition = ?context.cell.id().partition(),
         156  +
                    origin_scheme = %context.cell.id().origin().scheme(),
         157  +
                    origin_host = context.cell.id().origin().host(),
         158  +
                    origin_port = ?context.cell.id().origin().port(),
         159  +
                    h2_flight = ?flight,
         160  +
                    "HTTP/2 establishment started a flight"
         161  +
                );
         162  +
                drive_flight(context, flight, permit, io, connected).await;
         163  +
                return EstablishmentOutcome::Transferred;
         164  +
            }
         165  +
        }
         166  +
    }
         167  +
}
         168  +
         169  +
/// Handshakes, installs, and publishes one winning flight.
         170  +
async fn drive_flight(
         171  +
    context: AcquisitionContext,
         172  +
    flight: H2FlightId,
         173  +
    permit: EstablishmentPermit,
         174  +
    io: BoxConn,
         175  +
    connected: Connected,
         176  +
) {
         177  +
    let mut completion = FlightCompletionGuard::new(context.cell.clone(), flight);
         178  +
    let id = match next_connection_id(&context.pool) {
         179  +
        Ok(id) => id,
         180  +
        Err(error) => {
         181  +
            completion.fail(ConnectorError::other(Box::new(error), None));
         182  +
            return;
         183  +
        }
         184  +
    };
         185  +
    let info = ConnectionInfo::new(
         186  +
        id,
         187  +
        context.cell.id().origin().clone(),
         188  +
        context.partition.id(),
         189  +
        NegotiatedProtocol::Http2,
         190  +
        connected,
         191  +
    );
         192  +
    let (connection, physical) = ConnectionState::pending_open(info);
         193  +
    let io = ConnectionIo::new(io, physical);
         194  +
    let executor = PartitionExecutor {
         195  +
        spawner: context.owner_spawner.clone(),
         196  +
    };
         197  +
    let (sender, driver) = match hyper::client::conn::http2::Builder::new(executor)
         198  +
        .handshake::<_, SdkBody>(io)
         199  +
        .await
         200  +
    {
         201  +
        Ok(established) => established,
         202  +
        Err(error) => {
         203  +
            connection.logical_close(CloseReason::ProtocolClosed);
         204  +
            completion.fail(super::super::super::downcast_error(Box::new(error)));
         205  +
            return;
         206  +
        }
         207  +
    };
         208  +
         209  +
    if let Err(lease) = connection.open(permit.into_lease()) {
         210  +
        drop(lease);
         211  +
        connection.logical_close(CloseReason::ProtocolClosed);
         212  +
        completion.fail(ConnectorError::io(Box::new(std::io::Error::other(
         213  +
            "HTTP/2 connection closed before installation",
         214  +
        ))));
         215  +
        return;
         216  +
    }
         217  +
         218  +
    let installed = OriginCell::complete_h2_flight(
         219  +
        &context.cell,
         220  +
        flight,
         221  +
        connection.clone(),
         222  +
        H2Sender::from_hyper(sender),
         223  +
        context.cell.idle_deadline(),
         224  +
    );
         225  +
    let installed = match installed {
         226  +
        Ok(installed) => installed,
         227  +
        Err((_connection, _sender)) => {
         228  +
            connection.logical_close(CloseReason::ProtocolClosed);
         229  +
            completion.fail(ConnectorError::io(Box::new(std::io::Error::other(
         230  +
                "HTTP/2 flight became stale before installation",
         231  +
            ))));
         232  +
            return;
         233  +
        }
         234  +
    };
         235  +
         236  +
    let generation = installed;
         237  +
    let driver_guard = H2DriverGuard::new(H2CloseHandle::new(&context.cell, generation));
         238  +
    let driver_info = connection.info().clone();
         239  +
    context.owner_spawner.spawn(Box::pin(async move {
         240  +
        if let Err(error) = driver.await {
         241  +
            tracing::debug!(
         242  +
                connection_id = %driver_info.id(),
         243  +
                connection_partition = ?driver_info.owner_partition(),
         244  +
                origin_scheme = %driver_info.origin().scheme(),
         245  +
                origin_host = driver_info.origin().host(),
         246  +
                origin_port = ?driver_info.origin().port(),
         247  +
                error = ?error,
         248  +
                "HTTP/2 connection driver failed"
         249  +
            );
         250  +
        }
         251  +
        driver_guard.protocol_closed();
         252  +
    }));
         253  +
         254  +
    completion.disarm();
         255  +
    tracing::debug!(
         256  +
        connection_id = %connection.id(),
         257  +
        request_partition = ?context.partition.id(),
         258  +
        connection_partition = ?connection.owner_partition(),
         259  +
        origin_scheme = %connection.info().origin().scheme(),
         260  +
        origin_host = connection.info().origin().host(),
         261  +
        origin_port = ?connection.info().origin().port(),
         262  +
        h2_generation = ?generation,
         263  +
        "HTTP/2 connection established"
         264  +
    );
         265  +
}
         266  +
         267  +
/// Connector classification copied to each participant in one failed flight.
         268  +
#[derive(Clone, Copy, Debug)]
         269  +
enum SharedFailureKind {
         270  +
    /// Connector timeout classification.
         271  +
    Timeout,
         272  +
    /// Caller or configuration error classification.
         273  +
    User,
         274  +
    /// Retryable transport I/O classification.
         275  +
    Io,
         276  +
    /// Other connector classification and optional retry kind.
         277  +
    Other(Option<ErrorKind>),
         278  +
}
         279  +
         280  +
/// Cloneable wrapper that preserves one flight failure for every participant.
         281  +
#[derive(Clone, Debug)]
         282  +
struct SharedFlightFailure {
         283  +
    /// Original source retained for every participant result.
         284  +
    source: StdArc<BoxError>,
         285  +
    /// Connector classification copied without consuming the source.
         286  +
    kind: SharedFailureKind,
         287  +
}
         288  +
         289  +
impl SharedFlightFailure {
         290  +
    fn new(error: ConnectorError) -> Self {
         291  +
        let kind = if error.is_timeout() {
         292  +
            SharedFailureKind::Timeout
         293  +
        } else if error.is_user() {
         294  +
            SharedFailureKind::User
         295  +
        } else if error.is_io() {
         296  +
            SharedFailureKind::Io
         297  +
        } else {
         298  +
            debug_assert!(error.is_other());
         299  +
            SharedFailureKind::Other(error.as_other())
         300  +
        };
         301  +
        Self {
         302  +
            source: StdArc::new(error.into_source()),
         303  +
            kind,
         304  +
        }
         305  +
    }
         306  +
         307  +
    fn connector_error(&self) -> ConnectorError {
         308  +
        let source: BoxError = Box::new(self.clone());
         309  +
        match self.kind {
         310  +
            SharedFailureKind::Timeout => ConnectorError::timeout(source),
         311  +
            SharedFailureKind::User => ConnectorError::user(source),
         312  +
            SharedFailureKind::Io => ConnectorError::io(source),
         313  +
            SharedFailureKind::Other(kind) => ConnectorError::other(source, kind),
         314  +
        }
         315  +
    }
         316  +
}
         317  +
         318  +
impl std::fmt::Display for SharedFlightFailure {
         319  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         320  +
        std::fmt::Display::fmt(&self.source, f)
         321  +
    }
         322  +
}
         323  +
         324  +
impl std::error::Error for SharedFlightFailure {
         325  +
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         326  +
        Some(self.source.as_ref().as_ref())
         327  +
    }
         328  +
}
         329  +
         330  +
/// Fails every participant still retained by one exact flight.
         331  +
fn fail_participants(cell: &OriginCell, flight: H2FlightId, error: ConnectorError) {
         332  +
    let Some(participants) = cell.fail_h2_flight(flight) else {
         333  +
        return;
         334  +
    };
         335  +
    let error = SharedFlightFailure::new(error);
         336  +
    for participant in participants {
         337  +
        cell.complete_establishment(
         338  +
            participant,
         339  +
            AcquisitionResult::Failed(error.connector_error()),
         340  +
        );
         341  +
    }
         342  +
}
         343  +
         344  +
#[cfg(all(test, not(smithy_http_client_loom)))]
         345  +
mod tests {
         346  +
    use super::*;
         347  +
    use crate::client::pool::admission::ProtocolRequirement;
         348  +
    use crate::client::pool::cell::AcquisitionEvent;
         349  +
    use crate::client::pool::origin::OriginKey;
         350  +
    use crate::client::pool::partition::EligibilityGroup;
         351  +
    use http_1x::uri::Scheme;
         352  +
    use std::error::Error as _;
         353  +
    use std::sync::atomic::{AtomicUsize, Ordering};
         354  +
    use std::task::{Context, Poll, Waker};
         355  +
         356  +
    fn cell() -> crate::sync::Arc<OriginCell> {
         357  +
        crate::sync::Arc::new(OriginCell::new(
         358  +
            super::super::super::partition::PartitionId::from_index(1),
         359  +
            OriginKey::from_parts(Scheme::HTTPS, "example.com", None).unwrap(),
         360  +
            EligibilityGroup::Pool,
         361  +
            None,
         362  +
            None,
         363  +
        ))
         364  +
    }
         365  +
         366  +
    fn launching_waiter(cell: &crate::sync::Arc<OriginCell>) -> WaiterId {
         367  +
        let waiter = OriginCell::register_waiter(cell, ProtocolRequirement::H2Required);
         368  +
        let event = cell.poll_waiter(waiter, &mut Context::from_waker(Waker::noop()));
         369  +
        let Poll::Ready(AcquisitionEvent::Establish(permit)) = event else {
         370  +
            panic!("new H2 waiter did not receive establishment authority");
         371  +
        };
         372  +
        assert!(cell.start_establishment(waiter));
         373  +
        drop(permit);
         374  +
        waiter
         375  +
    }
         376  +
         377  +
    fn failed_event(cell: &OriginCell, waiter: WaiterId) -> ConnectorError {
         378  +
        let event = cell.poll_waiter(waiter, &mut Context::from_waker(Waker::noop()));
         379  +
        let Poll::Ready(AcquisitionEvent::Complete(AcquisitionResult::Failed(error))) = event
         380  +
        else {
         381  +
            panic!("flight participant did not receive a failure");
         382  +
        };
         383  +
        error
         384  +
    }
         385  +
         386  +
    #[test]
         387  +
    fn flight_failure_reaches_every_live_participant() {
         388  +
        let cell = cell();
         389  +
        let first = launching_waiter(&cell);
         390  +
        let second = launching_waiter(&cell);
         391  +
        let H2FlightInstall::Driver(flight) = cell.install_or_join_h2_flight(first) else {
         392  +
            panic!("first participant did not become the flight driver");
         393  +
        };
         394  +
        assert!(matches!(
         395  +
            cell.install_or_join_h2_flight(second),
         396  +
            H2FlightInstall::Joined
         397  +
        ));
         398  +
         399  +
        let mut completion = FlightCompletionGuard::new(cell.clone(), flight);
         400  +
        completion.fail(ConnectorError::io(Box::new(std::io::Error::other(
         401  +
            "synthetic HTTP/2 handshake failure",
         402  +
        ))));
         403  +
         404  +
        for waiter in [first, second] {
         405  +
            let error = failed_event(&cell, waiter);
         406  +
            assert!(error.is_io());
         407  +
            let source = error.source().expect("flight failure lost its source");
         408  +
            assert_eq!("synthetic HTTP/2 handshake failure", source.to_string());
         409  +
            assert!(
         410  +
                source
         411  +
                    .source()
         412  +
                    .expect("flight failure lost its original source")
         413  +
                    .downcast_ref::<std::io::Error>()
         414  +
                    .is_some(),
         415  +
                "flight failure did not preserve the original error"
         416  +
            );
         417  +
        }
         418  +
    }
         419  +
         420  +
    #[test]
         421  +
    fn task_drop_fails_only_participants_still_owned_by_the_flight() {
         422  +
        let cell = cell();
         423  +
        let live = launching_waiter(&cell);
         424  +
        let cancelled = launching_waiter(&cell);
         425  +
        let H2FlightInstall::Driver(flight) = cell.install_or_join_h2_flight(live) else {
         426  +
            panic!("first participant did not become the flight driver");
         427  +
        };
         428  +
        assert!(matches!(
         429  +
            cell.install_or_join_h2_flight(cancelled),
         430  +
            H2FlightInstall::Joined
         431  +
        ));
         432  +
        assert!(OriginCell::cancel_waiter(&cell, cancelled));
         433  +
         434  +
        drop(FlightCompletionGuard::new(cell.clone(), flight));
         435  +
         436  +
        let error = failed_event(&cell, live);
         437  +
        assert!(error.is_io());
         438  +
        assert_eq!(
         439  +
            "HTTP/2 establishment task was dropped",
         440  +
            error
         441  +
                .source()
         442  +
                .expect("task-drop failure lost its source")
         443  +
                .to_string()
         444  +
        );
         445  +
        let successor = OriginCell::register_waiter(&cell, ProtocolRequirement::H2Required);
         446  +
        assert_ne!(cancelled, successor);
         447  +
        assert!(OriginCell::cancel_waiter(&cell, successor));
         448  +
    }
         449  +
         450  +
    #[derive(Debug)]
         451  +
    struct CountingSpawner {
         452  +
        submissions: StdArc<AtomicUsize>,
         453  +
    }
         454  +
         455  +
    impl DriverSpawner for CountingSpawner {
         456  +
        fn spawn(&self, future: std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
         457  +
            self.submissions.fetch_add(1, Ordering::Relaxed);
         458  +
            drop(future);
         459  +
        }
         460  +
    }
         461  +
         462  +
    #[test]
         463  +
    fn hyper_executor_submits_work_to_the_partition_spawner() {
         464  +
        let submissions = StdArc::new(AtomicUsize::new(0));
         465  +
        let executor = PartitionExecutor {
         466  +
            spawner: StdArc::new(CountingSpawner {
         467  +
                submissions: submissions.clone(),
         468  +
            }),
         469  +
        };
         470  +
         471  +
        executor.execute(async {});
         472  +
         473  +
        assert_eq!(1, submissions.load(Ordering::Relaxed));
         474  +
    }
         475  +
}