AWS SDK

AWS SDK

rev. 214b1cfffb5029555908aba78574468110a9ad23 (ignoring whitespace)

Files changed:

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

@@ -1,1 +179,187 @@
   18     18   
//! Waiting --reserve admission or reuse delivery-----> DeliveryPending
   19     19   
//! Waiting --local H1 return--------------------------> Ready
   20     20   
//! DeliveryPending --capacity-------------------------> ReadyToEstablish
   21     21   
//! DeliveryPending --borrowed H1----------------------> Ready
   22     22   
//! DeliveryPending --local H1--> DeliveryPending(pending H1)
   23     23   
//! DeliveryPending(pending H1) --delivery--> Ready + return delivered value
   24     24   
//! ReadyToEstablish --local H1 return-----------------> Ready + return permit
   25     25   
//! ReadyToEstablish --poll----------------------------> Launching(Submitted)
   26     26   
//! Launching(Submitted) --first owner-runtime poll----> Launching(Started)
   27     27   
//! Launching --local H1 or establishment result------> Ready
          28  +
//! Waiting --local or peer H2 activation--------------> Ready
          29  +
//! DeliveryPending --H2 activation--------------------> DeliveryPending(pending H2)
          30  +
//! ReadyToEstablish / Launching --H2 activation-------> Ready
   28     31   
//! Ready --poll---------------------------------------> removed; result transferred
   29     32   
//! ```
   30     33   
//!
   31     34   
//! Cancellation has one residence used only while a committed delivery is
   32     35   
//! crossing the cell lock boundary:
   33     36   
//!
   34     37   
//! ```text
   35     38   
//! Waiting ----------------------> removed; retire or advance demand
   36     39   
//! DeliveryPending --------------> DeliveryCancelled
   37     40   
//! DeliveryCancelled --delivery-> removed; return payloads
   38     41   
//! ReadyToEstablish / Ready -----> removed; return the held event
   39     42   
//! Launching --------------------> removed; task settles separately
   40     43   
//! ```
   41     44   
//!
   42     45   
//! Admission updates, returned values, and task wakes are detached for the
   43     46   
//! caller to process after releasing the cell lock.
   44     47   
   45     48   
use super::super::admission::{DemandId, DemandSnapshot, ProtocolRequirement, SnapshotVersion};
   46     49   
use super::super::partition::EligibilityGroup;
   47     50   
use super::{AcquisitionEvent, AcquisitionResult, EstablishmentPermit};
   48     51   
use std::collections::{BTreeSet, HashMap};
   49     52   
use std::num::NonZeroUsize;
   50     53   
use std::task::{Context, Poll, Waker};
   51     54   
   52     55   
/// Local waiter identity within one cell.
   53     56   
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
   54     57   
pub(in crate::client::pool) struct WaiterId(pub(in crate::client::pool) u64);
   55     58   
   56     59   
/// Complete acquisition state protected by one origin cell lock.
   57     60   
///
   58     61   
/// At every completed transition:
   59     62   
///
   60     63   
/// - `records` owns every retained acquisition attempt.
   61     64   
/// - [`WaitingQueueState::Active`] contains exactly the linked
   62     65   
///   [`WaiterState::Waiting`] records.
   63     66   
/// - The active demand identifies and describes the FIFO head.
   64     67   
/// - `h1_candidates` contains exactly each H1-compatible
   65     68   
///   [`WaiterState::DeliveryPending`] without a pending H1,
   66     69   
///   [`WaiterState::ReadyToEstablish`], or [`WaiterState::Launching`] attempt.
          70  +
/// - `h2_activation_candidates` contains every delivery-pending, ready-to-establish, or
          71  +
///   launching attempt that has not received another result. The waiting FIFO
          72  +
///   head is compared with this set without duplicating its queue residence.
   67     73   
/// - Delivery-pending, ready-to-establish, launching, delivery-cancelled, and
   68     74   
///   ready attempts remain in `records` but never in the bounded FIFO.
   69     75   
///
   70     76   
/// Transitions return admission publications, values requiring fallback, and
   71     77   
/// wakers to the caller; none runs while this state is locked.
   72     78   
#[derive(Debug, Default)]
   73     79   
pub(super) struct AcquisitionQueue {
   74     80   
    /// Every attempt still waiting, launching, or holding a terminal result.
   75     81   
    records: HashMap<WaiterId, WaiterRecord>,
   76     82   
    /// FIFO endpoints and demand for the records currently waiting.
   77     83   
    waiting: WaitingQueueState,
   78     84   
    /// H1-compatible attempts still competing with a returned sender.
   79     85   
    h1_candidates: BTreeSet<WaiterId>,
          86  +
    /// Attempts outside the FIFO that may accept an H2 activation.
          87  +
    h2_activation_candidates: BTreeSet<WaiterId>,
   80     88   
    /// Next cell-local waiter identity.
   81     89   
    next_waiter: u64,
   82     90   
    /// Next identity for a head-waiter demand generation.
   83     91   
    next_demand_id: u64,
   84     92   
}
   85     93   
   86     94   
/// Owner-runtime progress of one submitted establishment task.
   87     95   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
   88     96   
enum EstablishmentPhase {
   89     97   
    /// The task was submitted but has not begun polling the connector.
   90     98   
    Submitted,
   91     99   
    /// The owner runtime polled the task and the pool owns completion.
   92    100   
    Started,
   93    101   
}
   94    102   
   95    103   
/// Whether the cell has an active FIFO and aggregate head demand.
   96    104   
#[derive(Debug, Default)]
   97    105   
enum WaitingQueueState {
   98    106   
    /// No waiter is linked and no demand is active.
   99    107   
    #[default]
  100    108   
    Empty,
  101    109   
    /// A nonempty FIFO represented by its endpoints, length, and head demand.
  102    110   
    Active {
  103    111   
        /// Oldest waiting record.
  104    112   
        head: WaiterId,
  105    113   
        /// Youngest waiting record.
  106    114   
        tail: WaiterId,
  107    115   
        /// Number of records linked from `head` through `tail`.
  108    116   
        len: NonZeroUsize,
  109    117   
        /// Demand generation represented by `head`.
  110    118   
        demand: DemandTicket,
  111    119   
    },
  112    120   
}
  113    121   
  114    122   
/// One request retained while waiting for or owning an acquisition result.
  115    123   
#[derive(Debug)]
  116    124   
struct WaiterRecord {
  117    125   
    /// Protocol requirement published while this waiter is the head.
  118    126   
    requirement: ProtocolRequirement,
  119    127   
    /// Queue residence, delivery state, and any owned result.
  120    128   
    state: WaiterState,
  121    129   
}
  122    130   
  123    131   
/// Authoritative cell-local ownership state for one waiter.
  124    132   
#[derive(Debug)]
  125    133   
enum WaiterState {
  126    134   
    /// The waiter is linked in the cell-local FIFO.
  127    135   
    Waiting {
  128    136   
        /// Older waiting record, or `None` at the head.
  129    137   
        previous: Option<WaiterId>,
  130    138   
        /// Newer waiting record, or `None` at the tail.
  131    139   
        next: Option<WaiterId>,
  132    140   
        /// Latest task waiting for an acquisition result.
  133    141   
        waker: Option<Waker>,
  134    142   
    },
  135    143   
    /// A committed capacity or reuse delivery reserved this waiter but has
  136    144   
    /// not installed its payload.
  137    145   
    DeliveryPending {
  138    146   
        /// Latest task waiting for the crossing delivery.
  139    147   
        waker: Option<Waker>,
  140         -
        /// H1 return that won while capacity was crossing.
  141         -
        pending_h1: Option<AcquisitionResult>,
         148  +
        /// Local protocol result that won while delivery was crossing.
         149  +
        pending_result: Option<AcquisitionResult>,
  142    150   
    },
  143    151   
    /// Cancellation won after reservation while the committed delivery was
  144    152   
    /// outside the lock.
  145    153   
    DeliveryCancelled {
  146    154   
        /// Task detached for wake after the crossing payload is returned.
  147    155   
        waker: Option<Waker>,
  148         -
        /// H1 result returned after the crossing closes, if one arrived first.
  149         -
        pending_h1: Option<AcquisitionResult>,
         156  +
        /// Protocol result returned after the crossing closes, if one arrived first.
         157  +
        pending_result: Option<AcquisitionResult>,
  150    158   
    },
  151    159   
    /// Capacity is ready to start an establishment attempt.
  152    160   
    ReadyToEstablish {
  153    161   
        /// Optional bounded-origin capacity owned until connection install.
  154    162   
        permit: EstablishmentPermit,
  155    163   
    },
  156    164   
    /// Establishment was submitted to the owner runtime and may be unpolled.
  157    165   
    Launching {
  158    166   
        /// Ownership phase used to distinguish submission from first poll.
  159    167   
        phase: EstablishmentPhase,
@@ -183,191 +245,251 @@
  203    211   
        if !bounded {
  204    212   
            self.records.insert(
  205    213   
                waiter,
  206    214   
                WaiterRecord {
  207    215   
                    requirement,
  208    216   
                    state: WaiterState::ReadyToEstablish {
  209    217   
                        permit: EstablishmentPermit::unbounded(),
  210    218   
                    },
  211    219   
                },
  212    220   
            );
  213         -
            if requirement == ProtocolRequirement::H1Compatible {
  214         -
                self.h1_candidates.insert(waiter);
  215         -
            }
         221  +
            self.add_protocol_candidates(waiter, requirement);
  216    222   
            self.assert_consistent();
  217    223   
            return (waiter, None);
  218    224   
        }
  219    225   
  220    226   
        let previous = match &self.waiting {
  221    227   
            WaitingQueueState::Empty => None,
  222    228   
            WaitingQueueState::Active { tail, .. } => Some(*tail),
  223    229   
        };
  224    230   
        let initial_demand = previous.is_none().then(|| self.new_demand(requirement));
  225    231   
@@ -252,258 +488,616 @@
  272    278   
  273    279   
    /// Returns the oldest acquisition attempt that can accept HTTP/1.
  274    280   
    ///
  275    281   
    /// A bounded waiter remains a candidate after receiving capacity so a
  276    282   
    /// returned sender can still beat its lazy or pool-owned establishment
  277    283   
    /// attempt. Comparing waiter identities preserves arrival order between
  278    284   
    /// that set and the current capacity-waiting head.
  279    285   
    fn oldest_h1_candidate(&self) -> Option<WaiterId> {
  280    286   
        let waiting = match &self.waiting {
  281    287   
            WaitingQueueState::Active { head, .. }
  282         -
                if self.records[head].requirement == ProtocolRequirement::H1Compatible =>
         288  +
                if self.records[head].requirement.accepts_h1() =>
  283    289   
            {
  284    290   
                Some(*head)
  285    291   
            }
  286    292   
            WaitingQueueState::Empty | WaitingQueueState::Active { .. } => None,
  287    293   
        };
  288    294   
        match (waiting, self.h1_candidates.first().copied()) {
  289    295   
            (Some(waiting), Some(launching)) => Some(waiting.min(launching)),
  290    296   
            (Some(waiting), None) => Some(waiting),
  291    297   
            (None, launching) => launching,
  292    298   
        }
  293    299   
    }
  294    300   
  295    301   
    /// Returns whether a returned H1 can satisfy a live acquisition attempt.
  296    302   
    pub(super) fn can_accept_h1(&self) -> bool {
  297    303   
        self.oldest_h1_candidate().is_some()
  298    304   
    }
  299    305   
  300    306   
    /// Returns whether a previously admitted local attempt still accepts H1.
  301    307   
    ///
  302    308   
    /// These candidates left the capacity FIFO before its current head and
  303    309   
    /// therefore precede cross-cell reuse for that newer aggregate demand.
  304    310   
    pub(super) fn has_prior_h1_candidate(&self) -> bool {
  305    311   
        !self.h1_candidates.is_empty()
  306    312   
    }
  307    313   
         314  +
    /// Returns the oldest attempt that may accept an HTTP/2 activation.
         315  +
    pub(super) fn oldest_h2_candidate(&self) -> Option<WaiterId> {
         316  +
        let waiting = match &self.waiting {
         317  +
            WaitingQueueState::Active { head, .. }
         318  +
                if self.records[head].requirement.accepts_h2() =>
         319  +
            {
         320  +
                Some(*head)
         321  +
            }
         322  +
            WaitingQueueState::Active { .. } => None,
         323  +
            WaitingQueueState::Empty => None,
         324  +
        };
         325  +
        match (waiting, self.h2_activation_candidates.first().copied()) {
         326  +
            (Some(waiting), Some(active)) => Some(waiting.min(active)),
         327  +
            (Some(waiting), None) => Some(waiting),
         328  +
            (None, active) => active,
         329  +
        }
         330  +
    }
         331  +
         332  +
    /// Advances the active demand beyond an accepted route publication.
         333  +
    ///
         334  +
    /// Admission retires the published version when it acknowledges route
         335  +
    /// visibility. The waiter remains queued locally while the route gate
         336  +
    /// offers activations. Reserving the following version here ensures that
         337  +
    /// route invalidation can republish the same demand generation without an
         338  +
    /// older inactive snapshot winning.
         339  +
    pub(super) fn suppress_published_demand(&mut self, demand: DemandId) -> bool {
         340  +
        let WaitingQueueState::Active {
         341  +
            demand: current, ..
         342  +
        } = &mut self.waiting
         343  +
        else {
         344  +
            return false;
         345  +
        };
         346  +
        if current.id != demand {
         347  +
            return false;
         348  +
        }
         349  +
        let acknowledged = current.version.next();
         350  +
        current.version = acknowledged.next();
         351  +
        self.assert_consistent();
         352  +
        true
         353  +
    }
         354  +
         355  +
    /// Returns the current head demand for publication after route loss.
         356  +
    pub(super) fn current_demand_snapshot(
         357  +
        &self,
         358  +
        eligibility_group: &EligibilityGroup,
         359  +
    ) -> Option<DemandSnapshot> {
         360  +
        match &self.waiting {
         361  +
            WaitingQueueState::Empty => None,
         362  +
            WaitingQueueState::Active { demand, .. } => Some(demand.snapshot(eligibility_group)),
         363  +
        }
         364  +
    }
         365  +
         366  +
    /// Returns whether `waiter` is still the oldest H2 activation candidate.
         367  +
    pub(super) fn is_oldest_h2_candidate(&self, waiter: WaiterId) -> bool {
         368  +
        self.oldest_h2_candidate() == Some(waiter)
         369  +
    }
         370  +
         371  +
    /// Returns the newest waiter identity committed before a publication.
         372  +
    pub(super) fn publication_cutoff(&self) -> Option<WaiterId> {
         373  +
        (!self.records.is_empty()).then(|| {
         374  +
            WaiterId(
         375  +
                self.next_waiter
         376  +
                    .checked_sub(1)
         377  +
                    .expect("nonempty waiter set had no allocated identity"),
         378  +
            )
         379  +
        })
         380  +
    }
         381  +
         382  +
    /// Returns whether an acquisition at or before `cutoff` still needs H2.
         383  +
    pub(super) fn has_h2_candidate_through(&self, cutoff: WaiterId) -> bool {
         384  +
        self.oldest_h2_candidate()
         385  +
            .is_some_and(|waiter| waiter <= cutoff)
         386  +
    }
         387  +
         388  +
    /// Returns whether any acquisition must precede a new direct H2 arrival.
         389  +
    pub(super) fn has_h2_candidate(&self) -> bool {
         390  +
        self.oldest_h2_candidate().is_some()
         391  +
    }
         392  +
         393  +
    /// Returns whether one transferred generation waiter remains launchable.
         394  +
    pub(super) fn is_launching_h2_candidate(&self, waiter: WaiterId) -> bool {
         395  +
        self.h2_activation_candidates.contains(&waiter)
         396  +
            && self.records.get(&waiter).is_some_and(|record| {
         397  +
                record.requirement.accepts_h2()
         398  +
                    && matches!(record.state, WaiterState::Launching { .. })
         399  +
            })
         400  +
    }
         401  +
         402  +
    /// Adds one waiter to each protocol index its requirement accepts.
         403  +
    fn add_protocol_candidates(&mut self, waiter: WaiterId, requirement: ProtocolRequirement) {
         404  +
        if requirement.accepts_h1() {
         405  +
            self.h1_candidates.insert(waiter);
         406  +
        }
         407  +
        if requirement.accepts_h2() {
         408  +
            self.h2_activation_candidates.insert(waiter);
         409  +
        }
         410  +
    }
         411  +
         412  +
    /// Removes one waiter from both protocol candidate indexes.
         413  +
    fn remove_protocol_candidates(&mut self, waiter: WaiterId) {
         414  +
        self.h1_candidates.remove(&waiter);
         415  +
        self.h2_activation_candidates.remove(&waiter);
         416  +
    }
         417  +
  308    418   
    /// Commits a returned HTTP/1 sender to the oldest compatible attempt.
  309    419   
    ///
  310    420   
    /// Payload construction is delayed until all panic-capable requesting-cell checks
  311    421   
    /// have completed. Once constructed, the result is either state-owned or
  312    422   
    /// returned to the caller for cleanup after the cell lock is released.
  313    423   
    pub(super) fn install_returned_h1(
  314    424   
        &mut self,
  315    425   
        result: impl FnOnce() -> AcquisitionResult,
  316    426   
        eligibility_group: &EligibilityGroup,
  317         -
    ) -> H1Install {
         427  +
    ) -> WaiterInstall {
  318    428   
        let Some(waiter) = self.oldest_h1_candidate() else {
  319         -
            return H1Install::rejected(result());
         429  +
            return WaiterInstall::rejected(result());
  320    430   
        };
         431  +
        self.install_protocol_result(waiter, result, eligibility_group)
         432  +
    }
  321    433   
         434  +
    /// Installs a selected protocol result through the shared waiter transition.
         435  +
    fn install_protocol_result(
         436  +
        &mut self,
         437  +
        waiter: WaiterId,
         438  +
        result: impl FnOnce() -> AcquisitionResult,
         439  +
        eligibility_group: &EligibilityGroup,
         440  +
    ) -> WaiterInstall {
  322    441   
        if matches!(
  323    442   
            self.records.get(&waiter).map(|record| &record.state),
  324    443   
            Some(WaiterState::Waiting { .. })
  325    444   
        ) {
  326    445   
            let removed = self.pop_head(eligibility_group);
  327    446   
            debug_assert_eq!(removed.waiter, waiter);
  328    447   
            let record = self
  329    448   
                .records
  330    449   
                .get_mut(&waiter)
  331         -
                .expect("HTTP/1 requesting waiter disappeared");
         450  +
                .expect("selected requesting waiter disappeared");
  332    451   
            let WaiterState::Waiting { waker, .. } = &mut record.state else {
  333         -
                unreachable!("HTTP/1 requesting waiter left the waiting state");
         452  +
                unreachable!("selected requesting waiter left the waiting state");
  334    453   
            };
  335    454   
            let waker = waker.take();
  336    455   
            record.state = WaiterState::Ready(result());
  337    456   
            let retired =
  338    457   
                DemandSnapshot::inactive(removed.demand.id, removed.demand.version.next());
  339    458   
            self.assert_consistent();
  340         -
            return H1Install {
         459  +
            return WaiterInstall {
         460  +
                waiter: Some(waiter),
  341    461   
                demand_updates: [Some(retired), removed.successor],
  342    462   
                returned_event: None,
  343    463   
                waker,
  344    464   
            };
  345    465   
        }
  346    466   
  347    467   
        let record = self
  348    468   
            .records
  349    469   
            .get_mut(&waiter)
  350         -
            .expect("HTTP/1 candidate waiter disappeared");
         470  +
            .expect("selected candidate waiter disappeared");
  351    471   
        let (returned_event, waker) = match &mut record.state {
  352    472   
            WaiterState::DeliveryPending {
  353    473   
                waker: _,
  354         -
                pending_h1,
         474  +
                pending_result,
  355    475   
            } => {
  356         -
                debug_assert!(pending_h1.is_none());
  357         -
                *pending_h1 = Some(result());
         476  +
                debug_assert!(pending_result.is_none());
         477  +
                *pending_result = Some(result());
  358    478   
                (None, None)
  359    479   
            }
  360    480   
            WaiterState::ReadyToEstablish { .. } | WaiterState::Launching { .. } => {
  361    481   
                let previous = std::mem::replace(&mut record.state, WaiterState::Ready(result()));
  362    482   
                match previous {
  363    483   
                    WaiterState::ReadyToEstablish { permit } => {
  364    484   
                        (Some(AcquisitionEvent::Establish(permit)), None)
  365    485   
                    }
  366    486   
                    WaiterState::Launching { waker, .. } => (None, waker),
  367         -
                    _ => unreachable!("HTTP/1 candidate changed state under the cell lock"),
         487  +
                    _ => unreachable!("selected candidate changed state under the cell lock"),
  368    488   
                }
  369    489   
            }
  370    490   
            WaiterState::Waiting { .. }
  371    491   
            | WaiterState::DeliveryCancelled { .. }
  372    492   
            | WaiterState::Ready(_) => {
  373         -
                unreachable!("HTTP/1 candidate was not eligible for a returned sender")
         493  +
                unreachable!("selected waiter was not eligible for a protocol result")
  374    494   
            }
  375    495   
        };
  376         -
        self.h1_candidates.remove(&waiter);
         496  +
        self.remove_protocol_candidates(waiter);
  377    497   
        if returned_event.is_none() {
  378         -
            // The incoming H1 is state-owned and no permit is detached, so an
         498  +
            // The result is state-owned and no permit is detached, so an
  379    499   
            // invariant failure cannot drop a pool value under this lock.
  380    500   
            self.assert_consistent();
  381    501   
        }
  382         -
        H1Install {
         502  +
        WaiterInstall {
         503  +
            waiter: Some(waiter),
  383    504   
            demand_updates: [None, None],
  384    505   
            returned_event,
  385    506   
            waker,
  386    507   
        }
  387    508   
    }
  388    509   
  389    510   
    /// Cancels one waiter and detaches all cross-lock cleanup work.
  390    511   
    ///
  391    512   
    /// Removing the head retires its demand and starts a successor generation.
  392    513   
    /// Removing another waiting record leaves the active demand unchanged.
  393    514   
    /// Cancellation during delivery leaves a marker for result installation to
  394    515   
    /// observe. A ready result is returned to the caller for cleanup after unlock.
  395    516   
    pub(super) fn cancel_waiter(
  396    517   
        &mut self,
  397    518   
        waiter: WaiterId,
  398    519   
        eligibility_group: &EligibilityGroup,
  399    520   
    ) -> Option<WaiterCancellation> {
  400    521   
        let state = &self.records.get(&waiter)?.state;
  401    522   
        let cancellation = if matches!(state, WaiterState::Waiting { .. }) {
  402    523   
            let is_head = matches!(
  403    524   
                self.waiting,
  404    525   
                WaitingQueueState::Active { head, .. } if head == waiter
  405    526   
            );
  406    527   
            let demand_updates = if is_head {
  407    528   
                let removed = self.pop_head(eligibility_group);
  408    529   
                debug_assert_eq!(waiter, removed.waiter);
  409    530   
                let record = self
  410    531   
                    .records
  411    532   
                    .remove(&waiter)
  412    533   
                    .expect("cancelled head waiter disappeared");
  413    534   
                debug_assert!(matches!(record.state, WaiterState::Waiting { .. }));
  414    535   
                let retired =
  415    536   
                    DemandSnapshot::inactive(removed.demand.id, removed.demand.version.next());
  416    537   
                [Some(retired), removed.successor]
  417    538   
            } else {
  418    539   
                self.remove_non_head(waiter);
  419    540   
                [None, None]
  420    541   
            };
  421    542   
            WaiterCancellation {
  422    543   
                demand_updates,
  423    544   
                returned_events: [None, None],
  424    545   
            }
  425    546   
        } else if matches!(state, WaiterState::DeliveryPending { .. }) {
  426         -
            self.h1_candidates.remove(&waiter);
         547  +
            self.remove_protocol_candidates(waiter);
  427    548   
            let record = self.records.get_mut(&waiter)?;
  428         -
            let WaiterState::DeliveryPending { waker, pending_h1 } = &mut record.state else {
         549  +
            let WaiterState::DeliveryPending {
         550  +
                waker,
         551  +
                pending_result,
         552  +
            } = &mut record.state
         553  +
            else {
  429    554   
                unreachable!("delivery-pending waiter changed state under the cell lock");
  430    555   
            };
  431    556   
            let waker = waker.take();
  432         -
            let pending_h1 = pending_h1.take();
  433         -
            record.state = WaiterState::DeliveryCancelled { waker, pending_h1 };
         557  +
            let pending_result = pending_result.take();
         558  +
            record.state = WaiterState::DeliveryCancelled {
         559  +
                waker,
         560  +
                pending_result,
         561  +
            };
  434    562   
            WaiterCancellation {
  435    563   
                demand_updates: [None, None],
  436    564   
                returned_events: [None, None],
  437    565   
            }
  438    566   
        } else if matches!(
  439    567   
            state,
  440    568   
            WaiterState::ReadyToEstablish { .. } | WaiterState::Ready(_)
  441    569   
        ) {
  442    570   
            // Validate before detaching the event. Once it is local,
  443    571   
            // this method must return without another panic-capable check.
  444    572   
            self.assert_consistent();
  445    573   
            let record = self.records.remove(&waiter)?;
  446         -
            self.h1_candidates.remove(&waiter);
         574  +
            self.remove_protocol_candidates(waiter);
  447    575   
            let event = match record.state {
  448    576   
                WaiterState::ReadyToEstablish { permit } => AcquisitionEvent::Establish(permit),
  449    577   
                WaiterState::Ready(result) => AcquisitionEvent::Complete(result),
  450    578   
                _ => unreachable!("ready waiter changed state under the cell lock"),
  451    579   
            };
  452    580   
            return Some(WaiterCancellation {
  453    581   
                demand_updates: [None, None],
  454    582   
                returned_events: [Some(event), None],
  455    583   
            });
  456    584   
        } else if matches!(state, WaiterState::Launching { .. }) {
  457    585   
            self.assert_consistent();
  458         -
            self.h1_candidates.remove(&waiter);
         586  +
            self.remove_protocol_candidates(waiter);
  459    587   
            self.records.remove(&waiter)?;
  460    588   
            return Some(WaiterCancellation {
  461    589   
                demand_updates: [None, None],
  462    590   
                returned_events: [None, None],
  463    591   
            });
  464    592   
        } else {
  465    593   
            debug_assert!(matches!(state, WaiterState::DeliveryCancelled { .. }));
  466    594   
            return None;
  467    595   
        };
  468    596   
@@ -525,653 +831,993 @@
  545    673   
    pub(super) fn start_establishment(&mut self, waiter: WaiterId) -> bool {
  546    674   
        let Some(record) = self.records.get_mut(&waiter) else {
  547    675   
            return false;
  548    676   
        };
  549    677   
        match &mut record.state {
  550    678   
            WaiterState::Launching { phase, .. } if *phase == EstablishmentPhase::Submitted => {
  551    679   
                *phase = EstablishmentPhase::Started;
  552    680   
                self.assert_consistent();
  553    681   
                true
  554    682   
            }
  555         -
            WaiterState::Ready(AcquisitionResult::H1(_)) => false,
         683  +
            WaiterState::Ready(_) => false,
  556    684   
            WaiterState::Launching {
  557    685   
                phase: EstablishmentPhase::Started,
  558    686   
                ..
  559    687   
            } => {
  560    688   
                panic!("establishment attempt was started more than once")
  561    689   
            }
  562    690   
            _ => {
  563    691   
                panic!("establishment start did not name a launching waiter")
  564    692   
            }
  565    693   
        }
  566    694   
    }
  567    695   
  568    696   
    /// Reserves the current head when a delivery still names its demand
  569    697   
    /// generation.
  570    698   
    ///
  571    699   
    /// The reserved waiter leaves the FIFO and changes in place to
  572    700   
    /// [`WaiterState::DeliveryPending`]. Any remaining head receives a new demand
  573    701   
    /// generation returned for admission acknowledgement.
  574    702   
    pub(super) fn reserve_delivery_waiter(
  575    703   
        &mut self,
  576    704   
        demand: DemandId,
  577    705   
        eligibility_group: &EligibilityGroup,
  578    706   
    ) -> DeliveryReservation {
  579    707   
        let current = matches!(
  580    708   
            &self.waiting,
  581    709   
            WaitingQueueState::Active {
  582    710   
                demand: current,
  583    711   
                ..
  584    712   
            } if current.id == demand
  585    713   
        );
  586    714   
        if !current {
  587    715   
            return DeliveryReservation::Rejected;
  588    716   
        }
  589    717   
  590    718   
        let removed = self.pop_head(eligibility_group);
  591    719   
        debug_assert_eq!(removed.demand.id, demand);
  592    720   
        let record = self
  593    721   
            .records
  594    722   
            .get_mut(&removed.waiter)
  595    723   
            .expect("reserved queue head disappeared");
  596    724   
        let WaiterState::Waiting { waker, .. } = &mut record.state else {
  597    725   
            unreachable!("reserved queue head left the waiting state");
  598    726   
        };
  599    727   
        let waker = waker.take();
  600         -
        let accepts_h1 = record.requirement == ProtocolRequirement::H1Compatible;
         728  +
        let requirement = record.requirement;
  601    729   
        record.state = WaiterState::DeliveryPending {
  602    730   
            waker,
  603         -
            pending_h1: None,
         731  +
            pending_result: None,
  604    732   
        };
  605         -
        if accepts_h1 {
  606         -
            self.h1_candidates.insert(removed.waiter);
  607         -
        }
         733  +
        self.add_protocol_candidates(removed.waiter, requirement);
  608    734   
  609    735   
        self.assert_consistent();
  610    736   
        DeliveryReservation::Reserved {
  611    737   
            waiter: removed.waiter,
  612    738   
            successor: removed.successor,
  613    739   
        }
  614    740   
    }
  615    741   
  616    742   
    /// Completes the unlocked capacity crossing without panicking while the
  617    743   
    /// incoming permit remains a droppable local.
  618    744   
    ///
  619    745   
    /// A live receiver takes ownership of the permit before another
  620    746   
    /// panic-capable operation. If an H1 return already won, both resources
  621    747   
    /// leave the lock as separate events and the permit is refunnelled.
  622    748   
    pub(super) fn install_capacity(
  623    749   
        &mut self,
  624    750   
        waiter: WaiterId,
  625    751   
        permit: EstablishmentPermit,
  626    752   
    ) -> ResultInstall {
  627    753   
        let Some(record) = self.records.get_mut(&waiter) else {
  628    754   
            return ResultInstall::invalid(
  629    755   
                AcquisitionEvent::Establish(permit),
  630    756   
                ResultInstallError::MissingWaiter,
  631    757   
            );
  632    758   
        };
  633    759   
  634    760   
        match &mut record.state {
  635         -
            WaiterState::DeliveryPending { waker, pending_h1 } => {
         761  +
            WaiterState::DeliveryPending {
         762  +
                waker,
         763  +
                pending_result,
         764  +
            } => {
  636    765   
                let waker = waker.take();
  637         -
                if let Some(h1) = pending_h1.take() {
  638         -
                    record.state = WaiterState::Ready(h1);
  639         -
                    self.h1_candidates.remove(&waiter);
         766  +
                if let Some(result) = pending_result.take() {
         767  +
                    record.state = WaiterState::Ready(result);
         768  +
                    self.remove_protocol_candidates(waiter);
  640    769   
                    // The rejected permit must cross the lock boundary even
  641    770   
                    // if other state is already inconsistent.
  642    771   
                    return ResultInstall {
  643    772   
                        returned_events: [Some(AcquisitionEvent::Establish(permit)), None],
  644    773   
                        waker,
  645    774   
                        error: None,
  646    775   
                        accepted: false,
  647    776   
                    };
  648    777   
                }
  649    778   
                record.state = WaiterState::ReadyToEstablish { permit };
  650    779   
                // The permit is state-owned before this check can panic.
  651    780   
                self.assert_consistent();
  652    781   
                ResultInstall {
  653    782   
                    returned_events: [None, None],
  654    783   
                    waker,
  655    784   
                    error: None,
  656    785   
                    accepted: true,
  657    786   
                }
  658    787   
            }
  659         -
            WaiterState::DeliveryCancelled { waker, pending_h1 } => {
         788  +
            WaiterState::DeliveryCancelled {
         789  +
                waker,
         790  +
                pending_result,
         791  +
            } => {
  660    792   
                let waker = waker.take();
  661         -
                let pending_h1 = pending_h1.take();
         793  +
                let pending_result = pending_result.take();
  662    794   
                self.records.remove(&waiter);
  663    795   
                ResultInstall {
  664    796   
                    returned_events: [
  665         -
                        pending_h1.map(AcquisitionEvent::Complete),
         797  +
                        pending_result.map(AcquisitionEvent::Complete),
  666    798   
                        Some(AcquisitionEvent::Establish(permit)),
  667    799   
                    ],
  668    800   
                    waker,
  669    801   
                    error: None,
  670    802   
                    accepted: false,
  671    803   
                }
  672    804   
            }
  673    805   
            WaiterState::Waiting { .. }
  674    806   
            | WaiterState::ReadyToEstablish { .. }
  675    807   
            | WaiterState::Launching { .. }
  676    808   
            | WaiterState::Ready(_) => ResultInstall::invalid(
  677    809   
                AcquisitionEvent::Establish(permit),
  678    810   
                ResultInstallError::UnexpectedState,
  679    811   
            ),
  680    812   
        }
  681    813   
    }
  682    814   
  683    815   
    /// Installs a borrowed HTTP/1 result after its requesting waiter was reserved.
  684    816   
    ///
  685    817   
    /// A local return may have won while the borrowed sender crossed its
  686    818   
    /// owning-cell and requesting-cell locks. In that case the local result stays ready and
  687    819   
    /// the borrowed result leaves the lock for ordinary owning-cell return.
  688    820   
    pub(super) fn install_borrowed_h1(
  689    821   
        &mut self,
  690    822   
        waiter: WaiterId,
  691    823   
        result: AcquisitionResult,
  692    824   
    ) -> ResultInstall {
  693    825   
        let Some(record) = self.records.get_mut(&waiter) else {
  694    826   
            return ResultInstall::invalid(
  695    827   
                AcquisitionEvent::Complete(result),
  696    828   
                ResultInstallError::MissingWaiter,
  697    829   
            );
  698    830   
        };
  699    831   
  700    832   
        match &mut record.state {
  701         -
            WaiterState::DeliveryPending { waker, pending_h1 } => {
         833  +
            WaiterState::DeliveryPending {
         834  +
                waker,
         835  +
                pending_result,
         836  +
            } => {
  702    837   
                let waker = waker.take();
  703         -
                if let Some(local_h1) = pending_h1.take() {
  704         -
                    record.state = WaiterState::Ready(local_h1);
  705         -
                    self.h1_candidates.remove(&waiter);
         838  +
                if let Some(local_result) = pending_result.take() {
         839  +
                    record.state = WaiterState::Ready(local_result);
         840  +
                    self.remove_protocol_candidates(waiter);
  706    841   
                    // The rejected borrowed sender must cross the lock
  707    842   
                    // boundary before any panic-capable consistency check.
  708    843   
                    return ResultInstall {
  709    844   
                        returned_events: [Some(AcquisitionEvent::Complete(result)), None],
  710    845   
                        waker,
  711    846   
                        error: None,
  712    847   
                        accepted: false,
  713    848   
                    };
  714    849   
                }
  715    850   
                record.state = WaiterState::Ready(result);
  716         -
                self.h1_candidates.remove(&waiter);
         851  +
                self.remove_protocol_candidates(waiter);
  717    852   
                self.assert_consistent();
  718    853   
                ResultInstall {
  719    854   
                    returned_events: [None, None],
  720    855   
                    waker,
  721    856   
                    error: None,
  722    857   
                    accepted: true,
  723    858   
                }
  724    859   
            }
  725         -
            WaiterState::DeliveryCancelled { waker, pending_h1 } => {
         860  +
            WaiterState::DeliveryCancelled {
         861  +
                waker,
         862  +
                pending_result,
         863  +
            } => {
  726    864   
                let waker = waker.take();
  727         -
                let pending_h1 = pending_h1.take();
         865  +
                let pending_result = pending_result.take();
  728    866   
                self.records.remove(&waiter);
  729    867   
                ResultInstall {
  730    868   
                    returned_events: [
  731         -
                        pending_h1.map(AcquisitionEvent::Complete),
         869  +
                        pending_result.map(AcquisitionEvent::Complete),
  732    870   
                        Some(AcquisitionEvent::Complete(result)),
  733    871   
                    ],
  734    872   
                    waker,
  735    873   
                    error: None,
  736    874   
                    accepted: false,
  737    875   
                }
  738    876   
            }
  739    877   
            WaiterState::Waiting { .. }
  740    878   
            | WaiterState::ReadyToEstablish { .. }
  741    879   
            | WaiterState::Launching { .. }
  742    880   
            | WaiterState::Ready(_) => ResultInstall::invalid(
  743    881   
                AcquisitionEvent::Complete(result),
  744    882   
                ResultInstallError::UnexpectedState,
  745    883   
            ),
  746    884   
        }
  747    885   
    }
  748    886   
  749    887   
    /// Commits one terminal establishment result when its waiter is still live.
  750    888   
    ///
  751    889   
    /// Completion may occur from either launch phase: normal execution starts
  752    890   
    /// the connector first, while dropping an unpolled submitted task installs
  753    891   
    /// a terminal runtime error.
  754    892   
    pub(super) fn install_establishment_result(
  755    893   
        &mut self,
  756    894   
        waiter: WaiterId,
  757    895   
        result: AcquisitionResult,
  758    896   
    ) -> ResultInstall {
  759    897   
        let Some(record) = self.records.get_mut(&waiter) else {
  760    898   
            return ResultInstall::rejected(AcquisitionEvent::Complete(result));
  761    899   
        };
  762         -
        if matches!(record.state, WaiterState::Ready(AcquisitionResult::H1(_))) {
         900  +
        if matches!(record.state, WaiterState::Ready(_)) {
  763    901   
            return ResultInstall::rejected(AcquisitionEvent::Complete(result));
  764    902   
        }
  765         -
        let WaiterState::Launching { waker, .. } = &mut record.state else {
         903  +
        if !matches!(record.state, WaiterState::Launching { .. }) {
  766    904   
            return ResultInstall::invalid(
  767    905   
                AcquisitionEvent::Complete(result),
  768    906   
                ResultInstallError::UnexpectedState,
  769    907   
            );
         908  +
        }
         909  +
        let WaiterState::Launching { waker, .. } = &mut record.state else {
         910  +
            unreachable!("validated launching waiter changed state")
  770    911   
        };
  771    912   
        let waker = waker.take();
  772    913   
        record.state = WaiterState::Ready(result);
  773         -
        self.h1_candidates.remove(&waiter);
         914  +
        self.remove_protocol_candidates(waiter);
  774    915   
        self.assert_consistent();
  775    916   
        ResultInstall {
  776    917   
            returned_events: [None, None],
  777    918   
            waker,
  778    919   
            error: None,
  779    920   
            accepted: true,
  780    921   
        }
  781    922   
    }
  782    923   
  783    924   
    /// Removes a ready waiter and transfers ownership of its result.
  784    925   
    pub(super) fn take_ready_result(&mut self, waiter: WaiterId) -> Option<AcquisitionResult> {
  785    926   
        if !matches!(
  786    927   
            self.records.get(&waiter).map(|record| &record.state),
  787    928   
            Some(WaiterState::Ready(_))
  788    929   
        ) {
  789    930   
            return None;
  790    931   
        }
  791    932   
  792    933   
        // Validate before detaching the result. Returning it must be the last
  793    934   
        // operation performed while the cell lock is held.
  794    935   
        self.assert_consistent();
  795    936   
        let record = self.records.remove(&waiter)?;
  796    937   
        let WaiterState::Ready(result) = record.state else {
  797    938   
            unreachable!("ready waiter changed state under the cell lock");
  798    939   
        };
  799    940   
        Some(result)
  800    941   
    }
  801    942   
         943  +
    /// Offers an H2 activation to the oldest compatible acquisition.
         944  +
    ///
         945  +
    /// `cutoff` retains publication priority. A later waiter is left pending
         946  +
    /// until every live acquisition at or before the cutoff has received an
         947  +
    /// activation opportunity.
         948  +
    pub(super) fn install_h2(
         949  +
        &mut self,
         950  +
        cutoff: Option<WaiterId>,
         951  +
        result: impl FnOnce(WaiterId) -> AcquisitionResult,
         952  +
        eligibility_group: &EligibilityGroup,
         953  +
    ) -> WaiterInstall {
         954  +
        let Some(waiter) = self.oldest_h2_candidate() else {
         955  +
            return WaiterInstall::empty();
         956  +
        };
         957  +
        if cutoff.is_some_and(|cutoff| waiter > cutoff) {
         958  +
            return WaiterInstall::empty();
         959  +
        }
         960  +
         961  +
        self.install_protocol_result(waiter, || result(waiter), eligibility_group)
         962  +
    }
         963  +
  802    964   
    /// Unlinks the FIFO head and installs any successor demand.
  803    965   
    ///
  804    966   
    /// The head record remains in `records`; the caller either changes it to
  805    967   
    /// `DeliveryPending` or removes it for cancellation.
  806    968   
    fn pop_head(&mut self, eligibility_group: &EligibilityGroup) -> RemovedHead {
  807    969   
        let waiting = std::mem::take(&mut self.waiting);
  808    970   
        let WaitingQueueState::Active {
  809    971   
            head,
  810    972   
            tail,
  811    973   
            len,
@@ -928,1090 +989,1156 @@
  948   1110   
        DemandTicket {
  949   1111   
            id: DemandId::from_u64(value),
  950   1112   
            version: SnapshotVersion::INITIAL,
  951   1113   
            requirement,
  952   1114   
        }
  953   1115   
    }
  954   1116   
  955   1117   
    /// Checks map, FIFO, and demand relationships in debug and test builds.
  956   1118   
    pub(super) fn assert_consistent(&self) {
  957   1119   
        #[cfg(debug_assertions)]
        1120  +
        {
        1121  +
            if std::thread::panicking() {
        1122  +
                return;
        1123  +
            }
  958   1124   
            self.assert_consistent_debug();
  959   1125   
        }
        1126  +
    }
  960   1127   
  961   1128   
    #[cfg(debug_assertions)]
  962   1129   
    fn assert_consistent_debug(&self) {
  963   1130   
        let waiting_records = self
  964   1131   
            .records
  965   1132   
            .values()
  966   1133   
            .filter(|record| matches!(record.state, WaiterState::Waiting { .. }))
  967   1134   
            .count();
  968   1135   
        match &self.waiting {
  969   1136   
            WaitingQueueState::Empty => {
@@ -1000,1167 +1129,1330 @@
 1020   1187   
                    waiting_records, traversed,
 1021   1188   
                    "waiting record was not reachable from the queue head"
 1022   1189   
                );
 1023   1190   
            }
 1024   1191   
        }
 1025   1192   
 1026   1193   
        let expected_h1_candidates = self
 1027   1194   
            .records
 1028   1195   
            .iter()
 1029   1196   
            .filter_map(|(waiter, record)| {
 1030         -
                (record.requirement == ProtocolRequirement::H1Compatible
        1197  +
                (record.requirement.accepts_h1()
 1031   1198   
                    && matches!(
 1032   1199   
                        record.state,
 1033   1200   
                        WaiterState::DeliveryPending {
 1034         -
                            pending_h1: None,
        1201  +
                            pending_result: None,
 1035   1202   
                            ..
 1036   1203   
                        } | WaiterState::ReadyToEstablish { .. }
 1037   1204   
                            | WaiterState::Launching { .. }
 1038   1205   
                    ))
 1039   1206   
                .then_some(*waiter)
 1040   1207   
            })
 1041   1208   
            .collect::<BTreeSet<_>>();
 1042   1209   
        assert_eq!(
 1043   1210   
            expected_h1_candidates, self.h1_candidates,
 1044   1211   
            "HTTP/1 acquisition candidates did not match waiter state"
 1045   1212   
        );
        1213  +
        1214  +
        let expected_h2_activation_candidates = self
        1215  +
            .records
        1216  +
            .iter()
        1217  +
            .filter_map(|(waiter, record)| {
        1218  +
                (record.requirement.accepts_h2()
        1219  +
                    && matches!(
        1220  +
                        record.state,
        1221  +
                        WaiterState::DeliveryPending {
        1222  +
                            pending_result: None,
        1223  +
                            ..
        1224  +
                        } | WaiterState::ReadyToEstablish { .. }
        1225  +
                            | WaiterState::Launching { .. }
        1226  +
                    ))
        1227  +
                .then_some(*waiter)
        1228  +
            })
        1229  +
            .collect::<BTreeSet<_>>();
        1230  +
        assert_eq!(
        1231  +
            expected_h2_activation_candidates, self.h2_activation_candidates,
        1232  +
            "HTTP/2 acquisition candidates did not match waiter state"
        1233  +
        );
 1046   1234   
    }
 1047   1235   
 1048   1236   
    #[cfg(test)]
 1049   1237   
    pub(super) fn snapshot(&self) -> CellSnapshot {
 1050   1238   
        let (waiting, demand) = match &self.waiting {
 1051   1239   
            WaitingQueueState::Empty => (0, None),
 1052   1240   
            WaitingQueueState::Active { len, demand, .. } => (len.get(), Some(demand.id)),
 1053   1241   
        };
 1054   1242   
        CellSnapshot {
 1055   1243   
            waiting,
 1056   1244   
            retained: self.records.len(),
 1057   1245   
            demand,
 1058   1246   
        }
 1059   1247   
    }
 1060   1248   
}
 1061   1249   
 1062   1250   
/// Result of reserving a requesting waiter for one unlocked delivery.
 1063   1251   
pub(super) enum DeliveryReservation {
 1064   1252   
    /// The current head was reserved and may have a successor demand.
 1065   1253   
    Reserved {
 1066   1254   
        waiter: WaiterId,
 1067   1255   
        successor: Option<DemandSnapshot>,
 1068   1256   
    },
 1069   1257   
    /// The delivery no longer matches live cell demand.
 1070   1258   
    Rejected,
 1071   1259   
}
 1072   1260   
 1073   1261   
/// Values detached from cell state by cancellation.
 1074   1262   
pub(super) struct WaiterCancellation {
 1075   1263   
    /// Demand retirement and optional successor published after unlocking.
 1076   1264   
    pub(super) demand_updates: [Option<DemandSnapshot>; 2],
 1077   1265   
    /// Intermediate and terminal events returned for cleanup after unlocking.
 1078   1266   
    pub(super) returned_events: [Option<AcquisitionEvent>; 2],
 1079   1267   
}
 1080   1268   
 1081         -
/// Values detached after attempting cell-local HTTP/1 service.
 1082         -
pub(super) struct H1Install {
        1269  +
/// Values detached after installing a selected protocol result.
        1270  +
pub(super) struct WaiterInstall {
        1271  +
    /// Waiter that accepted the result, when one remained eligible.
        1272  +
    pub(super) waiter: Option<WaiterId>,
 1083   1273   
    /// Demand retirement and optional successor published after unlocking.
 1084   1274   
    pub(super) demand_updates: [Option<DemandSnapshot>; 2],
 1085         -
    /// Establishment permit or rejected H1 returned after unlocking.
        1275  +
    /// Establishment permit or rejected result returned after unlocking.
 1086   1276   
    pub(super) returned_event: Option<AcquisitionEvent>,
 1087   1277   
    /// Waiting task woken after demand publication.
 1088   1278   
    pub(super) waker: Option<Waker>,
 1089   1279   
}
 1090   1280   
 1091         -
impl H1Install {
        1281  +
impl WaiterInstall {
 1092   1282   
    /// Preserves a result when no live attempt can accept HTTP/1.
 1093   1283   
    fn rejected(result: AcquisitionResult) -> Self {
 1094   1284   
        Self {
        1285  +
            waiter: None,
 1095   1286   
            demand_updates: [None, None],
 1096   1287   
            returned_event: Some(AcquisitionEvent::Complete(result)),
 1097   1288   
            waker: None,
 1098   1289   
        }
 1099   1290   
    }
        1291  +
        1292  +
    /// Reports that no live attempt can accept an HTTP/2 activation.
        1293  +
    fn empty() -> Self {
        1294  +
        Self {
        1295  +
            waiter: None,
        1296  +
            demand_updates: [None, None],
        1297  +
            returned_event: None,
        1298  +
            waker: None,
        1299  +
        }
        1300  +
    }
 1100   1301   
}
 1101   1302   
 1102   1303   
/// Invalid state observed while a committed event was crossing to the cell.
 1103   1304   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 1104   1305   
pub(super) enum ResultInstallError {
 1105   1306   
    /// The waiter reserved for the delivery no longer exists.
 1106   1307   
    MissingWaiter,
 1107   1308   
    /// The waiter exists but cannot receive the reserved delivery.
 1108   1309   
    UnexpectedState,
 1109   1310   
}
@@ -1140,1341 +1199,1414 @@
 1160   1361   
pub(super) struct CellSnapshot {
 1161   1362   
    pub(super) waiting: usize,
 1162   1363   
    pub(super) retained: usize,
 1163   1364   
    pub(super) demand: Option<DemandId>,
 1164   1365   
}
 1165   1366   
 1166   1367   
#[cfg(all(test, not(smithy_http_client_loom)))]
 1167   1368   
mod tests {
 1168   1369   
    use super::*;
 1169   1370   
        1371  +
    #[test]
        1372  +
    fn h1_required_waiter_is_not_an_h2_candidate() {
        1373  +
        let mut queue = AcquisitionQueue::default();
        1374  +
        let (waiter, snapshot) = queue.register_waiter(
        1375  +
            ProtocolRequirement::H1Required,
        1376  +
            &EligibilityGroup::Pool,
        1377  +
            false,
        1378  +
        );
        1379  +
        1380  +
        assert!(snapshot.is_none());
        1381  +
        assert_eq!(Some(waiter), queue.oldest_h1_candidate());
        1382  +
        assert_eq!(None, queue.oldest_h2_candidate());
        1383  +
    }
        1384  +
 1170   1385   
    #[test]
 1171   1386   
    fn head_cancellation_uses_the_successors_protocol_requirement() {
 1172   1387   
        let mut queue = AcquisitionQueue::default();
 1173   1388   
        let (head, initial) = queue.register_waiter(
 1174   1389   
            ProtocolRequirement::H2Required,
 1175   1390   
            &EligibilityGroup::Pool,
 1176   1391   
            true,
 1177   1392   
        );
 1178   1393   
        let (_successor, no_new_demand) = queue.register_waiter(
 1179   1394   
            ProtocolRequirement::H1Compatible,

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

@@ -30,30 +448,495 @@
   50     50   
    /// The runtime driving the connection shut down.
   51     51   
    OwnerRuntimeShutdown,
   52     52   
}
   53     53   
   54     54   
/// Protocol selected for one installed physical connection.
   55     55   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
   56     56   
pub(super) enum NegotiatedProtocol {
   57     57   
    /// HTTP/1.1 with one exclusive request sender.
   58     58   
    Http1,
   59     59   
    /// HTTP/2 with a multiplexed request sender.
   60         -
    #[allow(
   61         -
        dead_code,
   62         -
        reason = "constructed once HTTP/2 generations are implemented"
   63         -
    )]
   64     60   
    Http2,
   65     61   
}
   66     62   
   67     63   
/// Immutable identity and transport facts shared by a connection's owners.
   68     64   
///
   69         -
/// The protocol record, request metadata, tracing, and future lifecycle events
          65  +
/// The protocol record, request metadata, tracing, and lifecycle events
   70     66   
/// all retain this one allocation rather than reconstructing origin or address
   71     67   
/// data at each transition.
   72     68   
#[derive(Debug)]
   73     69   
pub(super) struct ConnectionInfo {
   74     70   
    /// Stable identity assigned by the owning pool.
   75     71   
    id: ConnectionId,
   76     72   
    /// Canonical origin this connection may serve.
   77     73   
    origin: OriginKey,
   78     74   
    /// Partition that retains the transport and protocol driver.
   79     75   
    owner_partition: PartitionId,
   80     76   
    /// Protocol selected by configuration or ALPN.
   81         -
    #[allow(
   82         -
        dead_code,
   83         -
        reason = "read when HTTP/2 generation selection is implemented"
   84         -
    )]
   85     77   
    protocol: NegotiatedProtocol,
   86     78   
    /// Local socket address reported by the connector, when available.
   87     79   
    local_addr: Option<SocketAddr>,
   88     80   
    /// Remote socket address reported by the connector, when available.
   89     81   
    remote_addr: Option<SocketAddr>,
   90     82   
    /// Whether the transport reaches the origin through a proxy.
   91     83   
    proxied: bool,
   92     84   
    /// Connector metadata copied into every response on this connection.
   93     85   
    connected: Connected,
   94     86   
}
   95     87   
   96     88   
impl ConnectionInfo {
   97         -
    /// Captures immutable facts when protocol establishment succeeds.
          89  +
    /// Captures immutable facts after transport negotiation selects a protocol.
   98     90   
    pub(super) fn new(
   99     91   
        id: ConnectionId,
  100     92   
        origin: OriginKey,
  101     93   
        owner_partition: PartitionId,
  102     94   
        protocol: NegotiatedProtocol,
  103     95   
        connected: Connected,
  104     96   
    ) -> Arc<Self> {
  105     97   
        let mut extras = Extensions::new();
  106     98   
        connected.get_extras(&mut extras);
  107     99   
        let http_info = extras.get::<HttpInfo>();
  108    100   
        Arc::new(Self {
  109    101   
            id,
  110    102   
            origin,
  111    103   
            owner_partition,
  112    104   
            protocol,
  113    105   
            local_addr: http_info.map(HttpInfo::local_addr),
  114    106   
            remote_addr: http_info.map(HttpInfo::remote_addr),
  115    107   
            proxied: connected.is_proxied(),
  116    108   
            connected,
  117    109   
        })
  118    110   
    }
  119    111   
  120    112   
    /// Returns the pool-assigned physical connection identity.
  121    113   
    pub(super) fn id(&self) -> ConnectionId {
  122    114   
        self.id
  123    115   
    }
  124    116   
  125    117   
    /// Returns the canonical origin this connection may serve.
  126    118   
    pub(super) fn origin(&self) -> &OriginKey {
  127    119   
        &self.origin
  128    120   
    }
  129    121   
  130    122   
    /// Returns the partition that owns this connection's I/O and driver.
  131    123   
    pub(super) fn owner_partition(&self) -> PartitionId {
  132    124   
        self.owner_partition
  133    125   
    }
  134    126   
  135    127   
    /// Returns the established HTTP protocol.
  136         -
    #[cfg(test)]
  137    128   
    pub(super) fn protocol(&self) -> NegotiatedProtocol {
  138    129   
        self.protocol
  139    130   
    }
  140    131   
  141    132   
    /// Returns the connector-reported local socket address.
  142    133   
    #[cfg(test)]
  143    134   
    pub(super) fn local_addr(&self) -> Option<SocketAddr> {
  144    135   
        self.local_addr
  145    136   
    }
  146    137   
  147    138   
    /// Returns the connector-reported remote socket address.
  148    139   
    #[cfg(test)]
  149    140   
    pub(super) fn remote_addr(&self) -> Option<SocketAddr> {
  150    141   
        self.remote_addr
  151    142   
    }
  152    143   
  153    144   
    /// Returns whether the origin is reached through a proxy.
  154    145   
    pub(super) fn is_proxied(&self) -> bool {
  155    146   
        self.proxied
  156    147   
    }
  157    148   
  158    149   
    /// Copies connector-provided values into a response extension map.
  159    150   
    pub(super) fn apply_connector_extras(&self, extensions: &mut Extensions) {
  160    151   
        self.connected.get_extras(extensions);
  161    152   
    }
  162    153   
  163         -
    /// Builds Smithy metadata with close authority for this H1 generation.
         154  +
    /// Builds Smithy metadata with close authority for this H1 record.
  164    155   
    pub(super) fn metadata(&self, close: super::cell::h1::H1CloseHandle) -> ConnectionMetadata {
  165    156   
        let mut builder = ConnectionMetadata::builder()
  166    157   
            .proxied(self.proxied)
  167    158   
            .connection_id(self.id)
  168    159   
            .poison_fn(move || {
  169    160   
                close.close(CloseReason::Poisoned);
  170    161   
            });
  171    162   
        builder
  172    163   
            .set_local_addr(self.local_addr)
  173    164   
            .set_remote_addr(self.remote_addr);
  174    165   
        builder.build()
  175    166   
    }
  176    167   
         168  +
    /// Builds Smithy metadata with close authority for this H2 generation.
         169  +
    pub(super) fn h2_metadata(&self, close: super::cell::h2::H2CloseHandle) -> ConnectionMetadata {
         170  +
        let mut builder = ConnectionMetadata::builder()
         171  +
            .proxied(self.proxied)
         172  +
            .connection_id(self.id)
         173  +
            .poison_fn(move || {
         174  +
                close.close(CloseReason::Poisoned);
         175  +
            });
         176  +
        builder
         177  +
            .set_local_addr(self.local_addr)
         178  +
            .set_remote_addr(self.remote_addr);
         179  +
        builder.build()
         180  +
    }
         181  +
  177    182   
    /// Creates synthetic HTTP/1 information for state-machine tests.
  178    183   
    #[cfg(test)]
  179    184   
    pub(super) fn for_test(id: ConnectionId, owner_partition: PartitionId) -> Arc<Self> {
  180    185   
        Self::new(
  181    186   
            id,
  182    187   
            OriginKey::from_parts(http_1x::uri::Scheme::HTTPS, "example.com", None)
  183    188   
                .expect("synthetic test origin is valid"),
  184    189   
            owner_partition,
  185    190   
            NegotiatedProtocol::Http1,
  186    191   
            Connected::new(),
  187    192   
        )
  188    193   
    }
  189    194   
}
  190    195   
  191         -
/// Shared protocol-independent ownership for one installed connection.
         196  +
/// Shared connection ownership from negotiated transport through close.
  192    197   
pub(super) struct ConnectionState {
  193    198   
    /// Identity and transport facts shared with metadata and lifecycle events.
  194    199   
    info: Arc<ConnectionInfo>,
  195    200   
    /// Dispatch, logical-close, and root-I/O completion state.
  196    201   
    lifecycle: Mutex<LifecycleState>,
  197    202   
}
  198    203   
  199    204   
/// Connection lifetime state serialized with dispatch commitment and close.
  200    205   
#[derive(Debug)]
  201    206   
struct LifecycleState {
  202    207   
    /// Dispatch eligibility and ownership of bounded capacity.
  203    208   
    logical: LogicalState,
  204    209   
    /// Requests that committed before logical close.
  205    210   
    in_flight: usize,
  206    211   
    /// Whether ownership of root transport I/O has ended.
  207    212   
    physical_complete: bool,
  208    213   
}
  209    214   
  210    215   
/// Whether a connection may accept dispatch and still owns bounded capacity.
  211    216   
#[derive(Debug)]
  212    217   
enum LogicalState {
         218  +
    /// The connector returned connected I/O and selected the HTTP protocol,
         219  +
    /// but Hyper has not produced the request handle required for dispatch.
         220  +
    PendingOpen,
  213    221   
    /// Dispatch may commit.
  214    222   
    Open {
  215    223   
        /// Bounded-origin slot released by logical close, when configured.
  216    224   
        lease: Option<CapacityLease>,
  217    225   
    },
  218    226   
    /// New dispatch is rejected while accepted work may still drain.
  219    227   
    Closed {
  220    228   
        /// Reason recorded by the first logical-close transition.
  221    229   
        reason: CloseReason,
  222    230   
    },
  223    231   
}
  224    232   
  225    233   
impl ConnectionState {
         234  +
    /// Creates state after transport establishment and protocol selection.
         235  +
    ///
         236  +
    /// For TLS transports, the connector has completed TLS and ALPN before this
         237  +
    /// call. Hyper protocol setup and cell installation have not occurred.
         238  +
    ///
         239  +
    /// The returned guard is the unique physical-lifetime owner. After Hyper
         240  +
    /// returns a request handle, [`Self::open`] attaches optional bounded
         241  +
    /// capacity before cell installation makes the connection discoverable.
         242  +
    pub(super) fn pending_open(info: Arc<ConnectionInfo>) -> (Arc<Self>, PhysicalConnectionGuard) {
         243  +
        let connection = Arc::new(Self {
         244  +
            info,
         245  +
            lifecycle: Mutex::new(LifecycleState {
         246  +
                logical: LogicalState::PendingOpen,
         247  +
                in_flight: 0,
         248  +
                physical_complete: false,
         249  +
            }),
         250  +
        });
         251  +
        let physical = PhysicalConnectionGuard {
         252  +
            connection: connection.clone(),
         253  +
            active: true,
         254  +
        };
         255  +
        (connection, physical)
         256  +
    }
         257  +
         258  +
    /// Opens dispatch commitment and transfers optional bounded capacity.
         259  +
    ///
         260  +
    /// The caller performs this transition after Hyper protocol setup and
         261  +
    /// before publishing the connection through its cell. Opening first
         262  +
    /// ensures that a newly visible request handle can commit dispatch.
         263  +
    ///
         264  +
    /// Returns `lease` when logical close won before opening.
         265  +
    pub(super) fn open(&self, lease: Option<CapacityLease>) -> Result<(), Option<CapacityLease>> {
         266  +
        let mut lifecycle = self.lifecycle.lock();
         267  +
        if !matches!(lifecycle.logical, LogicalState::PendingOpen) {
         268  +
            return Err(lease);
         269  +
        }
         270  +
        lifecycle.logical = LogicalState::Open { lease };
         271  +
        Ok(())
         272  +
    }
         273  +
  226    274   
    /// Creates a connection whose origin has no admission bound.
  227    275   
    ///
  228    276   
    /// The returned guard is the unique physical-lifetime owner and must move
  229    277   
    /// with the root I/O task.
         278  +
    #[cfg(test)]
  230    279   
    pub(super) fn unbounded(info: Arc<ConnectionInfo>) -> (Arc<Self>, PhysicalConnectionGuard) {
  231         -
        Self::new(info, None)
         280  +
        let (connection, physical) = Self::pending_open(info);
         281  +
        connection
         282  +
            .open(None)
         283  +
            .expect("new unbounded connection could not open");
         284  +
        (connection, physical)
  232    285   
    }
  233    286   
  234    287   
    /// Creates a connection that takes ownership of one bounded-origin slot.
  235    288   
    ///
  236    289   
    /// The returned guard is the unique physical-lifetime owner and must move
  237    290   
    /// with the root I/O task. Logical close returns `lease` independently of
  238    291   
    /// that guard.
         292  +
    #[cfg(test)]
  239    293   
    pub(super) fn bounded(
  240    294   
        info: Arc<ConnectionInfo>,
  241    295   
        lease: CapacityLease,
  242    296   
    ) -> (Arc<Self>, PhysicalConnectionGuard) {
  243         -
        Self::new(info, Some(lease))
  244         -
    }
  245         -
  246         -
    /// Builds shared connection state and its unique physical-lifetime guard.
  247         -
    fn new(
  248         -
        info: Arc<ConnectionInfo>,
  249         -
        lease: Option<CapacityLease>,
  250         -
    ) -> (Arc<Self>, PhysicalConnectionGuard) {
  251         -
        let connection = Arc::new(Self {
  252         -
            info,
  253         -
            lifecycle: Mutex::new(LifecycleState {
  254         -
                logical: LogicalState::Open { lease },
  255         -
                in_flight: 0,
  256         -
                physical_complete: false,
  257         -
            }),
  258         -
        });
  259         -
        let physical = PhysicalConnectionGuard {
  260         -
            connection: connection.clone(),
  261         -
            active: true,
  262         -
        };
         297  +
        let (connection, physical) = Self::pending_open(info);
         298  +
        connection
         299  +
            .open(Some(lease))
         300  +
            .expect("new bounded connection could not open");
  263    301   
        (connection, physical)
  264    302   
    }
  265    303   
  266    304   
    /// Returns this connection's stable identity.
  267    305   
    pub(super) fn id(&self) -> ConnectionId {
  268    306   
        self.info.id()
  269    307   
    }
  270    308   
  271    309   
    /// Returns the partition that owns this connection's I/O and driver.
  272    310   
    pub(super) fn owner_partition(&self) -> PartitionId {
  273    311   
        self.info.owner_partition()
  274    312   
    }
  275    313   
  276    314   
    /// Returns immutable identity and transport facts for this connection.
  277    315   
    pub(super) fn info(&self) -> &Arc<ConnectionInfo> {
  278    316   
        &self.info
  279    317   
    }
  280    318   
  281    319   
    /// Attempts to commit one request against logical close.
  282    320   
    ///
  283    321   
    /// Returns a guard and increments the in-flight count while the connection
  284    322   
    /// is open. Returns `None` without changing state after logical close.
  285    323   
    pub(super) fn try_commit_dispatch(connection: &Arc<Self>) -> Option<DispatchGuard> {
  286    324   
        let mut lifecycle = connection.lifecycle.lock();
  287    325   
        if !matches!(lifecycle.logical, LogicalState::Open { .. }) {
  288    326   
            return None;
  289    327   
        }
  290    328   
        lifecycle.in_flight = lifecycle
  291    329   
            .in_flight
  292    330   
            .checked_add(1)
  293    331   
            .expect("in-flight dispatch count exhausted");
  294    332   
        drop(lifecycle);
  295    333   
  296    334   
        Some(DispatchGuard {
  297    335   
            connection: connection.clone(),
  298    336   
            active: true,
  299    337   
        })
  300    338   
    }
  301    339   
  302    340   
    /// Performs the first logical-close transition.
  303    341   
    ///
  304    342   
    /// Returns `true` when this call closes the connection and records
  305    343   
    /// `reason`. Returns `false` when another close already won; the original
  306    344   
    /// reason remains unchanged.
  307    345   
    ///
  308    346   
    /// The detached lease is dropped only after the connection lock is
  309    347   
    /// released, so admission and connection locks are never nested.
  310    348   
    pub(super) fn logical_close(&self, reason: CloseReason) -> bool {
  311    349   
        let lease = {
  312    350   
            let mut lifecycle = self.lifecycle.lock();
  313         -
            let LogicalState::Open { lease } = &mut lifecycle.logical else {
  314         -
                return false;
  315         -
            };
         351  +
            match &mut lifecycle.logical {
         352  +
                LogicalState::PendingOpen => {
         353  +
                    lifecycle.logical = LogicalState::Closed { reason };
         354  +
                    None
         355  +
                }
         356  +
                LogicalState::Open { lease } => {
  316    357   
                    let lease = lease.take();
  317    358   
                    lifecycle.logical = LogicalState::Closed { reason };
  318    359   
                    lease
         360  +
                }
         361  +
                LogicalState::Closed { .. } => return false,
         362  +
            }
  319    363   
        };
  320    364   
        drop(lease);
  321    365   
        tracing::debug!(
  322    366   
            connection_id = %self.id(),
  323    367   
            connection_partition = ?self.owner_partition(),
         368  +
            protocol = ?self.info.protocol(),
  324    369   
            origin_scheme = %self.info.origin().scheme(),
  325    370   
            origin_host = self.info.origin().host(),
  326    371   
            origin_port = ?self.info.origin().port(),
  327    372   
            close_reason = ?reason,
  328    373   
            "connection logically closed"
  329    374   
        );
  330    375   
        true
  331    376   
    }
  332    377   
  333    378   
    /// Refines a driver-observed close after Hyper confirms an upgrade.
  334    379   
    ///
  335    380   
    /// Hyper may transfer upgraded I/O and complete its HTTP/1 driver in one
  336    381   
    /// poll. The driver can therefore record [`CloseReason::ProtocolClosed`]
  337    382   
    /// before the request task observes the upgrading response. This method
  338    383   
    /// changes that stored reason to [`CloseReason::Upgraded`].
  339    384   
    ///
  340    385   
    /// Returns `true` only when that refinement was applied. It does not close
  341    386   
    /// the connection again, change I/O ownership, or release capacity a second
  342    387   
    /// time.
  343    388   
    pub(super) fn refine_protocol_close_as_upgrade(&self) -> bool {
  344    389   
        let refined = {
  345    390   
            let mut lifecycle = self.lifecycle.lock();
  346    391   
            match &mut lifecycle.logical {
  347    392   
                LogicalState::Closed { reason } if *reason == CloseReason::ProtocolClosed => {
  348    393   
                    *reason = CloseReason::Upgraded;
  349    394   
                    true
  350    395   
                }
  351    396   
                _ => false,
  352    397   
            }
  353    398   
        };
  354    399   
        if refined {
  355    400   
            tracing::debug!(
  356    401   
                connection_id = %self.id(),
  357    402   
                connection_partition = ?self.owner_partition(),
  358    403   
                origin_scheme = %self.info.origin().scheme(),
  359    404   
                origin_host = self.info.origin().host(),
  360    405   
                origin_port = ?self.info.origin().port(),
  361    406   
                previous_close_reason = ?CloseReason::ProtocolClosed,
  362    407   
                close_reason = ?CloseReason::Upgraded,
  363    408   
                "refined connection close reason after HTTP/1 upgrade"
  364    409   
            );
  365    410   
        }
  366    411   
        refined
  367    412   
    }
  368    413   
  369    414   
    /// Verifies a close reason at a protocol handoff boundary in debug builds.
  370    415   
    #[cfg(debug_assertions)]
  371    416   
    pub(super) fn debug_assert_close_reason(&self, expected: CloseReason) {
  372    417   
        let lifecycle = self.lifecycle.lock();
  373    418   
        let actual = match lifecycle.logical {
         419  +
            LogicalState::PendingOpen => None,
  374    420   
            LogicalState::Open { .. } => None,
  375    421   
            LogicalState::Closed { reason } => Some(reason),
  376    422   
        };
  377    423   
        debug_assert_eq!(Some(expected), actual);
  378    424   
    }
  379    425   
  380    426   
    /// Removes one dispatch previously committed by [`Self::try_commit_dispatch`].
  381    427   
    fn finish_dispatch(&self) {
  382    428   
        let mut lifecycle = self.lifecycle.lock();
  383    429   
        lifecycle.in_flight = lifecycle
  384    430   
            .in_flight
  385    431   
            .checked_sub(1)
  386    432   
            .expect("completed a dispatch that was not in flight");
  387    433   
    }
  388    434   
  389    435   
    /// Records that the connection's root I/O is no longer live.
  390    436   
    ///
  391    437   
    /// # Panics
  392    438   
    ///
  393    439   
    /// Panics if physical ownership completes more than once.
  394    440   
    fn finish_physical(&self) {
  395    441   
        {
  396    442   
            let mut lifecycle = self.lifecycle.lock();
  397    443   
            assert!(
  398    444   
                !lifecycle.physical_complete,
  399    445   
                "physical connection ownership completed more than once"
  400    446   
            );
  401    447   
            lifecycle.physical_complete = true;
  402    448   
        }
  403    449   
        tracing::debug!(
  404    450   
            connection_id = %self.id(),
  405    451   
            connection_partition = ?self.owner_partition(),
  406    452   
            origin_scheme = %self.info.origin().scheme(),
  407    453   
            origin_host = self.info.origin().host(),
  408    454   
            origin_port = ?self.info.origin().port(),
  409    455   
            "connection root I/O ownership ended"
  410    456   
        );
  411    457   
    }
  412    458   
  413    459   
    /// Returns a consistent lifecycle snapshot.
  414    460   
    #[cfg(test)]
  415    461   
    pub(super) fn snapshot(&self) -> ConnectionSnapshot {
  416    462   
        let lifecycle = self.lifecycle.lock();
  417    463   
        ConnectionSnapshot {
  418    464   
            close_reason: match lifecycle.logical {
         465  +
                LogicalState::PendingOpen => None,
  419    466   
                LogicalState::Open { .. } => None,
  420    467   
                LogicalState::Closed { reason } => Some(reason),
  421    468   
            },
  422    469   
            in_flight: lifecycle.in_flight,
  423    470   
            physical_complete: lifecycle.physical_complete,
  424    471   
        }
  425    472   
    }
  426    473   
}
  427    474   
  428    475   
impl fmt::Debug for ConnectionState {

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

@@ -1,1 +0,530 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Request routing and protocol dispatch.
    7      7   
//!
    8      8   
//! This module is the request entry point after a [`super::Client`] has
    9      9   
//! resolved its partition. It rejects unsupported request forms, resolves the
   10         -
//! origin cell and partition runtime, and starts partition maintenance before
   11         -
//! handing the request to a protocol-specific dispatcher. Connection
   12         -
//! acquisition, wire-form preparation, and response ownership live in child
   13         -
//! modules.
          10  +
//! origin cell and partition runtime, and owns acquisition across protocol
          11  +
//! selection. Child modules prepare protocol wire forms and retain response
          12  +
//! ownership.
   14     13   
   15     14   
mod h1;
          15  +
mod h2;
   16     16   
   17         -
use super::cell::OriginCell;
   18         -
use super::establish::TransportTimeout;
          17  +
use self::h1::H1DispatchResult;
          18  +
use self::h2::H2DispatchResult;
          19  +
use super::admission::ProtocolRequirement;
          20  +
use super::cell::{AcquisitionEvent, AcquisitionResult, OriginCell, WaiterId};
          21  +
use super::establish::{self, TransportTimeout};
   19     22   
use super::partition::DriverSpawner;
   20     23   
use super::registry::PartitionState;
   21     24   
use super::{ConnectionPool, PoolInner};
   22     25   
use crate::sync::Arc;
   23     26   
use aws_smithy_runtime_api::client::result::ConnectorError;
   24     27   
use aws_smithy_types::body::SdkBody;
   25         -
use http_1x::{Method, Request, Response, Uri, Version};
          28  +
use http_1x::{header, Method, Request, Response, Uri, Version};
          29  +
use std::future::poll_fn;
   26     30   
use std::sync::Arc as StdArc;
   27     31   
          32  +
/// Replacement selections allowed after the initial HTTP/2 dispatch attempt.
          33  +
const MAX_H2_REACQUISITIONS: usize = 2;
          34  +
   28     35   
/// Stable state shared by acquisition and protocol dispatch for one request.
   29     36   
///
   30     37   
/// This context owns ordinary shared configuration and identities. Linear
   31     38   
/// acquisition values, such as an establishment permit or an exclusive
   32     39   
/// protocol handle, remain separate arguments so their transfer is explicit.
   33     40   
#[derive(Clone)]
   34     41   
pub(super) struct AcquisitionContext {
   35     42   
    /// Shared pool policy and transport construction.
   36     43   
    pub(super) pool: Arc<PoolInner>,
   37     44   
    /// Partition from which the request was issued.
   38     45   
    pub(super) partition: Arc<PartitionState>,
   39     46   
    /// Partition-local state for the request's canonical origin.
   40     47   
    pub(super) cell: Arc<OriginCell>,
   41     48   
    /// Absolute request URI retained across protocol wire-form changes.
   42     49   
    pub(super) absolute_uri: Uri,
   43     50   
    /// Runtime that owns establishment, drivers, and pending return work.
   44     51   
    pub(super) owner_spawner: StdArc<dyn DriverSpawner>,
   45     52   
    /// Transport connection timeout configured for this operation.
   46     53   
    pub(super) connect_timeout: Option<TransportTimeout>,
   47     54   
}
   48     55   
   49     56   
impl ConnectionPool {
   50     57   
    /// Resolves common acquisition state and dispatches one request.
   51     58   
    pub(super) async fn send_request(
   52     59   
        &self,
   53     60   
        partition: Arc<PartitionState>,
   54     61   
        request: Request<SdkBody>,
   55     62   
        connect_timeout: Option<TransportTimeout>,
   56     63   
    ) -> Result<Response<SdkBody>, ConnectorError> {
   57     64   
        validate_request_before_acquisition(&request)
   58     65   
            .map_err(|error| ConnectorError::user(error.into()))?;
   59     66   
   60     67   
        let absolute_uri = request.uri().clone();
   61     68   
        let cell = self
   62     69   
            .inner
   63     70   
            .registry
   64     71   
            .resolve_cell(&partition, &absolute_uri)
   65     72   
            .map_err(|error| ConnectorError::user(error.into()))?;
   66     73   
        tracing::trace!(
   67     74   
            request_partition = ?partition.id(),
   68     75   
            origin_scheme = %cell.id().origin().scheme(),
   69     76   
            origin_host = cell.id().origin().host(),
   70     77   
            origin_port = ?cell.id().origin().port(),
   71     78   
            "request resolved to connection-pool cell"
   72     79   
        );
   73     80   
   74     81   
        let owner_spawner = partition
   75     82   
            .owner_spawner()
   76     83   
            .map_err(|error| ConnectorError::user(error.into()))?;
   77     84   
        partition.ensure_maintenance_started(owner_spawner.as_ref());
   78     85   
   79     86   
        let context = AcquisitionContext {
   80     87   
            pool: self.inner.clone(),
   81     88   
            partition,
   82     89   
            cell,
   83     90   
            absolute_uri,
   84     91   
            owner_spawner,
   85     92   
            connect_timeout,
   86     93   
        };
   87     94   
   88         -
        self.send_h1_request(context, request).await
          95  +
        self.dispatch_acquired(context, request).await
          96  +
    }
          97  +
          98  +
    /// Acquires a compatible protocol value and dispatches the request.
          99  +
    ///
         100  +
    /// Reacquisition is allowed only while a protocol dispatcher returns the
         101  +
    /// original request envelope.
         102  +
    async fn dispatch_acquired(
         103  +
        &self,
         104  +
        context: AcquisitionContext,
         105  +
        mut request: Request<SdkBody>,
         106  +
    ) -> Result<Response<SdkBody>, ConnectorError> {
         107  +
        let requirement = protocol_requirement(&request);
         108  +
        let mut h2_reacquisitions = H2ReacquisitionBudget::default();
         109  +
         110  +
        loop {
         111  +
            match Self::acquire(&context, requirement).await? {
         112  +
                AcquisitionResult::H1(selection) => {
         113  +
                    match self.dispatch_h1(&context, request, selection).await? {
         114  +
                        H1DispatchResult::Response(response) => return Ok(response),
         115  +
                        H1DispatchResult::Reacquire(returned) => request = returned,
         116  +
                    }
         117  +
                }
         118  +
                AcquisitionResult::H2(activation) => {
         119  +
                    match self.dispatch_h2(&context, request, activation).await? {
         120  +
                        H2DispatchResult::Response(response) => return Ok(response),
         121  +
                        H2DispatchResult::Reacquire(reacquisition) => {
         122  +
                            let (returned, error) = reacquisition.into_parts();
         123  +
                            if !h2_reacquisitions.admit_replacement() {
         124  +
                                return Err(error);
         125  +
                            }
         126  +
                            request = returned;
         127  +
                        }
         128  +
                    }
         129  +
                }
         130  +
                AcquisitionResult::Failed(_) => {
         131  +
                    unreachable!("failed acquisition returned as a successful result")
         132  +
                }
         133  +
                AcquisitionResult::Reacquire => {
         134  +
                    unreachable!("internal reacquisition escaped the acquisition loop")
         135  +
                }
         136  +
            }
         137  +
        }
         138  +
    }
         139  +
         140  +
    /// Acquires one protocol value compatible with the request.
         141  +
    ///
         142  +
    /// Local reusable values complete immediately. Otherwise one waiter
         143  +
    /// remains registered while it receives a reusable protocol value, an
         144  +
    /// establishment failure, or authority to start establishment.
         145  +
    async fn acquire(
         146  +
        context: &AcquisitionContext,
         147  +
        requirement: ProtocolRequirement,
         148  +
    ) -> Result<AcquisitionResult, ConnectorError> {
         149  +
        'acquire: loop {
         150  +
            if requirement.accepts_h2() {
         151  +
                if let Some(activation) = OriginCell::select_h2(&context.cell) {
         152  +
                    tracing::trace!(
         153  +
                        connection_id = %activation.connection().id(),
         154  +
                        request_partition = ?context.partition.id(),
         155  +
                        connection_partition = ?activation.connection().owner_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  +
                        "HTTP/2 pool hit; activating local generation"
         160  +
                    );
         161  +
                    return Ok(AcquisitionResult::H2(activation));
         162  +
                }
         163  +
            }
         164  +
            if requirement.accepts_h1() {
         165  +
                if let Some(selection) = OriginCell::select_h1(&context.cell) {
         166  +
                    tracing::trace!(
         167  +
                        connection_id = %selection.connection_id(),
         168  +
                        request_partition = ?context.partition.id(),
         169  +
                        connection_partition = ?selection.connection().owner_partition(),
         170  +
                        origin_scheme = %selection.connection().info().origin().scheme(),
         171  +
                        origin_host = selection.connection().info().origin().host(),
         172  +
                        origin_port = ?selection.connection().info().origin().port(),
         173  +
                        "HTTP/1 pool hit; reusing idle connection"
         174  +
                    );
         175  +
                    return Ok(AcquisitionResult::H1(selection));
         176  +
                }
         177  +
            }
         178  +
         179  +
            let waiter = OriginCell::register_waiter(&context.cell, requirement);
         180  +
            tracing::trace!(
         181  +
                request_partition = ?context.partition.id(),
         182  +
                origin_scheme = %context.cell.id().origin().scheme(),
         183  +
                origin_host = context.cell.id().origin().host(),
         184  +
                origin_port = ?context.cell.id().origin().port(),
         185  +
                protocol_requirement = ?requirement,
         186  +
                "connection acquisition queued"
         187  +
            );
         188  +
            let mut waiter_guard = WaiterGuard::new(context.cell.clone(), waiter);
         189  +
            loop {
         190  +
                match poll_fn(|cx| context.cell.poll_waiter(waiter, cx)).await {
         191  +
                    AcquisitionEvent::Complete(AcquisitionResult::H1(selection)) => {
         192  +
                        waiter_guard.disarm();
         193  +
                        return Ok(AcquisitionResult::H1(selection));
         194  +
                    }
         195  +
                    AcquisitionEvent::Complete(AcquisitionResult::H2(activation)) => {
         196  +
                        waiter_guard.disarm();
         197  +
                        OriginCell::service_h2_waiters(&context.cell);
         198  +
                        OriginCell::service_peer_h2_waiters(&context.cell);
         199  +
                        tracing::trace!(
         200  +
                            connection_id = %activation.connection().id(),
         201  +
                            request_partition = ?context.partition.id(),
         202  +
                            connection_partition = ?activation.connection().owner_partition(),
         203  +
                            origin_scheme = %context.cell.id().origin().scheme(),
         204  +
                            origin_host = context.cell.id().origin().host(),
         205  +
                            origin_port = ?context.cell.id().origin().port(),
         206  +
                            "HTTP/2 acquisition completed"
         207  +
                        );
         208  +
                        return Ok(AcquisitionResult::H2(activation));
         209  +
                    }
         210  +
                    AcquisitionEvent::Complete(AcquisitionResult::Failed(error)) => {
         211  +
                        waiter_guard.disarm();
         212  +
                        return Err(error);
         213  +
                    }
         214  +
                    AcquisitionEvent::Complete(AcquisitionResult::Reacquire) => {
         215  +
                        waiter_guard.disarm();
         216  +
                        continue 'acquire;
         217  +
                    }
         218  +
                    AcquisitionEvent::Establish(permit) => {
         219  +
                        tracing::trace!(
         220  +
                            request_partition = ?context.partition.id(),
         221  +
                            origin_scheme = %context.cell.id().origin().scheme(),
         222  +
                            origin_host = context.cell.id().origin().host(),
         223  +
                            origin_port = ?context.cell.id().origin().port(),
         224  +
                            "connection establishment starting"
         225  +
                        );
         226  +
                        let attempt =
         227  +
                            establish::establish(context.clone(), waiter, permit, requirement);
         228  +
                        let completion =
         229  +
                            EstablishmentCompletionGuard::new(context.cell.clone(), waiter);
         230  +
                        context.owner_spawner.spawn(Box::pin(async move {
         231  +
                            let mut completion = completion;
         232  +
                            if !completion.start() {
         233  +
                                drop(attempt);
         234  +
                                completion.disarm();
         235  +
                                return;
         236  +
                            }
         237  +
                            match attempt.await {
         238  +
                                establish::EstablishmentOutcome::Complete(result) => {
         239  +
                                    completion.complete(result);
         240  +
                                }
         241  +
                                establish::EstablishmentOutcome::Transferred => completion.disarm(),
         242  +
                            }
         243  +
                        }));
         244  +
                    }
         245  +
                }
         246  +
            }
         247  +
        }
         248  +
    }
         249  +
}
         250  +
         251  +
/// Per-request bound on replacement HTTP/2 selections.
         252  +
#[derive(Default)]
         253  +
struct H2ReacquisitionBudget {
         254  +
    /// Replacement selections admitted after the initial selection.
         255  +
    completed: usize,
         256  +
}
         257  +
         258  +
impl H2ReacquisitionBudget {
         259  +
    /// Returns whether one more replacement selection may proceed.
         260  +
    fn admit_replacement(&mut self) -> bool {
         261  +
        if self.completed >= MAX_H2_REACQUISITIONS {
         262  +
            return false;
         263  +
        }
         264  +
        self.completed += 1;
         265  +
        true
         266  +
    }
         267  +
}
         268  +
         269  +
/// Guarantees a terminal result for one submitted establishment attempt.
         270  +
///
         271  +
/// The guard enters the owner-runtime future before submission. Runtime task
         272  +
/// drop reports failure to the waiter. A result that wins before the submitted
         273  +
/// future starts disarms the guard without polling the connector.
         274  +
struct EstablishmentCompletionGuard {
         275  +
    cell: Arc<OriginCell>,
         276  +
    waiter: WaiterId,
         277  +
    active: bool,
         278  +
}
         279  +
         280  +
impl EstablishmentCompletionGuard {
         281  +
    fn new(cell: Arc<OriginCell>, waiter: WaiterId) -> Self {
         282  +
        Self {
         283  +
            cell,
         284  +
            waiter,
         285  +
            active: true,
         286  +
        }
         287  +
    }
         288  +
         289  +
    fn start(&self) -> bool {
         290  +
        self.cell.start_establishment(self.waiter)
         291  +
    }
         292  +
         293  +
    fn complete(mut self, result: AcquisitionResult) {
         294  +
        self.active = false;
         295  +
        self.cell.complete_establishment(self.waiter, result);
         296  +
    }
         297  +
         298  +
    fn disarm(&mut self) {
         299  +
        self.active = false;
         300  +
    }
         301  +
}
         302  +
         303  +
impl Drop for EstablishmentCompletionGuard {
         304  +
    fn drop(&mut self) {
         305  +
        if self.active {
         306  +
            let error = ConnectorError::io(EstablishmentTaskDropped.into());
         307  +
            self.cell
         308  +
                .complete_establishment(self.waiter, AcquisitionResult::Failed(error));
         309  +
            tracing::debug!(
         310  +
                request_partition = ?self.cell.id().partition(),
         311  +
                connection_partition = ?self.cell.id().partition(),
         312  +
                origin_scheme = %self.cell.id().origin().scheme(),
         313  +
                origin_host = self.cell.id().origin().host(),
         314  +
                origin_port = ?self.cell.id().origin().port(),
         315  +
                "connection establishment task dropped"
         316  +
            );
         317  +
        }
         318  +
    }
         319  +
}
         320  +
         321  +
/// The owner runtime discarded an establishment future before completion.
         322  +
#[derive(Debug)]
         323  +
struct EstablishmentTaskDropped;
         324  +
         325  +
impl std::fmt::Display for EstablishmentTaskDropped {
         326  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         327  +
        f.write_str("the owner-runtime connection establishment task was dropped")
         328  +
    }
         329  +
}
         330  +
         331  +
impl std::error::Error for EstablishmentTaskDropped {}
         332  +
         333  +
/// Cancels a request's waiter until it consumes a terminal acquisition result.
         334  +
struct WaiterGuard {
         335  +
    cell: Arc<OriginCell>,
         336  +
    waiter: WaiterId,
         337  +
    active: bool,
         338  +
}
         339  +
         340  +
impl WaiterGuard {
         341  +
    fn new(cell: Arc<OriginCell>, waiter: WaiterId) -> Self {
         342  +
        Self {
         343  +
            cell,
         344  +
            waiter,
         345  +
            active: true,
         346  +
        }
         347  +
    }
         348  +
         349  +
    fn disarm(&mut self) {
         350  +
        self.active = false;
         351  +
    }
         352  +
}
         353  +
         354  +
impl Drop for WaiterGuard {
         355  +
    fn drop(&mut self) {
         356  +
        if self.active {
         357  +
            OriginCell::cancel_waiter(&self.cell, self.waiter);
         358  +
        }
   89    359   
    }
   90    360   
}
   91    361   
   92    362   
/// Rejects request forms the pool cannot route before acquiring connection state.
   93    363   
fn validate_request_before_acquisition(
   94    364   
    request: &Request<SdkBody>,
   95    365   
) -> Result<(), RequestDispatchError> {
         366  +
    if request.version() == Version::HTTP_2 && request.method() == Method::CONNECT {
         367  +
        return Err(RequestDispatchError::ExtendedConnectUnsupported);
         368  +
    }
         369  +
    if request.version() == Version::HTTP_2 && has_upgrade_semantics(request) {
         370  +
        return Err(RequestDispatchError::UpgradeOverHttp2);
         371  +
    }
   96    372   
    match request.version() {
   97    373   
        Version::HTTP_11 | Version::HTTP_2 => Ok(()),
   98    374   
        Version::HTTP_10 if request.method() == Method::CONNECT => {
   99    375   
            Err(RequestDispatchError::ConnectOverHttp10)
  100    376   
        }
  101    377   
        Version::HTTP_10 => Ok(()),
  102    378   
        version => Err(RequestDispatchError::UnsupportedVersion(version)),
  103    379   
    }
  104    380   
}
  105    381   
         382  +
/// Returns the protocol capability needed to preserve the request's wire semantics.
         383  +
fn protocol_requirement(request: &Request<SdkBody>) -> ProtocolRequirement {
         384  +
    if request.version() == Version::HTTP_2 {
         385  +
        ProtocolRequirement::H2Required
         386  +
    } else if request.version() == Version::HTTP_10
         387  +
        || request.method() == Method::CONNECT
         388  +
        || has_upgrade_semantics(request)
         389  +
    {
         390  +
        ProtocolRequirement::H1Required
         391  +
    } else {
         392  +
        ProtocolRequirement::H1Compatible
         393  +
    }
         394  +
}
         395  +
         396  +
/// Returns whether the request asks HTTP/1 to switch protocols.
         397  +
fn has_upgrade_semantics(request: &Request<SdkBody>) -> bool {
         398  +
    request.headers().contains_key(header::UPGRADE)
         399  +
        || request
         400  +
            .headers()
         401  +
            .get_all(header::CONNECTION)
         402  +
            .iter()
         403  +
            .any(|value| {
         404  +
                value.to_str().is_ok_and(|value| {
         405  +
                    value
         406  +
                        .split(',')
         407  +
                        .any(|token| token.trim().eq_ignore_ascii_case("upgrade"))
         408  +
                })
         409  +
            })
         410  +
}
         411  +
         412  +
/// Marker for a `Host` header synthesized during an HTTP/1 dispatch attempt.
         413  +
#[derive(Clone, Copy, Debug)]
         414  +
struct H1HostHeaderInserted;
         415  +
  106    416   
/// A request rejected before Hyper accepts it for dispatch.
  107    417   
#[derive(Debug)]
  108    418   
enum RequestDispatchError {
  109    419   
    /// The absolute URI did not contain the authority required for `Host`.
  110    420   
    MissingAuthority,
  111    421   
    /// The derived `Host` value was not a valid HTTP header value.
  112    422   
    InvalidHostHeader(http_1x::header::InvalidHeaderValue),
  113    423   
    /// HTTP/1.0 cannot represent a CONNECT request accepted by this client.
  114    424   
    ConnectOverHttp10,
  115    425   
    /// The request selected an HTTP version this client cannot dispatch.
  116    426   
    UnsupportedVersion(Version),
         427  +
    /// Extended CONNECT requires HTTP/2 stream-lifecycle support.
         428  +
    ExtendedConnectUnsupported,
         429  +
    /// HTTP/1 upgrade semantics cannot be represented by this HTTP/2 path.
         430  +
    UpgradeOverHttp2,
  117    431   
}
  118    432   
  119    433   
impl std::fmt::Display for RequestDispatchError {
  120    434   
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  121    435   
        match self {
  122    436   
            Self::MissingAuthority => f.write_str("request URI must contain an authority"),
  123    437   
            Self::InvalidHostHeader(_) => {
  124    438   
                f.write_str("request URI authority is not a valid Host header")
  125    439   
            }
  126    440   
            Self::ConnectOverHttp10 => f.write_str("CONNECT is not supported over HTTP/1.0"),
  127    441   
            Self::UnsupportedVersion(version) => {
  128    442   
                write!(f, "unsupported HTTP version: {version:?}")
  129    443   
            }
         444  +
            Self::ExtendedConnectUnsupported => {
         445  +
                f.write_str("extended CONNECT over HTTP/2 is not supported")
         446  +
            }
         447  +
            Self::UpgradeOverHttp2 => {
         448  +
                f.write_str("HTTP/1 protocol upgrade semantics cannot use HTTP/2")
         449  +
            }
  130    450   
        }
  131    451   
    }
  132    452   
}
  133    453   
  134    454   
impl std::error::Error for RequestDispatchError {
  135    455   
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
  136    456   
        match self {
  137    457   
            Self::InvalidHostHeader(error) => Some(error),
  138    458   
            _ => None,
  139    459   
        }
  140    460   
    }
  141    461   
}
         462  +
         463  +
#[cfg(all(test, not(smithy_http_client_loom)))]
         464  +
mod tests {
         465  +
    use super::*;
         466  +
         467  +
    fn request(method: Method, version: Version) -> Request<SdkBody> {
         468  +
        Request::builder()
         469  +
            .method(method)
         470  +
            .uri("https://example.com/resource")
         471  +
            .version(version)
         472  +
            .body(SdkBody::empty())
         473  +
            .unwrap()
         474  +
    }
         475  +
         476  +
    #[test]
         477  +
    fn request_semantics_select_protocol_capability() {
         478  +
        assert_eq!(
         479  +
            ProtocolRequirement::H1Compatible,
         480  +
            protocol_requirement(&request(Method::GET, Version::HTTP_11))
         481  +
        );
         482  +
        assert_eq!(
         483  +
            ProtocolRequirement::H2Required,
         484  +
            protocol_requirement(&request(Method::GET, Version::HTTP_2))
         485  +
        );
         486  +
        assert_eq!(
         487  +
            ProtocolRequirement::H1Required,
         488  +
            protocol_requirement(&request(Method::GET, Version::HTTP_10))
         489  +
        );
         490  +
        assert_eq!(
         491  +
            ProtocolRequirement::H1Required,
         492  +
            protocol_requirement(&request(Method::CONNECT, Version::HTTP_11))
         493  +
        );
         494  +
         495  +
        let mut upgrade = request(Method::GET, Version::HTTP_11);
         496  +
        upgrade
         497  +
            .headers_mut()
         498  +
            .insert(header::CONNECTION, "keep-alive, Upgrade".parse().unwrap());
         499  +
        assert_eq!(
         500  +
            ProtocolRequirement::H1Required,
         501  +
            protocol_requirement(&upgrade)
         502  +
        );
         503  +
    }
         504  +
         505  +
    #[test]
         506  +
    fn unsupported_h2_tunnel_and_upgrade_forms_are_rejected() {
         507  +
        assert!(matches!(
         508  +
            validate_request_before_acquisition(&request(Method::CONNECT, Version::HTTP_2)),
         509  +
            Err(RequestDispatchError::ExtendedConnectUnsupported)
         510  +
        ));
         511  +
         512  +
        let mut upgrade = request(Method::GET, Version::HTTP_2);
         513  +
        upgrade
         514  +
            .headers_mut()
         515  +
            .insert(header::UPGRADE, "websocket".parse().unwrap());
         516  +
        assert!(matches!(
         517  +
            validate_request_before_acquisition(&upgrade),
         518  +
            Err(RequestDispatchError::UpgradeOverHttp2)
         519  +
        ));
         520  +
    }
         521  +
         522  +
    #[test]
         523  +
    fn h2_reacquisition_is_bounded_after_two_replacements() {
         524  +
        let mut budget = H2ReacquisitionBudget::default();
         525  +
        for _ in 0..MAX_H2_REACQUISITIONS {
         526  +
            assert!(budget.admit_replacement());
         527  +
        }
         528  +
        assert!(!budget.admit_replacement());
         529  +
    }
         530  +
}

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

@@ -1,1 +374,177 @@
    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         -
//! HTTP/1 acquisition, exclusive dispatch, and response completion.
           6  +
//! HTTP/1 wire preparation, exclusive dispatch, and response completion.
    7      7   
//!
    8      8   
//! The parent dispatcher has already validated the request, resolved its
    9         -
//! origin cell, and selected the partition runtime. This module prepares the
   10         -
//! HTTP/1 wire form, acquires Hyper's exclusive request sender, and owns that
           9  +
//! origin cell, acquired an HTTP/1 selection, and selected the partition
          10  +
//! runtime. This module prepares the HTTP/1 wire form and owns the exclusive
   11     11   
//! sender until the response reaches a reusable message boundary. Every
   12         -
//! cancellation path either returns the sender to its connection-owning cell
   13         -
//! or retires the connection.
          12  +
//! cancellation path returns the sender to its connection cell or retires the
          13  +
//! connection.
   14     14   
   15         -
use super::super::admission::ProtocolRequirement;
   16     15   
use super::super::cell::h1::{H1Exchange, H1Selection};
   17         -
use super::super::cell::{AcquisitionEvent, AcquisitionResult, OriginCell, WaiterId};
   18     16   
use super::super::connection::{CloseReason, ConnectionState, DispatchGuard};
   19         -
use super::super::establish;
   20     17   
use super::super::partition::DriverSpawner;
   21     18   
use super::super::ConnectionPool;
   22         -
use super::{AcquisitionContext, RequestDispatchError};
   23         -
use crate::sync::Arc;
          19  +
use super::{AcquisitionContext, H1HostHeaderInserted, RequestDispatchError};
   24     20   
use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
   25     21   
use aws_smithy_runtime_api::client::result::ConnectorError;
   26     22   
use aws_smithy_types::body::SdkBody;
   27     23   
use http_1x::{Method, Request, Response, Uri, Version};
   28     24   
use hyper::body::Body;
   29     25   
use std::future::poll_fn;
   30     26   
use std::pin::Pin;
   31     27   
use std::sync::Arc as StdArc;
   32     28   
use std::task::{Context, Poll};
   33     29   
          30  +
/// Result of one HTTP/1 dispatch attempt while the pool owns the request.
          31  +
pub(super) enum H1DispatchResult {
          32  +
    /// Hyper accepted the request and produced a guarded response.
          33  +
    Response(Response<SdkBody>),
          34  +
    /// The selection became stale or Hyper returned the request unsent.
          35  +
    Reacquire(Request<SdkBody>),
          36  +
}
          37  +
   34     38   
impl ConnectionPool {
   35         -
    /// Acquires an HTTP/1 connection and sends one request.
          39  +
    /// Attempts one dispatch through an acquired HTTP/1 selection.
   36     40   
    ///
   37         -
    /// The reacquisition loop runs only while the pool still owns the request. A
   38         -
    /// selection that closes before dispatch can be replaced immediately.
   39         -
    /// Hyper may also return an unconsumed request when a reused connection
   40         -
    /// rejects it before writing. Once Hyper accepts a request on a fresh
   41         -
    /// connection, or begins an exchange on any connection, this function does
   42         -
    /// not reacquire.
   43         -
    pub(super) async fn send_h1_request(
          41  +
    /// The parent may reacquire only when this function returns the original
          42  +
    /// request. A fresh connection's send error is terminal even if Hyper
          43  +
    /// returns the request.
          44  +
    pub(super) async fn dispatch_h1(
   44     45   
        &self,
   45         -
        context: AcquisitionContext,
          46  +
        context: &AcquisitionContext,
   46     47   
        mut request: Request<SdkBody>,
   47         -
    ) -> Result<Response<SdkBody>, ConnectorError> {
          48  +
        mut selection: H1Selection,
          49  +
    ) -> Result<H1DispatchResult, ConnectorError> {
   48     50   
        let request_method = request.method().clone();
   49         -
        // Add the HTTP/1 header that depends on the absolute request URI.
   50         -
        add_host_header(&mut request, &context.absolute_uri)
   51         -
            .map_err(|error| ConnectorError::user(error.into()))?;
   52         -
   53         -
        // Redispatch is valid only when the original request is recovered intact.
   54         -
        loop {
   55         -
            let mut selection = Self::acquire_h1(&context).await?;
   56     51   
        let reused = selection.is_reused();
   57     52   
        let connection = selection.connection().clone();
   58     53   
        let close_handle = selection.close_handle();
   59     54   
        let captured_metadata = request
   60     55   
            .extensions()
   61     56   
            .get::<CaptureSmithyConnection>()
   62     57   
            .cloned()
   63     58   
            .map(|capture| {
   64     59   
                let metadata = connection.info().metadata(close_handle.clone());
   65     60   
                let captured = metadata.clone();
   66     61   
                capture.set_connection_retriever(move || Some(captured.clone()));
   67     62   
                metadata
   68     63   
            });
   69     64   
   70     65   
        if request.version() == Version::HTTP_2 {
   71     66   
            let metadata =
   72     67   
                captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
   73     68   
            return Err(ConnectorError::user(
   74     69   
                "an HTTP/2 request cannot use an HTTP/1 connection".into(),
   75     70   
            )
   76     71   
            .with_connection(metadata));
   77     72   
        }
   78     73   
          74  +
        add_host_header(&mut request, &context.absolute_uri)
          75  +
            .map_err(|error| ConnectorError::user(error.into()))?;
   79     76   
        rewrite_h1_request_target(&mut request, connection.info().is_proxied());
   80     77   
   81     78   
        // Commit against logical close before transferring the request to
   82     79   
        // Hyper. A close that wins this race leaves the request untouched.
   83     80   
        let Some(dispatch) = ConnectionState::try_commit_dispatch(&connection) else {
   84     81   
            tracing::trace!(
   85     82   
                connection_id = %connection.id(),
   86     83   
                request_partition = ?context.partition.id(),
   87     84   
                connection_partition = ?connection.owner_partition(),
   88     85   
                origin_scheme = %connection.info().origin().scheme(),
   89     86   
                origin_host = connection.info().origin().host(),
   90     87   
                origin_port = ?connection.info().origin().port(),
   91         -
                    "HTTP/1 selection became stale before dispatch; reacquiring"
          88  +
                "HTTP/1 selection became stale before dispatch"
   92     89   
            );
   93     90   
            *request.uri_mut() = context.absolute_uri.clone();
   94     91   
            selection.retire_connection(CloseReason::ProtocolClosed);
   95         -
                continue;
          92  +
            return Ok(H1DispatchResult::Reacquire(request));
   96     93   
        };
   97     94   
        let send = selection.sender_mut().hyper_mut().try_send_request(request);
   98     95   
   99     96   
        let exchange = selection.into_exchange();
  100     97   
        match send.await {
  101     98   
            Ok(mut response) => {
  102     99   
                // Response-body ownership keeps both the accepted dispatch
  103    100   
                // and exclusive request handle out of the pool until Hyper
  104    101   
                // proves a complete message boundary.
  105    102   
                connection
  106    103   
                    .info()
  107    104   
                    .apply_connector_extras(response.extensions_mut());
  108         -
                    return Ok(guard_response(
         105  +
                Ok(H1DispatchResult::Response(guard_response(
  109    106   
                    response,
  110    107   
                    request_method,
  111    108   
                    exchange,
  112    109   
                    dispatch,
  113    110   
                    context.owner_spawner.clone(),
  114         -
                    ));
         111  +
                )))
  115    112   
            }
  116    113   
            Err(mut error) => {
  117    114   
                if let Some(mut returned) = error.take_message() {
  118    115   
                    exchange.retire_connection(CloseReason::ProtocolClosed);
  119    116   
                    drop(dispatch);
  120    117   
                    if reused {
  121    118   
                        tracing::trace!(
  122    119   
                            connection_id = %connection.id(),
  123    120   
                            request_partition = ?context.partition.id(),
  124    121   
                            connection_partition = ?connection.owner_partition(),
  125    122   
                            origin_scheme = %connection.info().origin().scheme(),
  126    123   
                            origin_host = connection.info().origin().host(),
  127    124   
                            origin_port = ?connection.info().origin().port(),
  128         -
                                "reused HTTP/1 connection rejected request; reacquiring"
         125  +
                            "reused HTTP/1 connection returned request unsent"
  129    126   
                        );
  130    127   
                        *returned.uri_mut() = context.absolute_uri.clone();
  131         -
                            // Hyper returned the original request without
  132         -
                            // consuming its body, so reacquisition is authoritative.
  133         -
                            request = returned;
  134         -
                            continue;
         128  +
                        return Ok(H1DispatchResult::Reacquire(returned));
  135    129   
                    }
  136         -
                        let metadata = captured_metadata
  137         -
                            .unwrap_or_else(|| connection.info().metadata(close_handle));
  138         -
                        return Err(super::super::super::downcast_error(Box::new(
  139         -
                            error.into_error(),
  140         -
                        ))
  141         -
                        .with_connection(metadata));
  142         -
                    }
  143         -
                    exchange.retire_connection(CloseReason::IncompleteH1Exchange);
  144         -
                    drop(dispatch);
  145    130   
                    let metadata = captured_metadata
  146    131   
                        .unwrap_or_else(|| connection.info().metadata(close_handle));
  147    132   
                    return Err(
  148    133   
                        super::super::super::downcast_error(Box::new(error.into_error()))
  149    134   
                            .with_connection(metadata),
  150    135   
                    );
  151    136   
                }
         137  +
                exchange.retire_connection(CloseReason::IncompleteH1Exchange);
         138  +
                drop(dispatch);
         139  +
                let metadata =
         140  +
                    captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
         141  +
                Err(
         142  +
                    super::super::super::downcast_error(Box::new(error.into_error()))
         143  +
                        .with_connection(metadata),
         144  +
                )
  152    145   
            }
  153    146   
        }
  154    147   
    }
  155         -
  156         -
    /// Returns one exclusive HTTP/1 request handle for this request.
  157         -
    ///
  158         -
    /// A local idle handle completes immediately. Otherwise one cell-local
  159         -
    /// waiter remains registered while it receives either a reusable handle,
  160         -
    /// an establishment failure, or capacity to start establishment. Starting
  161         -
    /// establishment does not complete the waiter: a returning connection may
  162         -
    /// still satisfy it first.
  163         -
    async fn acquire_h1(context: &AcquisitionContext) -> Result<H1Selection, ConnectorError> {
  164         -
        if let Some(selection) = OriginCell::select_h1(&context.cell) {
  165         -
            tracing::trace!(
  166         -
                connection_id = %selection.connection_id(),
  167         -
                request_partition = ?context.partition.id(),
  168         -
                connection_partition = ?selection.connection().owner_partition(),
  169         -
                origin_scheme = %selection.connection().info().origin().scheme(),
  170         -
                origin_host = selection.connection().info().origin().host(),
  171         -
                origin_port = ?selection.connection().info().origin().port(),
  172         -
                "HTTP/1 pool hit; reusing idle connection"
  173         -
            );
  174         -
            return Ok(selection);
  175         -
        }
  176         -
  177         -
        let waiter = context
  178         -
            .cell
  179         -
            .register_waiter(ProtocolRequirement::H1Compatible);
  180         -
        tracing::trace!(
  181         -
            request_partition = ?context.partition.id(),
  182         -
            origin_scheme = %context.cell.id().origin().scheme(),
  183         -
            origin_host = context.cell.id().origin().host(),
  184         -
            origin_port = ?context.cell.id().origin().port(),
  185         -
            "HTTP/1 pool miss; acquisition queued"
  186         -
        );
  187         -
        let mut waiter_guard = WaiterGuard::new(context.cell.clone(), waiter);
  188         -
        loop {
  189         -
            match poll_fn(|cx| context.cell.poll_waiter(waiter, cx)).await {
  190         -
                AcquisitionEvent::Complete(AcquisitionResult::H1(selection)) => {
  191         -
                    tracing::trace!(
  192         -
                        connection_id = %selection.connection_id(),
  193         -
                        request_partition = ?context.partition.id(),
  194         -
                        connection_partition = ?selection.connection().owner_partition(),
  195         -
                        origin_scheme = %selection.connection().info().origin().scheme(),
  196         -
                        origin_host = selection.connection().info().origin().host(),
  197         -
                        origin_port = ?selection.connection().info().origin().port(),
  198         -
                        "HTTP/1 waiter acquired a reusable connection"
  199         -
                    );
  200         -
                    waiter_guard.disarm();
  201         -
                    return Ok(selection);
  202         -
                }
  203         -
                AcquisitionEvent::Complete(AcquisitionResult::Failed(error)) => {
  204         -
                    waiter_guard.disarm();
  205         -
                    return Err(error);
  206         -
                }
  207         -
                AcquisitionEvent::Establish(permit) => {
  208         -
                    tracing::trace!(
  209         -
                        request_partition = ?context.partition.id(),
  210         -
                        origin_scheme = %context.cell.id().origin().scheme(),
  211         -
                        origin_host = context.cell.id().origin().host(),
  212         -
                        origin_port = ?context.cell.id().origin().port(),
  213         -
                        "HTTP/1 waiter starting connection establishment"
  214         -
                    );
  215         -
                    let attempt = establish::establish_h1(context.clone(), permit);
  216         -
                    let completion = EstablishmentCompletion::new(context.cell.clone(), waiter);
  217         -
                    context.owner_spawner.spawn(Box::pin(async move {
  218         -
                        let mut completion = completion;
  219         -
                        if !completion.start() {
  220         -
                            drop(attempt);
  221         -
                            completion.disarm();
  222         -
                            return;
  223         -
                        }
  224         -
                        let result = attempt
  225         -
                            .await
  226         -
                            .map(AcquisitionResult::H1)
  227         -
                            .unwrap_or_else(AcquisitionResult::Failed);
  228         -
                        completion.complete(result);
  229         -
                    }));
  230         -
                }
  231         -
            }
  232         -
        }
  233         -
    }
  234         -
}
  235         -
  236         -
/// Guarantees a terminal result for one submitted establishment attempt.
  237         -
///
  238         -
/// This guard moves into the owner-runtime future before it is submitted. If
  239         -
/// the runtime discards that future, [`Drop`] reports a failure to the waiter.
  240         -
/// If another connection satisfies the waiter before the submitted future
  241         -
/// starts, [`Self::start`] rejects the attempt and the guard is disarmed.
  242         -
struct EstablishmentCompletion {
  243         -
    /// Cell retaining the waiter and establishment state.
  244         -
    cell: Arc<OriginCell>,
  245         -
    /// Waiter that receives the attempt's terminal result.
  246         -
    waiter: WaiterId,
  247         -
    /// Whether task drop still owes a failure result.
  248         -
    active: bool,
  249         -
}
  250         -
  251         -
impl EstablishmentCompletion {
  252         -
    /// Arms task-drop completion before the future is submitted.
  253         -
    fn new(cell: Arc<OriginCell>, waiter: WaiterId) -> Self {
  254         -
        Self {
  255         -
            cell,
  256         -
            waiter,
  257         -
            active: true,
  258         -
        }
  259         -
    }
  260         -
  261         -
    /// Marks the waiter started immediately before the connector is first polled.
  262         -
    fn start(&self) -> bool {
  263         -
        self.cell.start_establishment(self.waiter)
  264         -
    }
  265         -
  266         -
    /// Delivers the attempt result and disarms task-drop completion.
  267         -
    fn complete(mut self, result: AcquisitionResult) {
  268         -
        self.active = false;
  269         -
        self.cell.complete_establishment(self.waiter, result);
  270         -
    }
  271         -
  272         -
    /// Disarms after another connection has already completed the waiter.
  273         -
    fn disarm(&mut self) {
  274         -
        self.active = false;
  275         -
    }
  276         -
}
  277         -
  278         -
impl Drop for EstablishmentCompletion {
  279         -
    fn drop(&mut self) {
  280         -
        if self.active {
  281         -
            let error = ConnectorError::other(EstablishmentTaskDropped.into(), None);
  282         -
            self.cell
  283         -
                .complete_establishment(self.waiter, AcquisitionResult::Failed(error));
  284         -
            tracing::debug!(
  285         -
                request_partition = ?self.cell.id().partition(),
  286         -
                connection_partition = ?self.cell.id().partition(),
  287         -
                origin_scheme = %self.cell.id().origin().scheme(),
  288         -
                origin_host = self.cell.id().origin().host(),
  289         -
                origin_port = ?self.cell.id().origin().port(),
  290         -
                "HTTP/1 connection establishment task dropped"
  291         -
            );
  292         -
        }
  293         -
    }
  294         -
}
  295         -
  296         -
/// The owner runtime discarded an establishment future before completion.
  297         -
#[derive(Debug)]
  298         -
struct EstablishmentTaskDropped;
  299         -
  300         -
impl std::fmt::Display for EstablishmentTaskDropped {
  301         -
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  302         -
        f.write_str("the owner-runtime connection establishment task was dropped")
  303         -
    }
  304         -
}
  305         -
  306         -
impl std::error::Error for EstablishmentTaskDropped {}
  307         -
  308         -
/// Cancels a request's retained waiter if acquisition does not complete.
  309         -
///
  310         -
/// The request future owns this guard from waiter registration until it
  311         -
/// consumes a terminal result. Dropping the future cancels the waiter; cell
  312         -
/// cancellation returns any crossing capacity or HTTP/1 handle after releasing
  313         -
/// the cell lock.
  314         -
struct WaiterGuard {
  315         -
    /// Cell retaining the waiter.
  316         -
    cell: Arc<OriginCell>,
  317         -
    /// Waiter cancelled if the request future does not consume a result.
  318         -
    waiter: WaiterId,
  319         -
    /// Whether request cancellation is still armed.
  320         -
    active: bool,
  321         -
}
  322         -
  323         -
impl WaiterGuard {
  324         -
    /// Arms cancellation for a newly registered waiter.
  325         -
    fn new(cell: Arc<OriginCell>, waiter: WaiterId) -> Self {
  326         -
        Self {
  327         -
            cell,
  328         -
            waiter,
  329         -
            active: true,
  330         -
        }
  331         -
    }
  332         -
  333         -
    /// Disarms after the request consumes its terminal result.
  334         -
    fn disarm(&mut self) {
  335         -
        self.active = false;
  336         -
    }
  337         -
}
  338         -
  339         -
impl Drop for WaiterGuard {
  340         -
    fn drop(&mut self) {
  341         -
        if self.active {
  342         -
            OriginCell::cancel_waiter(&self.cell, self.waiter);
  343         -
        }
  344         -
    }
  345    148   
}
  346    149   
  347    150   
/// Attaches connection return and dispatch completion to an accepted response.
  348    151   
///
  349    152   
/// An ordinary response retains both guards until Hyper proves a complete
  350    153   
/// message boundary. Successful CONNECT and `101` responses retire the pool
  351    154   
/// record before the response exposes Hyper's upgraded root I/O.
  352    155   
fn guard_response(
  353    156   
    response: Response<hyper::body::Incoming>,
  354    157   
    method: Method,
@@ -575,378 +668,474 @@
  595    398   
    };
  596    399   
    let host = match absolute_uri.port_u16() {
  597    400   
        Some(port) if Some(port) != default_port => {
  598    401   
            format!("{}:{port}", authority.host())
  599    402   
        }
  600    403   
        _ => authority.host().to_string(),
  601    404   
    };
  602    405   
    let value =
  603    406   
        http_1x::HeaderValue::from_str(&host).map_err(RequestDispatchError::InvalidHostHeader)?;
  604    407   
    request.headers_mut().insert(http_1x::header::HOST, value);
         408  +
    request.extensions_mut().insert(H1HostHeaderInserted);
  605    409   
    Ok(())
  606    410   
}
  607    411   
  608    412   
/// Rewrites an absolute URI into the request target required by HTTP/1.
  609    413   
fn rewrite_h1_request_target(request: &mut Request<SdkBody>, is_proxied: bool) {
  610    414   
    use http_1x::uri::Parts;
  611    415   
  612    416   
    if request.method() == Method::CONNECT {
  613    417   
        if let Some(authority) = request.uri().authority().cloned() {
  614    418   
            let mut parts = Parts::default();
  615    419   
            parts.authority = Some(authority);
  616    420   
            *request.uri_mut() = Uri::from_parts(parts).expect("request authority is a valid URI");
  617    421   
        }
  618    422   
        return;
  619    423   
    }
  620    424   
    if is_proxied {
  621    425   
        return;
  622    426   
    }
  623    427   
    let mut parts = Parts::default();
  624    428   
    parts.path_and_query = request.uri().path_and_query().cloned();
  625    429   
    *request.uri_mut() = Uri::from_parts(parts).expect("request path and query are a valid URI");
  626    430   
}
  627    431   
  628    432   
#[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
  629    433   
mod tests {
  630    434   
    use super::*;
  631    435   
    use crate::client::pool::cell::h1::H1Sender;
         436  +
    use crate::client::pool::cell::OriginCell;
  632    437   
    use crate::client::pool::connection::ConnectionInfo;
  633    438   
    use crate::client::pool::registry::PartitionState;
  634    439   
    use crate::client::pool::{
  635    440   
        Client, ConnectionId, ConnectionPool, ConnectionReuseScope, Partition, PartitionId,
  636    441   
        TokioDriverSpawner,
  637    442   
    };
  638    443   
    use crate::client::timeout::test::{NeverConnects, NeverReplies};
         444  +
    use crate::sync::Arc;
  639    445   
    use aws_smithy_async::rt::sleep::TokioSleep;
  640    446   
    use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
  641    447   
    use aws_smithy_runtime_api::client::http::{HttpClient, HttpConnector, HttpConnectorSettings};
  642    448   
    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
  643    449   
    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
  644    450   
    use http_body_util::BodyExt;
  645    451   
    use hyper_util::client::legacy::connect::{Connected, Connection};
  646    452   
    use hyper_util::rt::TokioIo;
  647    453   
    use std::future::Future;
  648    454   
    use std::io::{self, IoSlice};
@@ -829,635 +889,695 @@
  849    655   
            .partition(PartitionId::from_index(7))
  850    656   
            .unwrap();
  851    657   
        let request = Request::get("http://127.0.0.1:9/")
  852    658   
            .body(SdkBody::empty())
  853    659   
            .unwrap();
  854    660   
  855    661   
        let error = pool
  856    662   
            .send_request(partition, request, None)
  857    663   
            .await
  858    664   
            .expect_err("dropped establishment task did not fail the request");
  859         -
        assert!(error.is_other());
         665  +
        assert!(error.is_io());
  860    666   
        assert_eq!(
  861    667   
            "the owner-runtime connection establishment task was dropped",
  862    668   
            std::error::Error::source(&error).unwrap().to_string()
  863    669   
        );
  864    670   
    }
  865    671   
  866    672   
    struct TestServer {
  867    673   
        endpoint: String,
  868    674   
        accepted: StdArc<AtomicUsize>,
  869    675   
        task: tokio::task::JoinHandle<()>,
@@ -1322,1128 +1405,1227 @@
 1342   1148   
 1343   1149   
        drop(request(&pool, partition.clone(), server.uri("/first")).await);
 1344   1150   
        assert_eq!(
 1345   1151   
            hyper::body::Bytes::from_static(b"ok"),
 1346   1152   
            consume(request(&pool, partition, server.uri("/second")).await).await
 1347   1153   
        );
 1348   1154   
        assert_eq!(2, server.accepted());
 1349   1155   
    }
 1350   1156   
 1351   1157   
    #[tokio::test]
 1352         -
    async fn incompatible_h1_selection_reports_connection_identity() {
        1158  +
    async fn incompatible_h1_result_remains_reusable_for_compatible_demand() {
 1353   1159   
        let server = TestServer::start().await;
 1354         -
        let pool = ConnectionPool::builder().build_http().unwrap();
        1160  +
        let pool = ConnectionPool::builder()
        1161  +
            .max_connections_per_host(1)
        1162  +
            .build_http()
        1163  +
            .unwrap();
 1355   1164   
        let partition = anonymous_partition(&pool);
 1356   1165   
        let capture = CaptureSmithyConnection::new();
 1357         -
        let mut request = Request::get(server.uri("/h2"))
        1166  +
        let mut h2_request = Request::get(server.uri("/h2"))
 1358   1167   
            .version(Version::HTTP_2)
 1359   1168   
            .body(SdkBody::empty())
 1360   1169   
            .unwrap();
 1361         -
        request.extensions_mut().insert(capture.clone());
        1170  +
        h2_request.extensions_mut().insert(capture.clone());
 1362   1171   
 1363   1172   
        let error = pool
 1364         -
            .send_request(partition, request, None)
        1173  +
            .send_request(partition.clone(), h2_request, None)
 1365   1174   
            .await
 1366   1175   
            .expect_err("an HTTP/2 request unexpectedly used HTTP/1");
 1367   1176   
        assert!(error.is_user());
 1368         -
        assert!(error
        1177  +
        let error_connection = error
 1369   1178   
            .connection_metadata()
 1370         -
            .and_then(|metadata| metadata.connection_id())
 1371         -
            .is_some());
 1372         -
        assert!(capture
        1179  +
            .expect("the mismatch error omitted connection metadata");
        1180  +
        let captured_connection = capture
 1373   1181   
            .get()
 1374         -
            .and_then(|metadata| metadata.connection_id())
 1375         -
            .is_some());
        1182  +
            .expect("the mismatch request did not capture its connection");
        1183  +
        assert_eq!(
        1184  +
            error_connection.connection_id(),
        1185  +
            captured_connection.connection_id()
        1186  +
        );
        1187  +
        1188  +
        let response = request(&pool, partition, server.uri("/h1")).await;
        1189  +
        assert_eq!(
        1190  +
            hyper::body::Bytes::from_static(b"ok"),
        1191  +
            consume(response).await
        1192  +
        );
        1193  +
        assert_eq!(
        1194  +
            1,
        1195  +
            server.accepted(),
        1196  +
            "the rejected HTTP/2 request discarded a reusable HTTP/1 connection"
        1197  +
        );
 1376   1198   
    }
 1377   1199   
 1378   1200   
    #[tokio::test]
 1379   1201   
    async fn captured_metadata_poisons_only_selected_generation() {
 1380   1202   
        let server = TestServer::start().await;
 1381   1203   
        let pool = ConnectionPool::builder().build_http().unwrap();
 1382   1204   
        let partition = anonymous_partition(&pool);
 1383   1205   
        let capture = CaptureSmithyConnection::new();
 1384   1206   
        let mut first = Request::get(server.uri("/first"))
 1385   1207   
            .body(SdkBody::empty())