AWS SDK

AWS SDK

rev. 39ed07918eb1729475c85ddf93a83d0b1539e928 (ignoring whitespace)

Files changed:

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

@@ -0,1 +0,639 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
//! HTTP/2 request authority and two-sided completion.
           7  +
//!
           8  +
//! A cell creates [`H2Activation`] only after reserving one prospective request
           9  +
//! against an exact generation. The activation owns the transient Hyper sender
          10  +
//! and the right to convert that prospective count into an accepted request.
          11  +
//! Dropping it before acceptance cancels the prospective count.
          12  +
//!
          13  +
//! Hyper acceptance creates one [`H2RequestClaim`]. The request upload and
          14  +
//! response lifetime hold distinct guard types so transposing the two sides is
          15  +
//! a type error. Either side may finish first. The claim releases the accepted
          16  +
//! generation count only after both sides finish, including drop and error
          17  +
//! paths.
          18  +
//!
          19  +
//! The claim mutex is never held while locking cell or connection state.
          20  +
          21  +
use super::super::super::connection::{ConnectionInfo, ConnectionState, DispatchGuard};
          22  +
use super::super::super::partition::PartitionId;
          23  +
use super::super::{OriginCell, WaiterId};
          24  +
use super::{H2CloseHandle, H2GenerationId, H2RouteId, H2Sender};
          25  +
use crate::sync::{Arc, Mutex, Weak};
          26  +
          27  +
/// Sender and connection cloned while a prospective request is cell-owned.
          28  +
pub(super) struct H2ActivationResources {
          29  +
    /// Transient sender clone for one activation.
          30  +
    pub(super) sender: H2Sender,
          31  +
    /// Whether the generation accepted an earlier request.
          32  +
    pub(super) reused: bool,
          33  +
    /// Connection retained through prospective dispatch.
          34  +
    pub(super) connection: Arc<ConnectionState>,
          35  +
}
          36  +
          37  +
/// Resources retained before and after dispatch transfer.
          38  +
enum H2ActivationState {
          39  +
    /// Sender and connection still owned by the activation.
          40  +
    Ready(H2ActivationResources),
          41  +
    /// Immutable identity retained after the sender leaves the activation.
          42  +
    Dispatched(H2RequestIdentity),
          43  +
}
          44  +
          45  +
/// Values transferred together when an activation begins Hyper dispatch.
          46  +
pub(in crate::client::pool) struct H2DispatchParts {
          47  +
    /// Transient sender clone for one dispatch attempt.
          48  +
    pub(in crate::client::pool) sender: H2Sender,
          49  +
    /// Upload-completion guard retained by the request body.
          50  +
    pub(in crate::client::pool) upload: H2UploadGuard,
          51  +
    /// Response-lifetime guard retained by the response future or body.
          52  +
    pub(in crate::client::pool) response: H2ResponseGuard,
          53  +
}
          54  +
          55  +
/// Prospective request authority for one exact generation.
          56  +
///
          57  +
/// The activation leaves the cell lock with one counted prospective request.
          58  +
/// Taking its dispatch parts transfers the sender and creates the two request
          59  +
/// guards. Hyper acceptance converts the prospective count to an accepted
          60  +
/// claim; rejection or drop cancels it.
          61  +
pub(in crate::client::pool) struct H2Activation {
          62  +
    /// Cell that owns the selected generation.
          63  +
    connection_cell: Arc<OriginCell>,
          64  +
    /// Exact generation that owns the prospective count.
          65  +
    generation: H2GenerationId,
          66  +
    /// Partition issuing this request.
          67  +
    request_partition: PartitionId,
          68  +
    /// Mutually exclusive pre-dispatch resources or post-transfer identity.
          69  +
    state: Option<H2ActivationState>,
          70  +
    /// Shared accounting for the upload and response sides.
          71  +
    claim: Arc<H2RequestClaim>,
          72  +
    /// Requesting-cell priority released at acceptance or cancellation.
          73  +
    turn: Option<H2ActivationTurnGuard>,
          74  +
    /// Whether drop must cancel the prospective generation count.
          75  +
    active: bool,
          76  +
}
          77  +
          78  +
impl std::fmt::Debug for H2Activation {
          79  +
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
          80  +
        f.debug_struct("H2Activation")
          81  +
            .field("generation", &self.generation)
          82  +
            .field(
          83  +
                "connection_id",
          84  +
                &self.state.as_ref().map(|state| match state {
          85  +
                    H2ActivationState::Ready(resources) => resources.connection.id(),
          86  +
                    H2ActivationState::Dispatched(identity) => identity.connection.id(),
          87  +
                }),
          88  +
            )
          89  +
            .field("active", &self.active)
          90  +
            .finish()
          91  +
    }
          92  +
}
          93  +
          94  +
impl H2Activation {
          95  +
    /// Builds an activation after the generation records its prospective claim.
          96  +
    pub(super) fn new(
          97  +
        connection_cell: Arc<OriginCell>,
          98  +
        generation: H2GenerationId,
          99  +
        resources: H2ActivationResources,
         100  +
        request_partition: PartitionId,
         101  +
        turn: Option<H2ActivationTurnGuard>,
         102  +
    ) -> Self {
         103  +
        let claim = Arc::new(H2RequestClaim {
         104  +
            connection_cell: Weak::from_arc(&connection_cell),
         105  +
            generation,
         106  +
            state: Mutex::new(H2RequestClaimState::Prospective {
         107  +
                upload_finished: false,
         108  +
                response_finished: false,
         109  +
            }),
         110  +
        });
         111  +
        Self {
         112  +
            connection_cell,
         113  +
            generation,
         114  +
            request_partition,
         115  +
            state: Some(H2ActivationState::Ready(resources)),
         116  +
            claim,
         117  +
            turn,
         118  +
            active: true,
         119  +
        }
         120  +
    }
         121  +
         122  +
    /// Returns the exact selected generation.
         123  +
    #[cfg(test)]
         124  +
    pub(in crate::client::pool) fn generation(&self) -> H2GenerationId {
         125  +
        self.generation
         126  +
    }
         127  +
         128  +
    /// Returns whether Hyper previously accepted a request on this generation.
         129  +
    ///
         130  +
    /// # Panics
         131  +
    ///
         132  +
    /// Panics if dispatch parts were already taken.
         133  +
    pub(in crate::client::pool) fn is_reused(&self) -> bool {
         134  +
        match self
         135  +
            .state
         136  +
            .as_ref()
         137  +
            .expect("HTTP/2 activation state missing")
         138  +
        {
         139  +
            H2ActivationState::Ready(resources) => resources.reused,
         140  +
            H2ActivationState::Dispatched(_) => {
         141  +
                panic!("HTTP/2 activation resources already taken")
         142  +
            }
         143  +
        }
         144  +
    }
         145  +
         146  +
    /// Returns the selected protocol-neutral connection.
         147  +
    ///
         148  +
    /// # Panics
         149  +
    ///
         150  +
    /// Panics if dispatch parts were already taken.
         151  +
    pub(in crate::client::pool) fn connection(&self) -> &Arc<ConnectionState> {
         152  +
        match self
         153  +
            .state
         154  +
            .as_ref()
         155  +
            .expect("HTTP/2 activation state missing")
         156  +
        {
         157  +
            H2ActivationState::Ready(resources) => &resources.connection,
         158  +
            H2ActivationState::Dispatched(_) => {
         159  +
                panic!("HTTP/2 activation resources already taken")
         160  +
            }
         161  +
        }
         162  +
    }
         163  +
         164  +
    /// Transfers the sender and typed request guards into one dispatch attempt.
         165  +
    ///
         166  +
    /// # Panics
         167  +
    ///
         168  +
    /// Panics if dispatch parts were already taken.
         169  +
    pub(in crate::client::pool) fn take_dispatch_parts(&mut self) -> H2DispatchParts {
         170  +
        let state = self.state.take().expect("HTTP/2 activation state missing");
         171  +
        let H2ActivationState::Ready(resources) = state else {
         172  +
            self.state = Some(state);
         173  +
            panic!("HTTP/2 activation dispatch parts already taken");
         174  +
        };
         175  +
        let identity = H2RequestIdentity {
         176  +
            request_partition: self.request_partition,
         177  +
            connection: resources.connection.info().clone(),
         178  +
            generation: self.generation,
         179  +
        };
         180  +
        self.state = Some(H2ActivationState::Dispatched(identity.clone()));
         181  +
        H2DispatchParts {
         182  +
            sender: resources.sender,
         183  +
            upload: H2UploadGuard::new(self.claim.clone(), Some(identity.clone())),
         184  +
            response: H2ResponseGuard::new(self.claim.clone(), Some(identity)),
         185  +
        }
         186  +
    }
         187  +
         188  +
    /// Retains a requesting-cell route turn until acceptance or cancellation.
         189  +
    pub(super) fn attach_peer_turn(
         190  +
        &mut self,
         191  +
        requesting_cell: &Arc<OriginCell>,
         192  +
        route: H2RouteId,
         193  +
        waiter: WaiterId,
         194  +
    ) {
         195  +
        debug_assert_eq!(
         196  +
            self.request_partition,
         197  +
            requesting_cell.id().partition(),
         198  +
            "peer HTTP/2 activation changed requesting partition"
         199  +
        );
         200  +
        assert!(
         201  +
            self.turn.is_none(),
         202  +
            "HTTP/2 activation acquired two requesting-cell turns"
         203  +
        );
         204  +
        self.turn = Some(H2ActivationTurnGuard::peer(requesting_cell, route, waiter));
         205  +
    }
         206  +
         207  +
    /// Returns close authority for the connection-owning generation.
         208  +
    pub(in crate::client::pool) fn close_handle(&self) -> H2CloseHandle {
         209  +
        H2CloseHandle::new(&self.connection_cell, self.generation)
         210  +
    }
         211  +
         212  +
    /// Converts the prospective reservation after Hyper accepts the request.
         213  +
    ///
         214  +
    /// # Panics
         215  +
    ///
         216  +
    /// Panics if dispatch parts were not transferred or the prospective
         217  +
    /// reservation disappeared before acceptance.
         218  +
    pub(in crate::client::pool) fn accept(mut self, dispatch: DispatchGuard) {
         219  +
        let Some(H2ActivationState::Dispatched(identity)) = self.state.as_ref() else {
         220  +
            panic!("HTTP/2 activation accepted before dispatch parts were taken");
         221  +
        };
         222  +
        assert!(
         223  +
            OriginCell::accept_h2_activation(&self.connection_cell, self.generation),
         224  +
            "prospective HTTP/2 activation disappeared before acceptance"
         225  +
        );
         226  +
        self.active = false;
         227  +
        self.claim.accept(dispatch);
         228  +
        if let Some(turn) = self.turn.take() {
         229  +
            turn.release();
         230  +
        }
         231  +
        tracing::trace!(
         232  +
            connection_id = %identity.connection.id(),
         233  +
            request_partition = ?identity.request_partition,
         234  +
            connection_partition = ?identity.connection.owner_partition(),
         235  +
            origin_scheme = %identity.connection.origin().scheme(),
         236  +
            origin_host = identity.connection.origin().host(),
         237  +
            origin_port = ?identity.connection.origin().port(),
         238  +
            h2_generation = ?identity.generation,
         239  +
            "HTTP/2 request accepted"
         240  +
        );
         241  +
    }
         242  +
}
         243  +
         244  +
impl Drop for H2Activation {
         245  +
    fn drop(&mut self) {
         246  +
        if self.active {
         247  +
            OriginCell::cancel_h2_activation(&self.connection_cell, self.generation);
         248  +
            self.claim.cancel();
         249  +
            if let Some(turn) = self.turn.take() {
         250  +
                turn.release();
         251  +
            }
         252  +
            match &self.state {
         253  +
                Some(H2ActivationState::Ready(resources)) => trace_activation_cancelled(
         254  +
                    self.request_partition,
         255  +
                    self.generation,
         256  +
                    resources.connection.info(),
         257  +
                ),
         258  +
                Some(H2ActivationState::Dispatched(identity)) => trace_activation_cancelled(
         259  +
                    identity.request_partition,
         260  +
                    identity.generation,
         261  +
                    &identity.connection,
         262  +
                ),
         263  +
                None => {}
         264  +
            }
         265  +
        }
         266  +
    }
         267  +
}
         268  +
         269  +
/// One activation-gate turn retained until Hyper acceptance or cancellation.
         270  +
pub(super) struct H2ActivationTurnGuard {
         271  +
    /// Requesting cell whose gate owns this turn.
         272  +
    cell: Weak<OriginCell>,
         273  +
    /// Waiter that received the activation opportunity.
         274  +
    waiter: WaiterId,
         275  +
    /// Local-generation or peer-route gate identity.
         276  +
    owner: H2ActivationGateOwner,
         277  +
}
         278  +
         279  +
/// Cell transition run when an activation accepts or cancels.
         280  +
enum H2ActivationGateOwner {
         281  +
    /// Gate attached to the connection cell's local generation.
         282  +
    Local { generation: H2GenerationId },
         283  +
    /// Gate attached to a requesting cell's peer route.
         284  +
    Peer { route: H2RouteId },
         285  +
}
         286  +
         287  +
impl H2ActivationTurnGuard {
         288  +
    pub(super) fn local(
         289  +
        cell: &Arc<OriginCell>,
         290  +
        generation: H2GenerationId,
         291  +
        waiter: WaiterId,
         292  +
    ) -> Self {
         293  +
        Self {
         294  +
            cell: Weak::from_arc(cell),
         295  +
            waiter,
         296  +
            owner: H2ActivationGateOwner::Local { generation },
         297  +
        }
         298  +
    }
         299  +
         300  +
    fn peer(cell: &Arc<OriginCell>, route: H2RouteId, waiter: WaiterId) -> Self {
         301  +
        Self {
         302  +
            cell: Weak::from_arc(cell),
         303  +
            waiter,
         304  +
            owner: H2ActivationGateOwner::Peer { route },
         305  +
        }
         306  +
    }
         307  +
         308  +
    fn release(self) {
         309  +
        if let Some(cell) = self.cell.upgrade() {
         310  +
            match self.owner {
         311  +
                H2ActivationGateOwner::Local { generation } => {
         312  +
                    OriginCell::release_local_h2_turn(&cell, generation, self.waiter)
         313  +
                }
         314  +
                H2ActivationGateOwner::Peer { route } => {
         315  +
                    OriginCell::release_peer_h2_turn(&cell, route, self.waiter)
         316  +
                }
         317  +
            }
         318  +
        }
         319  +
    }
         320  +
}
         321  +
         322  +
/// Shared accounting for one prospective or accepted HTTP/2 request.
         323  +
struct H2RequestClaim {
         324  +
    /// Connection cell updated after both accepted sides finish.
         325  +
    connection_cell: Weak<OriginCell>,
         326  +
    /// Exact generation whose request count this claim owns.
         327  +
    generation: H2GenerationId,
         328  +
    /// Request phase; never held while locking cell or connection state.
         329  +
    state: Mutex<H2RequestClaimState>,
         330  +
}
         331  +
         332  +
/// Shared request phase and independent completion bits.
         333  +
enum H2RequestClaimState {
         334  +
    /// Generation count reserved before Hyper accepts the request.
         335  +
    Prospective {
         336  +
        upload_finished: bool,
         337  +
        response_finished: bool,
         338  +
    },
         339  +
    /// Hyper accepted the request and connection dispatch is retained.
         340  +
    Accepted {
         341  +
        dispatch: DispatchGuard,
         342  +
        upload_finished: bool,
         343  +
        response_finished: bool,
         344  +
    },
         345  +
    /// Both sides finished or prospective dispatch was cancelled.
         346  +
    Complete,
         347  +
}
         348  +
         349  +
impl H2RequestClaim {
         350  +
    /// Converts prospective request state to accepted state.
         351  +
    fn accept(&self, dispatch: DispatchGuard) {
         352  +
        let completed_dispatch = {
         353  +
            let mut state = self.state.lock();
         354  +
            let previous = std::mem::replace(&mut *state, H2RequestClaimState::Complete);
         355  +
            let (upload_finished, response_finished) = match previous {
         356  +
                H2RequestClaimState::Prospective {
         357  +
                    upload_finished,
         358  +
                    response_finished,
         359  +
                } => (upload_finished, response_finished),
         360  +
                other @ (H2RequestClaimState::Accepted { .. } | H2RequestClaimState::Complete) => {
         361  +
                    *state = other;
         362  +
                    drop(state);
         363  +
                    panic!("HTTP/2 request claim accepted outside prospective state");
         364  +
                }
         365  +
            };
         366  +
            if upload_finished && response_finished {
         367  +
                Some(dispatch)
         368  +
            } else {
         369  +
                *state = H2RequestClaimState::Accepted {
         370  +
                    dispatch,
         371  +
                    upload_finished,
         372  +
                    response_finished,
         373  +
                };
         374  +
                None
         375  +
            }
         376  +
        };
         377  +
        if let Some(dispatch) = completed_dispatch {
         378  +
            // Dispatch completion takes the connection lifecycle lock. Keep it
         379  +
            // outside the claim lock so completion cannot nest pool locks.
         380  +
            drop(dispatch);
         381  +
            self.release_generation();
         382  +
        }
         383  +
    }
         384  +
         385  +
    /// Cancels request state after the prospective reservation ends.
         386  +
    fn cancel(&self) {
         387  +
        let mut state = self.state.lock();
         388  +
        if matches!(*state, H2RequestClaimState::Prospective { .. }) {
         389  +
            *state = H2RequestClaimState::Complete;
         390  +
        }
         391  +
    }
         392  +
         393  +
    fn finish_upload(&self) -> bool {
         394  +
        self.finish_side(|upload_finished, _| *upload_finished = true)
         395  +
    }
         396  +
         397  +
    fn finish_response(&self) -> bool {
         398  +
        self.finish_side(|_, response_finished| *response_finished = true)
         399  +
    }
         400  +
         401  +
    /// Marks one request side finished and releases the claim on the second.
         402  +
    fn finish_side(&self, finish: impl FnOnce(&mut bool, &mut bool)) -> bool {
         403  +
        let completed_dispatch = {
         404  +
            let mut state = self.state.lock();
         405  +
            let previous = std::mem::replace(&mut *state, H2RequestClaimState::Complete);
         406  +
            match previous {
         407  +
                H2RequestClaimState::Prospective {
         408  +
                    mut upload_finished,
         409  +
                    mut response_finished,
         410  +
                } => {
         411  +
                    finish(&mut upload_finished, &mut response_finished);
         412  +
                    *state = H2RequestClaimState::Prospective {
         413  +
                        upload_finished,
         414  +
                        response_finished,
         415  +
                    };
         416  +
                    None
         417  +
                }
         418  +
                H2RequestClaimState::Accepted {
         419  +
                    dispatch,
         420  +
                    mut upload_finished,
         421  +
                    mut response_finished,
         422  +
                } => {
         423  +
                    finish(&mut upload_finished, &mut response_finished);
         424  +
                    if upload_finished && response_finished {
         425  +
                        Some(dispatch)
         426  +
                    } else {
         427  +
                        *state = H2RequestClaimState::Accepted {
         428  +
                            dispatch,
         429  +
                            upload_finished,
         430  +
                            response_finished,
         431  +
                        };
         432  +
                        None
         433  +
                    }
         434  +
                }
         435  +
                H2RequestClaimState::Complete => None,
         436  +
            }
         437  +
        };
         438  +
        let request_complete = completed_dispatch.is_some();
         439  +
        if let Some(dispatch) = completed_dispatch {
         440  +
            // Dispatch completion takes the connection lifecycle lock. Keep it
         441  +
            // outside the claim lock so completion cannot nest pool locks.
         442  +
            drop(dispatch);
         443  +
            self.release_generation();
         444  +
        }
         445  +
        request_complete
         446  +
    }
         447  +
         448  +
    /// Releases the generation count after the claim lock is released.
         449  +
    fn release_generation(&self) {
         450  +
        if let Some(cell) = self.connection_cell.upgrade() {
         451  +
            OriginCell::release_h2_request(&cell, self.generation);
         452  +
        }
         453  +
    }
         454  +
         455  +
    #[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
         456  +
    fn for_test(cell: &Arc<OriginCell>) -> Arc<Self> {
         457  +
        Arc::new(Self {
         458  +
            connection_cell: Weak::from_arc(cell),
         459  +
            generation: H2GenerationId(0),
         460  +
            state: Mutex::new(H2RequestClaimState::Prospective {
         461  +
                upload_finished: false,
         462  +
                response_finished: false,
         463  +
            }),
         464  +
        })
         465  +
    }
         466  +
}
         467  +
         468  +
/// Request and connection identity retained for completion logs.
         469  +
#[derive(Clone)]
         470  +
struct H2RequestIdentity {
         471  +
    request_partition: PartitionId,
         472  +
    connection: Arc<ConnectionInfo>,
         473  +
    generation: H2GenerationId,
         474  +
}
         475  +
         476  +
fn trace_activation_cancelled(
         477  +
    request_partition: PartitionId,
         478  +
    generation: H2GenerationId,
         479  +
    connection: &ConnectionInfo,
         480  +
) {
         481  +
    tracing::trace!(
         482  +
        connection_id = %connection.id(),
         483  +
        request_partition = ?request_partition,
         484  +
        connection_partition = ?connection.owner_partition(),
         485  +
        origin_scheme = %connection.origin().scheme(),
         486  +
        origin_host = connection.origin().host(),
         487  +
        origin_port = ?connection.origin().port(),
         488  +
        h2_generation = ?generation,
         489  +
        "HTTP/2 activation cancelled before request acceptance"
         490  +
    );
         491  +
}
         492  +
         493  +
/// Linear guard for request-upload completion.
         494  +
pub(in crate::client::pool) struct H2UploadGuard {
         495  +
    claim: Arc<H2RequestClaim>,
         496  +
    identity: Option<H2RequestIdentity>,
         497  +
    active: bool,
         498  +
}
         499  +
         500  +
impl H2UploadGuard {
         501  +
    fn new(claim: Arc<H2RequestClaim>, identity: Option<H2RequestIdentity>) -> Self {
         502  +
        Self {
         503  +
            claim,
         504  +
            identity,
         505  +
            active: true,
         506  +
        }
         507  +
    }
         508  +
         509  +
    #[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
         510  +
    pub(in crate::client::pool) fn for_test(cell: &Arc<OriginCell>) -> (Self, H2RequestClaimProbe) {
         511  +
        let claim = H2RequestClaim::for_test(cell);
         512  +
        (
         513  +
            Self::new(claim.clone(), None),
         514  +
            H2RequestClaimProbe { claim },
         515  +
        )
         516  +
    }
         517  +
         518  +
    /// Finishes upload ownership before dropping the guard.
         519  +
    pub(in crate::client::pool) fn finish(mut self) {
         520  +
        self.finish_once();
         521  +
    }
         522  +
         523  +
    fn finish_once(&mut self) {
         524  +
        if self.active {
         525  +
            self.active = false;
         526  +
            let request_complete = self.claim.finish_upload();
         527  +
            trace_request_side(self.identity.take(), "upload", request_complete);
         528  +
        }
         529  +
    }
         530  +
}
         531  +
         532  +
impl Drop for H2UploadGuard {
         533  +
    fn drop(&mut self) {
         534  +
        self.finish_once();
         535  +
    }
         536  +
}
         537  +
         538  +
/// Linear guard for response-future and response-body completion.
         539  +
pub(in crate::client::pool) struct H2ResponseGuard {
         540  +
    claim: Arc<H2RequestClaim>,
         541  +
    identity: Option<H2RequestIdentity>,
         542  +
    active: bool,
         543  +
}
         544  +
         545  +
impl H2ResponseGuard {
         546  +
    fn new(claim: Arc<H2RequestClaim>, identity: Option<H2RequestIdentity>) -> Self {
         547  +
        Self {
         548  +
            claim,
         549  +
            identity,
         550  +
            active: true,
         551  +
        }
         552  +
    }
         553  +
         554  +
    #[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
         555  +
    pub(in crate::client::pool) fn for_test(cell: &Arc<OriginCell>) -> (Self, H2RequestClaimProbe) {
         556  +
        let claim = H2RequestClaim::for_test(cell);
         557  +
        (
         558  +
            Self::new(claim.clone(), None),
         559  +
            H2RequestClaimProbe { claim },
         560  +
        )
         561  +
    }
         562  +
         563  +
    /// Finishes response ownership before dropping the guard.
         564  +
    pub(in crate::client::pool) fn finish(mut self) {
         565  +
        self.finish_once();
         566  +
    }
         567  +
         568  +
    fn finish_once(&mut self) {
         569  +
        if self.active {
         570  +
            self.active = false;
         571  +
            let request_complete = self.claim.finish_response();
         572  +
            trace_request_side(self.identity.take(), "response", request_complete);
         573  +
        }
         574  +
    }
         575  +
}
         576  +
         577  +
impl Drop for H2ResponseGuard {
         578  +
    fn drop(&mut self) {
         579  +
        self.finish_once();
         580  +
    }
         581  +
}
         582  +
         583  +
fn trace_request_side(
         584  +
    identity: Option<H2RequestIdentity>,
         585  +
    request_side: &'static str,
         586  +
    request_complete: bool,
         587  +
) {
         588  +
    if let Some(identity) = identity {
         589  +
        tracing::trace!(
         590  +
            connection_id = %identity.connection.id(),
         591  +
            request_partition = ?identity.request_partition,
         592  +
            connection_partition = ?identity.connection.owner_partition(),
         593  +
            origin_scheme = %identity.connection.origin().scheme(),
         594  +
            origin_host = identity.connection.origin().host(),
         595  +
            origin_port = ?identity.connection.origin().port(),
         596  +
            h2_generation = ?identity.generation,
         597  +
            request_side,
         598  +
            request_complete,
         599  +
            "HTTP/2 request side finished"
         600  +
        );
         601  +
    }
         602  +
}
         603  +
         604  +
/// Test observation of one request claim's production state.
         605  +
#[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
         606  +
pub(in crate::client::pool) struct H2RequestClaimProbe {
         607  +
    claim: Arc<H2RequestClaim>,
         608  +
}
         609  +
         610  +
#[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
         611  +
impl H2RequestClaimProbe {
         612  +
    /// Returns whether the upload side reached its terminal transition.
         613  +
    pub(in crate::client::pool) fn upload_finished(&self) -> bool {
         614  +
        matches!(
         615  +
            &*self.claim.state.lock(),
         616  +
            H2RequestClaimState::Prospective {
         617  +
                upload_finished: true,
         618  +
                ..
         619  +
            } | H2RequestClaimState::Accepted {
         620  +
                upload_finished: true,
         621  +
                ..
         622  +
            } | H2RequestClaimState::Complete
         623  +
        )
         624  +
    }
         625  +
         626  +
    /// Returns whether the response side reached its terminal transition.
         627  +
    pub(in crate::client::pool) fn response_finished(&self) -> bool {
         628  +
        matches!(
         629  +
            &*self.claim.state.lock(),
         630  +
            H2RequestClaimState::Prospective {
         631  +
                response_finished: true,
         632  +
                ..
         633  +
            } | H2RequestClaimState::Accepted {
         634  +
                response_finished: true,
         635  +
                ..
         636  +
            } | H2RequestClaimState::Complete
         637  +
        )
         638  +
    }
         639  +
}

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

@@ -20,20 +671,676 @@
   40     40   
//! DeliveryCancelled --delivery-> removed; return payloads
   41     41   
//! ReadyToEstablish / Ready -----> removed; return the held event
   42     42   
//! Launching --------------------> removed; task settles separately
   43     43   
//! ```
   44     44   
//!
   45     45   
//! Admission updates, returned values, and task wakes are detached for the
   46     46   
//! caller to process after releasing the cell lock.
   47     47   
   48     48   
use super::super::admission::{DemandId, DemandSnapshot, ProtocolRequirement, SnapshotVersion};
   49     49   
use super::super::partition::EligibilityGroup;
   50         -
use super::{AcquisitionEvent, AcquisitionResult, EstablishmentPermit};
          50  +
use super::{AcquisitionOutcome, AcquisitionStep, EstablishmentPermit};
   51     51   
use std::collections::{BTreeSet, HashMap};
   52     52   
use std::num::NonZeroUsize;
   53     53   
use std::task::{Context, Poll, Waker};
   54     54   
   55     55   
/// Local waiter identity within one cell.
   56     56   
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
   57     57   
pub(in crate::client::pool) struct WaiterId(pub(in crate::client::pool) u64);
   58     58   
   59     59   
/// Complete acquisition state protected by one origin cell lock.
   60     60   
///
   61     61   
/// At every completed transition:
   62     62   
///
   63     63   
/// - `records` owns every retained acquisition attempt.
   64     64   
/// - [`WaitingQueueState::Active`] contains exactly the linked
   65     65   
///   [`WaiterState::Waiting`] records.
   66     66   
/// - The active demand identifies and describes the FIFO head.
   67         -
/// - `h1_candidates` contains exactly each H1-compatible
          67  +
/// - `h1_compatible_waiters` contains exactly each H1-compatible
   68     68   
///   [`WaiterState::DeliveryPending`] without a pending H1,
   69     69   
///   [`WaiterState::ReadyToEstablish`], or [`WaiterState::Launching`] attempt.
   70         -
/// - `h2_activation_candidates` contains every delivery-pending, ready-to-establish, or
          70  +
/// - `h2_compatible_waiters` contains every delivery-pending, ready-to-establish, or
   71     71   
///   launching attempt that has not received another result. The waiting FIFO
   72     72   
///   head is compared with this set without duplicating its queue residence.
   73     73   
/// - Delivery-pending, ready-to-establish, launching, delivery-cancelled, and
   74     74   
///   ready attempts remain in `records` but never in the bounded FIFO.
   75     75   
///
   76     76   
/// Transitions return admission publications, values requiring fallback, and
   77     77   
/// wakers to the caller; none runs while this state is locked.
   78     78   
#[derive(Debug, Default)]
   79     79   
pub(super) struct AcquisitionQueue {
   80     80   
    /// Every attempt still waiting, launching, or holding a terminal result.
   81     81   
    records: HashMap<WaiterId, WaiterRecord>,
   82     82   
    /// FIFO endpoints and demand for the records currently waiting.
   83     83   
    waiting: WaitingQueueState,
   84     84   
    /// H1-compatible attempts still competing with a returned sender.
   85         -
    h1_candidates: BTreeSet<WaiterId>,
          85  +
    h1_compatible_waiters: BTreeSet<WaiterId>,
   86     86   
    /// Attempts outside the FIFO that may accept an H2 activation.
   87         -
    h2_activation_candidates: BTreeSet<WaiterId>,
          87  +
    h2_compatible_waiters: BTreeSet<WaiterId>,
   88     88   
    /// Next cell-local waiter identity.
   89         -
    next_waiter: u64,
          89  +
    next_waiter_id: u64,
   90     90   
    /// Next identity for a head-waiter demand generation.
   91     91   
    next_demand_id: u64,
   92     92   
}
   93     93   
   94     94   
/// Owner-runtime progress of one submitted establishment task.
   95     95   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
   96     96   
enum EstablishmentPhase {
   97     97   
    /// The task was submitted but has not begun polling the connector.
   98     98   
    Submitted,
   99     99   
    /// The owner runtime polled the task and the pool owns completion.
  100    100   
    Started,
  101    101   
}
  102    102   
  103    103   
/// Whether the cell has an active FIFO and aggregate head demand.
  104    104   
#[derive(Debug, Default)]
  105    105   
enum WaitingQueueState {
  106    106   
    /// No waiter is linked and no demand is active.
  107    107   
    #[default]
  108    108   
    Empty,
  109    109   
    /// A nonempty FIFO represented by its endpoints, length, and head demand.
  110    110   
    Active {
  111    111   
        /// Oldest waiting record.
  112    112   
        head: WaiterId,
  113    113   
        /// Youngest waiting record.
  114    114   
        tail: WaiterId,
  115    115   
        /// Number of records linked from `head` through `tail`.
  116    116   
        len: NonZeroUsize,
  117    117   
        /// Demand generation represented by `head`.
  118    118   
        demand: DemandTicket,
  119    119   
    },
  120    120   
}
  121    121   
  122         -
/// One request retained while waiting for or owning an acquisition result.
         122  +
/// One request retained while waiting for or owning an acquisition outcome.
  123    123   
#[derive(Debug)]
  124    124   
struct WaiterRecord {
  125    125   
    /// Protocol requirement published while this waiter is the head.
  126    126   
    requirement: ProtocolRequirement,
  127         -
    /// Queue residence, delivery state, and any owned result.
         127  +
    /// Queue residence, delivery state, and any owned outcome.
  128    128   
    state: WaiterState,
  129    129   
}
  130    130   
  131    131   
/// Authoritative cell-local ownership state for one waiter.
  132    132   
#[derive(Debug)]
  133    133   
enum WaiterState {
  134    134   
    /// The waiter is linked in the cell-local FIFO.
  135    135   
    Waiting {
  136    136   
        /// Older waiting record, or `None` at the head.
  137    137   
        previous: Option<WaiterId>,
  138    138   
        /// Newer waiting record, or `None` at the tail.
  139    139   
        next: Option<WaiterId>,
  140         -
        /// Latest task waiting for an acquisition result.
         140  +
        /// Latest task waiting for an acquisition step.
  141    141   
        waker: Option<Waker>,
  142    142   
    },
  143    143   
    /// A committed capacity or reuse delivery reserved this waiter but has
  144    144   
    /// not installed its payload.
  145    145   
    DeliveryPending {
  146    146   
        /// Latest task waiting for the crossing delivery.
  147    147   
        waker: Option<Waker>,
  148         -
        /// Local protocol result that won while delivery was crossing.
  149         -
        pending_result: Option<AcquisitionResult>,
         148  +
        /// Local protocol outcome that won while delivery was crossing.
         149  +
        pending_result: Option<AcquisitionOutcome>,
  150    150   
    },
  151    151   
    /// Cancellation won after reservation while the committed delivery was
  152    152   
    /// outside the lock.
  153    153   
    DeliveryCancelled {
  154    154   
        /// Task detached for wake after the crossing payload is returned.
  155    155   
        waker: Option<Waker>,
  156         -
        /// Protocol result returned after the crossing closes, if one arrived first.
  157         -
        pending_result: Option<AcquisitionResult>,
         156  +
        /// Protocol outcome returned after the crossing closes, if one arrived first.
         157  +
        pending_result: Option<AcquisitionOutcome>,
  158    158   
    },
  159    159   
    /// Capacity is ready to start an establishment attempt.
  160    160   
    ReadyToEstablish {
  161    161   
        /// Optional bounded-origin capacity owned until connection install.
  162    162   
        permit: EstablishmentPermit,
  163    163   
    },
  164    164   
    /// Establishment was submitted to the owner runtime and may be unpolled.
  165    165   
    Launching {
  166    166   
        /// Ownership phase used to distinguish submission from first poll.
  167    167   
        phase: EstablishmentPhase,
  168    168   
        /// Latest task waiting for the attempt or returned H1 to complete.
  169    169   
        waker: Option<Waker>,
  170    170   
    },
  171         -
    /// The waiter owns its terminal acquisition result.
  172         -
    Ready(AcquisitionResult),
         171  +
    /// The waiter owns its terminal acquisition outcome.
         172  +
    Ready(AcquisitionOutcome),
  173    173   
}
  174    174   
  175    175   
/// Cell-local ownership of the aggregate head demand.
  176    176   
#[derive(Debug)]
  177    177   
struct DemandTicket {
  178    178   
    /// Identity of this head-waiter generation.
  179    179   
    id: DemandId,
  180    180   
    /// Version assigned to the next complete publication for this demand
  181    181   
    /// identity.
  182    182   
    version: SnapshotVersion,
  183    183   
    /// Protocol capability required by this head waiter.
  184    184   
    requirement: ProtocolRequirement,
  185    185   
}
  186    186   
  187    187   
impl DemandTicket {
  188    188   
    /// Creates the complete active state published for this head generation.
  189    189   
    fn snapshot(&self, eligibility_group: &EligibilityGroup) -> DemandSnapshot {
  190    190   
        DemandSnapshot::active(
  191    191   
            self.id,
  192    192   
            self.version,
  193    193   
            self.requirement,
  194    194   
            eligibility_group.clone(),
  195    195   
        )
  196    196   
    }
  197    197   
}
  198    198   
  199    199   
impl AcquisitionQueue {
  200    200   
    /// Registers one acquisition attempt.
  201    201   
    ///
  202    202   
    /// An unbounded attempt starts ready to establish. A bounded attempt joins
  203    203   
    /// the capacity FIFO and publishes demand only when it becomes the head.
  204    204   
    pub(super) fn register_waiter(
  205    205   
        &mut self,
  206    206   
        requirement: ProtocolRequirement,
  207    207   
        eligibility_group: &EligibilityGroup,
  208    208   
        bounded: bool,
  209    209   
    ) -> (WaiterId, Option<DemandSnapshot>) {
  210    210   
        let waiter = self.take_waiter_id();
  211    211   
        if !bounded {
  212    212   
            self.records.insert(
  213    213   
                waiter,
  214    214   
                WaiterRecord {
  215    215   
                    requirement,
  216    216   
                    state: WaiterState::ReadyToEstablish {
  217    217   
                        permit: EstablishmentPermit::unbounded(),
  218    218   
                    },
  219    219   
                },
  220    220   
            );
  221         -
            self.add_protocol_candidates(waiter, requirement);
         221  +
            self.add_protocol_waiters(waiter, requirement);
  222    222   
            self.assert_consistent();
  223    223   
            return (waiter, None);
  224    224   
        }
  225    225   
  226    226   
        let previous = match &self.waiting {
  227    227   
            WaitingQueueState::Empty => None,
  228    228   
            WaitingQueueState::Active { tail, .. } => Some(*tail),
  229    229   
        };
  230    230   
        let initial_demand = previous.is_none().then(|| self.new_demand(requirement));
  231    231   
  232    232   
        if let Some(previous) = previous {
  233    233   
            let previous = self
  234    234   
                .records
  235    235   
                .get_mut(&previous)
  236    236   
                .expect("waiting tail disappeared");
  237    237   
            let WaiterState::Waiting { next, .. } = &mut previous.state else {
  238    238   
                unreachable!("waiting tail left the waiting state");
  239    239   
            };
  240    240   
            debug_assert!(next.is_none());
  241    241   
            *next = Some(waiter);
  242    242   
        }
  243    243   
  244    244   
        let replaced = self.records.insert(
  245    245   
            waiter,
  246    246   
            WaiterRecord {
  247    247   
                requirement,
  248    248   
                state: WaiterState::Waiting {
  249    249   
                    previous,
  250    250   
                    next: None,
  251    251   
                    waker: None,
  252    252   
                },
  253    253   
            },
  254    254   
        );
  255    255   
        debug_assert!(replaced.is_none());
  256    256   
  257    257   
        let snapshot = match (&mut self.waiting, initial_demand) {
  258    258   
            (waiting @ WaitingQueueState::Empty, Some(demand)) => {
  259    259   
                let snapshot = demand.snapshot(eligibility_group);
  260    260   
                *waiting = WaitingQueueState::Active {
  261    261   
                    head: waiter,
  262    262   
                    tail: waiter,
  263    263   
                    len: NonZeroUsize::MIN,
  264    264   
                    demand,
  265    265   
                };
  266    266   
                Some(snapshot)
  267    267   
            }
  268    268   
            (WaitingQueueState::Active { tail, len, .. }, None) => {
  269    269   
                *tail = waiter;
  270    270   
                *len = len.checked_add(1).expect("waiter queue length exhausted");
  271    271   
                None
  272    272   
            }
  273    273   
            _ => unreachable!("waiter queue occupancy changed during registration"),
  274    274   
        };
  275    275   
        self.assert_consistent();
  276    276   
        (waiter, snapshot)
  277    277   
    }
  278    278   
  279    279   
    /// Returns the oldest acquisition attempt that can accept HTTP/1.
  280    280   
    ///
  281         -
    /// A bounded waiter remains a candidate after receiving capacity so a
         281  +
    /// A bounded waiter remains compatible after receiving capacity so a
  282    282   
    /// returned sender can still beat its lazy or pool-owned establishment
  283    283   
    /// attempt. Comparing waiter identities preserves arrival order between
  284    284   
    /// that set and the current capacity-waiting head.
  285         -
    fn oldest_h1_candidate(&self) -> Option<WaiterId> {
         285  +
    fn oldest_h1_compatible_waiter(&self) -> Option<WaiterId> {
  286    286   
        let waiting = match &self.waiting {
  287    287   
            WaitingQueueState::Active { head, .. }
  288    288   
                if self.records[head].requirement.accepts_h1() =>
  289    289   
            {
  290    290   
                Some(*head)
  291    291   
            }
  292    292   
            WaitingQueueState::Empty | WaitingQueueState::Active { .. } => None,
  293    293   
        };
  294         -
        match (waiting, self.h1_candidates.first().copied()) {
         294  +
        match (waiting, self.h1_compatible_waiters.first().copied()) {
  295    295   
            (Some(waiting), Some(launching)) => Some(waiting.min(launching)),
  296    296   
            (Some(waiting), None) => Some(waiting),
  297    297   
            (None, launching) => launching,
  298    298   
        }
  299    299   
    }
  300    300   
  301    301   
    /// Returns whether a returned H1 can satisfy a live acquisition attempt.
  302         -
    pub(super) fn can_accept_h1(&self) -> bool {
  303         -
        self.oldest_h1_candidate().is_some()
         302  +
    pub(super) fn has_h1_compatible_waiter(&self) -> bool {
         303  +
        self.oldest_h1_compatible_waiter().is_some()
  304    304   
    }
  305    305   
  306    306   
    /// Returns whether a previously admitted local attempt still accepts H1.
  307    307   
    ///
  308         -
    /// These candidates left the capacity FIFO before its current head and
         308  +
    /// These waiters left the capacity FIFO before its current head and
  309    309   
    /// therefore precede cross-cell reuse for that newer aggregate demand.
  310         -
    pub(super) fn has_prior_h1_candidate(&self) -> bool {
  311         -
        !self.h1_candidates.is_empty()
         310  +
    pub(super) fn has_prior_h1_waiter(&self) -> bool {
         311  +
        !self.h1_compatible_waiters.is_empty()
  312    312   
    }
  313    313   
  314    314   
    /// Returns the oldest attempt that may accept an HTTP/2 activation.
  315         -
    pub(super) fn oldest_h2_candidate(&self) -> Option<WaiterId> {
         315  +
    pub(super) fn oldest_h2_compatible_waiter(&self) -> Option<WaiterId> {
  316    316   
        let waiting = match &self.waiting {
  317    317   
            WaitingQueueState::Active { head, .. }
  318    318   
                if self.records[head].requirement.accepts_h2() =>
  319    319   
            {
  320    320   
                Some(*head)
  321    321   
            }
  322    322   
            WaitingQueueState::Active { .. } => None,
  323    323   
            WaitingQueueState::Empty => None,
  324    324   
        };
  325         -
        match (waiting, self.h2_activation_candidates.first().copied()) {
         325  +
        match (waiting, self.h2_compatible_waiters.first().copied()) {
  326    326   
            (Some(waiting), Some(active)) => Some(waiting.min(active)),
  327    327   
            (Some(waiting), None) => Some(waiting),
  328    328   
            (None, active) => active,
  329    329   
        }
  330    330   
    }
  331    331   
  332         -
    /// Advances the active demand beyond an accepted route publication.
         332  +
    /// Advances the active demand beyond an accepted peer route.
  333    333   
    ///
  334    334   
    /// Admission retires the published version when it acknowledges route
  335    335   
    /// visibility. The waiter remains queued locally while the route gate
  336    336   
    /// offers activations. Reserving the following version here ensures that
  337    337   
    /// route invalidation can republish the same demand generation without an
  338    338   
    /// older inactive snapshot winning.
  339         -
    pub(super) fn suppress_published_demand(&mut self, demand: DemandId) -> bool {
         339  +
    pub(super) fn supersede_demand_snapshot(&mut self, demand: DemandId) -> bool {
  340    340   
        let WaitingQueueState::Active {
  341    341   
            demand: current, ..
  342    342   
        } = &mut self.waiting
  343    343   
        else {
  344    344   
            return false;
  345    345   
        };
  346    346   
        if current.id != demand {
  347    347   
            return false;
  348    348   
        }
  349    349   
        let acknowledged = current.version.next();
  350    350   
        current.version = acknowledged.next();
  351    351   
        self.assert_consistent();
  352    352   
        true
  353    353   
    }
  354    354   
  355    355   
    /// Returns the current head demand for publication after route loss.
  356    356   
    pub(super) fn current_demand_snapshot(
  357    357   
        &self,
  358    358   
        eligibility_group: &EligibilityGroup,
  359    359   
    ) -> Option<DemandSnapshot> {
  360    360   
        match &self.waiting {
  361    361   
            WaitingQueueState::Empty => None,
  362    362   
            WaitingQueueState::Active { demand, .. } => Some(demand.snapshot(eligibility_group)),
  363    363   
        }
  364    364   
    }
  365    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)
         366  +
    /// Returns whether `waiter` is still the oldest H2-compatible waiter.
         367  +
    pub(super) fn is_oldest_h2_compatible_waiter(&self, waiter: WaiterId) -> bool {
         368  +
        self.oldest_h2_compatible_waiter() == Some(waiter)
  369    369   
    }
  370    370   
  371         -
    /// Returns the newest waiter identity committed before a publication.
  372         -
    pub(super) fn publication_cutoff(&self) -> Option<WaiterId> {
         371  +
    /// Returns the newest waiter identity committed before peer route installation.
         372  +
    pub(super) fn route_cutoff(&self) -> Option<WaiterId> {
  373    373   
        (!self.records.is_empty()).then(|| {
  374    374   
            WaiterId(
  375         -
                self.next_waiter
         375  +
                self.next_waiter_id
  376    376   
                    .checked_sub(1)
  377    377   
                    .expect("nonempty waiter set had no allocated identity"),
  378    378   
            )
  379    379   
        })
  380    380   
    }
  381    381   
  382    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()
         383  +
    pub(super) fn has_h2_compatible_waiter_through(&self, cutoff: WaiterId) -> bool {
         384  +
        self.oldest_h2_compatible_waiter()
  385    385   
            .is_some_and(|waiter| waiter <= cutoff)
  386    386   
    }
  387    387   
  388    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()
         389  +
    pub(super) fn has_h2_compatible_waiter(&self) -> bool {
         390  +
        self.oldest_h2_compatible_waiter().is_some()
  391    391   
    }
  392    392   
  393    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)
         394  +
    pub(super) fn is_launching_h2_waiter(&self, waiter: WaiterId) -> bool {
         395  +
        self.h2_compatible_waiters.contains(&waiter)
  396    396   
            && self.records.get(&waiter).is_some_and(|record| {
  397    397   
                record.requirement.accepts_h2()
  398    398   
                    && matches!(record.state, WaiterState::Launching { .. })
  399    399   
            })
  400    400   
    }
  401    401   
  402         -
    /// Adds one waiter to each protocol index its requirement accepts.
  403         -
    fn add_protocol_candidates(&mut self, waiter: WaiterId, requirement: ProtocolRequirement) {
         402  +
    /// Adds one waiter to each compatible-protocol index.
         403  +
    fn add_protocol_waiters(&mut self, waiter: WaiterId, requirement: ProtocolRequirement) {
  404    404   
        if requirement.accepts_h1() {
  405         -
            self.h1_candidates.insert(waiter);
         405  +
            self.h1_compatible_waiters.insert(waiter);
  406    406   
        }
  407    407   
        if requirement.accepts_h2() {
  408         -
            self.h2_activation_candidates.insert(waiter);
         408  +
            self.h2_compatible_waiters.insert(waiter);
  409    409   
        }
  410    410   
    }
  411    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);
         412  +
    /// Removes one waiter from both compatible-protocol indexes.
         413  +
    fn remove_protocol_waiters(&mut self, waiter: WaiterId) {
         414  +
        self.h1_compatible_waiters.remove(&waiter);
         415  +
        self.h2_compatible_waiters.remove(&waiter);
  416    416   
    }
  417    417   
  418    418   
    /// Commits a returned HTTP/1 sender to the oldest compatible attempt.
  419    419   
    ///
  420    420   
    /// Payload construction is delayed until all panic-capable requesting-cell checks
  421    421   
    /// have completed. Once constructed, the result is either state-owned or
  422    422   
    /// returned to the caller for cleanup after the cell lock is released.
  423         -
    pub(super) fn install_returned_h1(
         423  +
    pub(super) fn offer_returned_h1(
  424    424   
        &mut self,
  425         -
        result: impl FnOnce() -> AcquisitionResult,
         425  +
        result: impl FnOnce() -> AcquisitionOutcome,
  426    426   
        eligibility_group: &EligibilityGroup,
  427         -
    ) -> WaiterInstall {
  428         -
        let Some(waiter) = self.oldest_h1_candidate() else {
  429         -
            return WaiterInstall::rejected(result());
         427  +
    ) -> (Option<WaiterId>, WaiterResolution) {
         428  +
        let Some(waiter) = self.oldest_h1_compatible_waiter() else {
         429  +
            return (None, WaiterResolution::refused(result()));
  430    430   
        };
  431         -
        self.install_protocol_result(waiter, result, eligibility_group)
         431  +
        (
         432  +
            Some(waiter),
         433  +
            self.assign_protocol_outcome(waiter, result, eligibility_group),
         434  +
        )
  432    435   
    }
  433    436   
  434    437   
    /// Installs a selected protocol result through the shared waiter transition.
  435         -
    fn install_protocol_result(
         438  +
    fn assign_protocol_outcome(
  436    439   
        &mut self,
  437    440   
        waiter: WaiterId,
  438         -
        result: impl FnOnce() -> AcquisitionResult,
         441  +
        result: impl FnOnce() -> AcquisitionOutcome,
  439    442   
        eligibility_group: &EligibilityGroup,
  440         -
    ) -> WaiterInstall {
         443  +
    ) -> WaiterResolution {
  441    444   
        if matches!(
  442    445   
            self.records.get(&waiter).map(|record| &record.state),
  443    446   
            Some(WaiterState::Waiting { .. })
  444    447   
        ) {
  445    448   
            let removed = self.pop_head(eligibility_group);
  446    449   
            debug_assert_eq!(removed.waiter, waiter);
  447    450   
            let record = self
  448    451   
                .records
  449    452   
                .get_mut(&waiter)
  450    453   
                .expect("selected requesting waiter disappeared");
  451    454   
            let WaiterState::Waiting { waker, .. } = &mut record.state else {
  452    455   
                unreachable!("selected requesting waiter left the waiting state");
  453    456   
            };
  454    457   
            let waker = waker.take();
  455    458   
            record.state = WaiterState::Ready(result());
  456    459   
            let retired =
  457    460   
                DemandSnapshot::inactive(removed.demand.id, removed.demand.version.next());
  458    461   
            self.assert_consistent();
  459         -
            return WaiterInstall {
  460         -
                waiter: Some(waiter),
         462  +
            return WaiterResolution {
  461    463   
                demand_updates: [Some(retired), removed.successor],
  462         -
                returned_event: None,
         464  +
                returned_step: None,
  463    465   
                waker,
  464    466   
            };
  465    467   
        }
  466    468   
  467    469   
        let record = self
  468    470   
            .records
  469    471   
            .get_mut(&waiter)
  470         -
            .expect("selected candidate waiter disappeared");
  471         -
        let (returned_event, waker) = match &mut record.state {
         472  +
            .expect("selected compatible waiter disappeared");
         473  +
        let (returned_step, waker) = match &mut record.state {
  472    474   
            WaiterState::DeliveryPending {
  473    475   
                waker: _,
  474    476   
                pending_result,
  475    477   
            } => {
  476    478   
                debug_assert!(pending_result.is_none());
  477    479   
                *pending_result = Some(result());
  478    480   
                (None, None)
  479    481   
            }
  480    482   
            WaiterState::ReadyToEstablish { .. } | WaiterState::Launching { .. } => {
  481    483   
                let previous = std::mem::replace(&mut record.state, WaiterState::Ready(result()));
  482    484   
                match previous {
  483    485   
                    WaiterState::ReadyToEstablish { permit } => {
  484         -
                        (Some(AcquisitionEvent::Establish(permit)), None)
         486  +
                        (Some(AcquisitionStep::StartEstablishment(permit)), None)
  485    487   
                    }
  486    488   
                    WaiterState::Launching { waker, .. } => (None, waker),
  487         -
                    _ => unreachable!("selected candidate changed state under the cell lock"),
         489  +
                    _ => {
         490  +
                        unreachable!("selected compatible waiter changed state under the cell lock")
         491  +
                    }
  488    492   
                }
  489    493   
            }
  490    494   
            WaiterState::Waiting { .. }
  491    495   
            | WaiterState::DeliveryCancelled { .. }
  492    496   
            | WaiterState::Ready(_) => {
  493    497   
                unreachable!("selected waiter was not eligible for a protocol result")
  494    498   
            }
  495    499   
        };
  496         -
        self.remove_protocol_candidates(waiter);
  497         -
        if returned_event.is_none() {
         500  +
        self.remove_protocol_waiters(waiter);
         501  +
        if returned_step.is_none() {
  498    502   
            // The result is state-owned and no permit is detached, so an
  499    503   
            // invariant failure cannot drop a pool value under this lock.
  500    504   
            self.assert_consistent();
  501    505   
        }
  502         -
        WaiterInstall {
  503         -
            waiter: Some(waiter),
         506  +
        WaiterResolution {
  504    507   
            demand_updates: [None, None],
  505         -
            returned_event,
         508  +
            returned_step,
  506    509   
            waker,
  507    510   
        }
  508    511   
    }
  509    512   
  510    513   
    /// Cancels one waiter and detaches all cross-lock cleanup work.
  511    514   
    ///
  512    515   
    /// Removing the head retires its demand and starts a successor generation.
  513    516   
    /// Removing another waiting record leaves the active demand unchanged.
  514         -
    /// Cancellation during delivery leaves a marker for result installation to
  515         -
    /// observe. A ready result is returned to the caller for cleanup after unlock.
         517  +
    /// Cancellation while delivery is pending leaves a marker for the exact
         518  +
    /// waiter commit to observe. A ready step is returned for cleanup after unlock.
  516    519   
    pub(super) fn cancel_waiter(
  517    520   
        &mut self,
  518    521   
        waiter: WaiterId,
  519    522   
        eligibility_group: &EligibilityGroup,
  520    523   
    ) -> Option<WaiterCancellation> {
  521    524   
        let state = &self.records.get(&waiter)?.state;
  522    525   
        let cancellation = if matches!(state, WaiterState::Waiting { .. }) {
  523    526   
            let is_head = matches!(
  524    527   
                self.waiting,
  525    528   
                WaitingQueueState::Active { head, .. } if head == waiter
  526    529   
            );
  527    530   
            let demand_updates = if is_head {
  528    531   
                let removed = self.pop_head(eligibility_group);
  529    532   
                debug_assert_eq!(waiter, removed.waiter);
  530    533   
                let record = self
  531    534   
                    .records
  532    535   
                    .remove(&waiter)
  533    536   
                    .expect("cancelled head waiter disappeared");
  534    537   
                debug_assert!(matches!(record.state, WaiterState::Waiting { .. }));
  535    538   
                let retired =
  536    539   
                    DemandSnapshot::inactive(removed.demand.id, removed.demand.version.next());
  537    540   
                [Some(retired), removed.successor]
  538    541   
            } else {
  539    542   
                self.remove_non_head(waiter);
  540    543   
                [None, None]
  541    544   
            };
  542    545   
            WaiterCancellation {
  543    546   
                demand_updates,
  544         -
                returned_events: [None, None],
         547  +
                returned_steps: [None, None],
  545    548   
            }
  546    549   
        } else if matches!(state, WaiterState::DeliveryPending { .. }) {
  547         -
            self.remove_protocol_candidates(waiter);
         550  +
            self.remove_protocol_waiters(waiter);
  548    551   
            let record = self.records.get_mut(&waiter)?;
  549    552   
            let WaiterState::DeliveryPending {
  550    553   
                waker,
  551    554   
                pending_result,
  552    555   
            } = &mut record.state
  553    556   
            else {
  554    557   
                unreachable!("delivery-pending waiter changed state under the cell lock");
  555    558   
            };
  556    559   
            let waker = waker.take();
  557    560   
            let pending_result = pending_result.take();
  558    561   
            record.state = WaiterState::DeliveryCancelled {
  559    562   
                waker,
  560    563   
                pending_result,
  561    564   
            };
  562    565   
            WaiterCancellation {
  563    566   
                demand_updates: [None, None],
  564         -
                returned_events: [None, None],
         567  +
                returned_steps: [None, None],
  565    568   
            }
  566    569   
        } else if matches!(
  567    570   
            state,
  568    571   
            WaiterState::ReadyToEstablish { .. } | WaiterState::Ready(_)
  569    572   
        ) {
  570    573   
            // Validate before detaching the event. Once it is local,
  571    574   
            // this method must return without another panic-capable check.
  572    575   
            self.assert_consistent();
  573    576   
            let record = self.records.remove(&waiter)?;
  574         -
            self.remove_protocol_candidates(waiter);
         577  +
            self.remove_protocol_waiters(waiter);
  575    578   
            let event = match record.state {
  576         -
                WaiterState::ReadyToEstablish { permit } => AcquisitionEvent::Establish(permit),
  577         -
                WaiterState::Ready(result) => AcquisitionEvent::Complete(result),
         579  +
                WaiterState::ReadyToEstablish { permit } => {
         580  +
                    AcquisitionStep::StartEstablishment(permit)
         581  +
                }
         582  +
                WaiterState::Ready(result) => AcquisitionStep::Resolved(result),
  578    583   
                _ => unreachable!("ready waiter changed state under the cell lock"),
  579    584   
            };
  580    585   
            return Some(WaiterCancellation {
  581    586   
                demand_updates: [None, None],
  582         -
                returned_events: [Some(event), None],
         587  +
                returned_steps: [Some(event), None],
  583    588   
            });
  584    589   
        } else if matches!(state, WaiterState::Launching { .. }) {
  585    590   
            self.assert_consistent();
  586         -
            self.remove_protocol_candidates(waiter);
         591  +
            self.remove_protocol_waiters(waiter);
  587    592   
            self.records.remove(&waiter)?;
  588    593   
            return Some(WaiterCancellation {
  589    594   
                demand_updates: [None, None],
  590         -
                returned_events: [None, None],
         595  +
                returned_steps: [None, None],
  591    596   
            });
  592    597   
        } else {
  593    598   
            debug_assert!(matches!(state, WaiterState::DeliveryCancelled { .. }));
  594    599   
            return None;
  595    600   
        };
  596    601   
  597    602   
        self.assert_consistent();
  598    603   
        Some(cancellation)
  599    604   
    }
  600    605   
  601    606   
    /// Returns the next event or records the latest waker for a pending waiter.
  602    607   
    ///
  603    608   
    /// # Panics
  604    609   
    ///
  605    610   
    /// Panics if `waiter` is unknown, was cancelled, or was already consumed
  606    611   
    /// by an earlier ready poll.
  607    612   
    pub(super) fn poll_waiter(
  608    613   
        &mut self,
  609    614   
        waiter: WaiterId,
  610    615   
        cx: &mut Context<'_>,
  611         -
    ) -> Poll<AcquisitionEvent> {
         616  +
    ) -> Poll<AcquisitionStep> {
  612    617   
        if matches!(
  613    618   
            self.records.get(&waiter).map(|record| &record.state),
  614    619   
            Some(WaiterState::ReadyToEstablish { .. })
  615    620   
        ) {
  616    621   
            self.assert_consistent();
  617    622   
            let record = self
  618    623   
                .records
  619    624   
                .get_mut(&waiter)
  620    625   
                .expect("ready establishment waiter disappeared");
  621    626   
            let previous = std::mem::replace(
  622    627   
                &mut record.state,
  623    628   
                WaiterState::Launching {
  624    629   
                    phase: EstablishmentPhase::Submitted,
  625    630   
                    waker: None,
  626    631   
                },
  627    632   
            );
  628    633   
            let WaiterState::ReadyToEstablish { permit } = previous else {
  629    634   
                unreachable!("ready establishment waiter changed state under the cell lock");
  630    635   
            };
  631         -
            return Poll::Ready(AcquisitionEvent::Establish(permit));
         636  +
            return Poll::Ready(AcquisitionStep::StartEstablishment(permit));
  632    637   
        }
  633    638   
  634    639   
        if matches!(
  635    640   
            self.records.get(&waiter).map(|record| &record.state),
  636    641   
            Some(WaiterState::Ready(_))
  637    642   
        ) {
  638    643   
            return Poll::Ready(
  639    644   
                self.take_ready_result(waiter)
  640         -
                    .map(AcquisitionEvent::Complete)
  641         -
                    .expect("ready waiter lost its acquisition result"),
         645  +
                    .map(AcquisitionStep::Resolved)
         646  +
                    .expect("ready waiter lost its acquisition outcome"),
  642    647   
            );
  643    648   
        }
  644    649   
  645    650   
        let record = self
  646    651   
            .records
  647    652   
            .get_mut(&waiter)
  648    653   
            .expect("polled a cancelled, consumed, or unknown acquisition waiter");
  649    654   
        let waker = match &mut record.state {
  650    655   
            WaiterState::Waiting { waker, .. }
  651    656   
            | WaiterState::DeliveryPending { waker, .. }
@@ -703,708 +991,976 @@
  723    728   
            .expect("reserved queue head disappeared");
  724    729   
        let WaiterState::Waiting { waker, .. } = &mut record.state else {
  725    730   
            unreachable!("reserved queue head left the waiting state");
  726    731   
        };
  727    732   
        let waker = waker.take();
  728    733   
        let requirement = record.requirement;
  729    734   
        record.state = WaiterState::DeliveryPending {
  730    735   
            waker,
  731    736   
            pending_result: None,
  732    737   
        };
  733         -
        self.add_protocol_candidates(removed.waiter, requirement);
         738  +
        self.add_protocol_waiters(removed.waiter, requirement);
  734    739   
  735    740   
        self.assert_consistent();
  736    741   
        DeliveryReservation::Reserved {
  737    742   
            waiter: removed.waiter,
  738    743   
            successor: removed.successor,
  739    744   
        }
  740    745   
    }
  741    746   
  742    747   
    /// Completes the unlocked capacity crossing without panicking while the
  743    748   
    /// incoming permit remains a droppable local.
  744    749   
    ///
  745    750   
    /// A live receiver takes ownership of the permit before another
  746    751   
    /// panic-capable operation. If an H1 return already won, both resources
  747    752   
    /// leave the lock as separate events and the permit is refunnelled.
  748         -
    pub(super) fn install_capacity(
         753  +
    pub(super) fn commit_capacity(
  749    754   
        &mut self,
  750    755   
        waiter: WaiterId,
  751    756   
        permit: EstablishmentPermit,
  752         -
    ) -> ResultInstall {
         757  +
    ) -> CellCommitOutcome {
  753    758   
        let Some(record) = self.records.get_mut(&waiter) else {
  754         -
            return ResultInstall::invalid(
  755         -
                AcquisitionEvent::Establish(permit),
  756         -
                ResultInstallError::MissingWaiter,
         759  +
            return CellCommitOutcome::invalid(
         760  +
                AcquisitionStep::StartEstablishment(permit),
         761  +
                CellCommitError::MissingWaiter,
  757    762   
            );
  758    763   
        };
  759    764   
  760    765   
        match &mut record.state {
  761    766   
            WaiterState::DeliveryPending {
  762    767   
                waker,
  763    768   
                pending_result,
  764    769   
            } => {
  765    770   
                let waker = waker.take();
  766    771   
                if let Some(result) = pending_result.take() {
  767    772   
                    record.state = WaiterState::Ready(result);
  768         -
                    self.remove_protocol_candidates(waiter);
         773  +
                    self.remove_protocol_waiters(waiter);
  769    774   
                    // The rejected permit must cross the lock boundary even
  770    775   
                    // if other state is already inconsistent.
  771         -
                    return ResultInstall {
  772         -
                        returned_events: [Some(AcquisitionEvent::Establish(permit)), None],
         776  +
                    return CellCommitOutcome::Refused {
         777  +
                        returned: [Some(AcquisitionStep::StartEstablishment(permit)), None],
  773    778   
                        waker,
  774         -
                        error: None,
  775         -
                        accepted: false,
  776    779   
                    };
  777    780   
                }
  778    781   
                record.state = WaiterState::ReadyToEstablish { permit };
  779    782   
                // The permit is state-owned before this check can panic.
  780    783   
                self.assert_consistent();
  781         -
                ResultInstall {
  782         -
                    returned_events: [None, None],
  783         -
                    waker,
  784         -
                    error: None,
  785         -
                    accepted: true,
  786         -
                }
         784  +
                CellCommitOutcome::Committed { waker }
  787    785   
            }
  788    786   
            WaiterState::DeliveryCancelled {
  789    787   
                waker,
  790    788   
                pending_result,
  791    789   
            } => {
  792    790   
                let waker = waker.take();
  793    791   
                let pending_result = pending_result.take();
  794    792   
                self.records.remove(&waiter);
  795         -
                ResultInstall {
  796         -
                    returned_events: [
  797         -
                        pending_result.map(AcquisitionEvent::Complete),
  798         -
                        Some(AcquisitionEvent::Establish(permit)),
         793  +
                CellCommitOutcome::Refused {
         794  +
                    returned: [
         795  +
                        pending_result.map(AcquisitionStep::Resolved),
         796  +
                        Some(AcquisitionStep::StartEstablishment(permit)),
  799    797   
                    ],
  800    798   
                    waker,
  801         -
                    error: None,
  802         -
                    accepted: false,
  803    799   
                }
  804    800   
            }
  805    801   
            WaiterState::Waiting { .. }
  806    802   
            | WaiterState::ReadyToEstablish { .. }
  807    803   
            | WaiterState::Launching { .. }
  808         -
            | WaiterState::Ready(_) => ResultInstall::invalid(
  809         -
                AcquisitionEvent::Establish(permit),
  810         -
                ResultInstallError::UnexpectedState,
         804  +
            | WaiterState::Ready(_) => CellCommitOutcome::invalid(
         805  +
                AcquisitionStep::StartEstablishment(permit),
         806  +
                CellCommitError::UnexpectedState,
  811    807   
            ),
  812    808   
        }
  813    809   
    }
  814    810   
  815    811   
    /// Installs a borrowed HTTP/1 result after its requesting waiter was reserved.
  816    812   
    ///
  817    813   
    /// A local return may have won while the borrowed sender crossed its
  818    814   
    /// owning-cell and requesting-cell locks. In that case the local result stays ready and
  819    815   
    /// the borrowed result leaves the lock for ordinary owning-cell return.
  820         -
    pub(super) fn install_borrowed_h1(
         816  +
    pub(super) fn commit_borrowed_h1(
  821    817   
        &mut self,
  822    818   
        waiter: WaiterId,
  823         -
        result: AcquisitionResult,
  824         -
    ) -> ResultInstall {
         819  +
        result: AcquisitionOutcome,
         820  +
    ) -> CellCommitOutcome {
  825    821   
        let Some(record) = self.records.get_mut(&waiter) else {
  826         -
            return ResultInstall::invalid(
  827         -
                AcquisitionEvent::Complete(result),
  828         -
                ResultInstallError::MissingWaiter,
         822  +
            return CellCommitOutcome::invalid(
         823  +
                AcquisitionStep::Resolved(result),
         824  +
                CellCommitError::MissingWaiter,
  829    825   
            );
  830    826   
        };
  831    827   
  832    828   
        match &mut record.state {
  833    829   
            WaiterState::DeliveryPending {
  834    830   
                waker,
  835    831   
                pending_result,
  836    832   
            } => {
  837    833   
                let waker = waker.take();
  838    834   
                if let Some(local_result) = pending_result.take() {
  839    835   
                    record.state = WaiterState::Ready(local_result);
  840         -
                    self.remove_protocol_candidates(waiter);
         836  +
                    self.remove_protocol_waiters(waiter);
  841    837   
                    // The rejected borrowed sender must cross the lock
  842    838   
                    // boundary before any panic-capable consistency check.
  843         -
                    return ResultInstall {
  844         -
                        returned_events: [Some(AcquisitionEvent::Complete(result)), None],
         839  +
                    return CellCommitOutcome::Refused {
         840  +
                        returned: [Some(AcquisitionStep::Resolved(result)), None],
  845    841   
                        waker,
  846         -
                        error: None,
  847         -
                        accepted: false,
  848    842   
                    };
  849    843   
                }
  850    844   
                record.state = WaiterState::Ready(result);
  851         -
                self.remove_protocol_candidates(waiter);
         845  +
                self.remove_protocol_waiters(waiter);
  852    846   
                self.assert_consistent();
  853         -
                ResultInstall {
  854         -
                    returned_events: [None, None],
  855         -
                    waker,
  856         -
                    error: None,
  857         -
                    accepted: true,
  858         -
                }
         847  +
                CellCommitOutcome::Committed { waker }
  859    848   
            }
  860    849   
            WaiterState::DeliveryCancelled {
  861    850   
                waker,
  862    851   
                pending_result,
  863    852   
            } => {
  864    853   
                let waker = waker.take();
  865    854   
                let pending_result = pending_result.take();
  866    855   
                self.records.remove(&waiter);
  867         -
                ResultInstall {
  868         -
                    returned_events: [
  869         -
                        pending_result.map(AcquisitionEvent::Complete),
  870         -
                        Some(AcquisitionEvent::Complete(result)),
         856  +
                CellCommitOutcome::Refused {
         857  +
                    returned: [
         858  +
                        pending_result.map(AcquisitionStep::Resolved),
         859  +
                        Some(AcquisitionStep::Resolved(result)),
  871    860   
                    ],
  872    861   
                    waker,
  873         -
                    error: None,
  874         -
                    accepted: false,
  875    862   
                }
  876    863   
            }
  877    864   
            WaiterState::Waiting { .. }
  878    865   
            | WaiterState::ReadyToEstablish { .. }
  879    866   
            | WaiterState::Launching { .. }
  880         -
            | WaiterState::Ready(_) => ResultInstall::invalid(
  881         -
                AcquisitionEvent::Complete(result),
  882         -
                ResultInstallError::UnexpectedState,
         867  +
            | WaiterState::Ready(_) => CellCommitOutcome::invalid(
         868  +
                AcquisitionStep::Resolved(result),
         869  +
                CellCommitError::UnexpectedState,
  883    870   
            ),
  884    871   
        }
  885    872   
    }
  886    873   
  887    874   
    /// Commits one terminal establishment result when its waiter is still live.
  888    875   
    ///
  889    876   
    /// Completion may occur from either launch phase: normal execution starts
  890    877   
    /// the connector first, while dropping an unpolled submitted task installs
  891    878   
    /// a terminal runtime error.
  892         -
    pub(super) fn install_establishment_result(
         879  +
    pub(super) fn commit_establishment(
  893    880   
        &mut self,
  894    881   
        waiter: WaiterId,
  895         -
        result: AcquisitionResult,
  896         -
    ) -> ResultInstall {
         882  +
        result: AcquisitionOutcome,
         883  +
    ) -> CellCommitOutcome {
  897    884   
        let Some(record) = self.records.get_mut(&waiter) else {
  898         -
            return ResultInstall::rejected(AcquisitionEvent::Complete(result));
         885  +
            return CellCommitOutcome::refused(AcquisitionStep::Resolved(result));
  899    886   
        };
  900    887   
        if matches!(record.state, WaiterState::Ready(_)) {
  901         -
            return ResultInstall::rejected(AcquisitionEvent::Complete(result));
         888  +
            return CellCommitOutcome::refused(AcquisitionStep::Resolved(result));
  902    889   
        }
  903    890   
        if !matches!(record.state, WaiterState::Launching { .. }) {
  904         -
            return ResultInstall::invalid(
  905         -
                AcquisitionEvent::Complete(result),
  906         -
                ResultInstallError::UnexpectedState,
         891  +
            return CellCommitOutcome::invalid(
         892  +
                AcquisitionStep::Resolved(result),
         893  +
                CellCommitError::UnexpectedState,
  907    894   
            );
  908    895   
        }
  909    896   
        let WaiterState::Launching { waker, .. } = &mut record.state else {
  910    897   
            unreachable!("validated launching waiter changed state")
  911    898   
        };
  912    899   
        let waker = waker.take();
  913    900   
        record.state = WaiterState::Ready(result);
  914         -
        self.remove_protocol_candidates(waiter);
         901  +
        self.remove_protocol_waiters(waiter);
  915    902   
        self.assert_consistent();
  916         -
        ResultInstall {
  917         -
            returned_events: [None, None],
  918         -
            waker,
  919         -
            error: None,
  920         -
            accepted: true,
  921         -
        }
         903  +
        CellCommitOutcome::Committed { waker }
  922    904   
    }
  923    905   
  924    906   
    /// Removes a ready waiter and transfers ownership of its result.
  925         -
    pub(super) fn take_ready_result(&mut self, waiter: WaiterId) -> Option<AcquisitionResult> {
         907  +
    pub(super) fn take_ready_result(&mut self, waiter: WaiterId) -> Option<AcquisitionOutcome> {
  926    908   
        if !matches!(
  927    909   
            self.records.get(&waiter).map(|record| &record.state),
  928    910   
            Some(WaiterState::Ready(_))
  929    911   
        ) {
  930    912   
            return None;
  931    913   
        }
  932    914   
  933    915   
        // Validate before detaching the result. Returning it must be the last
  934    916   
        // operation performed while the cell lock is held.
  935    917   
        self.assert_consistent();
  936    918   
        let record = self.records.remove(&waiter)?;
  937    919   
        let WaiterState::Ready(result) = record.state else {
  938    920   
            unreachable!("ready waiter changed state under the cell lock");
  939    921   
        };
  940    922   
        Some(result)
  941    923   
    }
  942    924   
  943    925   
    /// Offers an H2 activation to the oldest compatible acquisition.
  944    926   
    ///
  945         -
    /// `cutoff` retains publication priority. A later waiter is left pending
         927  +
    /// `cutoff` retains peer-route priority. A later waiter is left pending
  946    928   
    /// until every live acquisition at or before the cutoff has received an
  947    929   
    /// activation opportunity.
  948         -
    pub(super) fn install_h2(
         930  +
    pub(super) fn offer_h2_activation(
  949    931   
        &mut self,
  950    932   
        cutoff: Option<WaiterId>,
  951         -
        result: impl FnOnce(WaiterId) -> AcquisitionResult,
         933  +
        result: impl FnOnce(WaiterId) -> AcquisitionOutcome,
  952    934   
        eligibility_group: &EligibilityGroup,
  953         -
    ) -> WaiterInstall {
  954         -
        let Some(waiter) = self.oldest_h2_candidate() else {
  955         -
            return WaiterInstall::empty();
         935  +
    ) -> (Option<WaiterId>, WaiterResolution) {
         936  +
        let Some(waiter) = self.oldest_h2_compatible_waiter() else {
         937  +
            return (None, WaiterResolution::empty());
  956    938   
        };
  957    939   
        if cutoff.is_some_and(|cutoff| waiter > cutoff) {
  958         -
            return WaiterInstall::empty();
         940  +
            return (None, WaiterResolution::empty());
  959    941   
        }
  960    942   
  961         -
        self.install_protocol_result(waiter, || result(waiter), eligibility_group)
         943  +
        (
         944  +
            Some(waiter),
         945  +
            self.assign_protocol_outcome(waiter, || result(waiter), eligibility_group),
         946  +
        )
  962    947   
    }
  963    948   
  964    949   
    /// Unlinks the FIFO head and installs any successor demand.
  965    950   
    ///
  966    951   
    /// The head record remains in `records`; the caller either changes it to
  967    952   
    /// `DeliveryPending` or removes it for cancellation.
  968    953   
    fn pop_head(&mut self, eligibility_group: &EligibilityGroup) -> RemovedHead {
  969    954   
        let waiting = std::mem::take(&mut self.waiting);
  970    955   
        let WaitingQueueState::Active {
  971    956   
            head,
@@ -1071,1056 +1158,1143 @@
 1091   1076   
            len.get()
 1092   1077   
                .checked_sub(1)
 1093   1078   
                .expect("waiter queue length underflowed"),
 1094   1079   
        )
 1095   1080   
        .expect("removing a non-head waiter emptied the queue");
 1096   1081   
        record
 1097   1082   
    }
 1098   1083   
 1099   1084   
    /// Allocates an identity that is never reused within this cell.
 1100   1085   
    fn take_waiter_id(&mut self) -> WaiterId {
 1101         -
        let value = self.next_waiter;
 1102         -
        self.next_waiter = value.checked_add(1).expect("waiter identity exhausted");
        1086  +
        let value = self.next_waiter_id;
        1087  +
        self.next_waiter_id = value.checked_add(1).expect("waiter identity exhausted");
 1103   1088   
        WaiterId(value)
 1104   1089   
    }
 1105   1090   
 1106   1091   
    /// Starts a demand generation for a waiter that became the FIFO head.
 1107   1092   
    fn new_demand(&mut self, requirement: ProtocolRequirement) -> DemandTicket {
 1108   1093   
        let value = self.next_demand_id;
 1109   1094   
        self.next_demand_id = value.checked_add(1).expect("demand identity exhausted");
 1110   1095   
        DemandTicket {
 1111   1096   
            id: DemandId::from_u64(value),
 1112   1097   
            version: SnapshotVersion::INITIAL,
 1113   1098   
            requirement,
 1114   1099   
        }
 1115   1100   
    }
 1116   1101   
 1117   1102   
    /// Checks map, FIFO, and demand relationships in debug and test builds.
 1118   1103   
    pub(super) fn assert_consistent(&self) {
 1119         -
        #[cfg(debug_assertions)]
        1104  +
        #[cfg(any(debug_assertions, test))]
 1120   1105   
        {
 1121   1106   
            if std::thread::panicking() {
 1122   1107   
                return;
 1123   1108   
            }
 1124   1109   
            self.assert_consistent_debug();
 1125   1110   
        }
 1126   1111   
    }
 1127   1112   
 1128         -
    #[cfg(debug_assertions)]
        1113  +
    #[cfg(any(debug_assertions, test))]
 1129   1114   
    fn assert_consistent_debug(&self) {
 1130   1115   
        let waiting_records = self
 1131   1116   
            .records
 1132   1117   
            .values()
 1133   1118   
            .filter(|record| matches!(record.state, WaiterState::Waiting { .. }))
 1134   1119   
            .count();
 1135   1120   
        match &self.waiting {
 1136   1121   
            WaitingQueueState::Empty => {
 1137   1122   
                assert_eq!(0, waiting_records, "empty queue retained waiting records");
 1138   1123   
            }
@@ -1163,1148 +1412,1400 @@
 1183   1168   
                    traversed,
 1184   1169   
                    "waiter queue length did not match its links"
 1185   1170   
                );
 1186   1171   
                assert_eq!(
 1187   1172   
                    waiting_records, traversed,
 1188   1173   
                    "waiting record was not reachable from the queue head"
 1189   1174   
                );
 1190   1175   
            }
 1191   1176   
        }
 1192   1177   
 1193         -
        let expected_h1_candidates = self
        1178  +
        let expected_h1_waiters = self
 1194   1179   
            .records
 1195   1180   
            .iter()
 1196   1181   
            .filter_map(|(waiter, record)| {
 1197   1182   
                (record.requirement.accepts_h1()
 1198   1183   
                    && matches!(
 1199   1184   
                        record.state,
 1200   1185   
                        WaiterState::DeliveryPending {
 1201   1186   
                            pending_result: None,
 1202   1187   
                            ..
 1203   1188   
                        } | WaiterState::ReadyToEstablish { .. }
 1204   1189   
                            | WaiterState::Launching { .. }
 1205   1190   
                    ))
 1206   1191   
                .then_some(*waiter)
 1207   1192   
            })
 1208   1193   
            .collect::<BTreeSet<_>>();
 1209   1194   
        assert_eq!(
 1210         -
            expected_h1_candidates, self.h1_candidates,
 1211         -
            "HTTP/1 acquisition candidates did not match waiter state"
        1195  +
            expected_h1_waiters, self.h1_compatible_waiters,
        1196  +
            "HTTP/1 compatible-waiter index did not match waiter state"
 1212   1197   
        );
 1213   1198   
 1214         -
        let expected_h2_activation_candidates = self
        1199  +
        let expected_h2_waiters = self
 1215   1200   
            .records
 1216   1201   
            .iter()
 1217   1202   
            .filter_map(|(waiter, record)| {
 1218   1203   
                (record.requirement.accepts_h2()
 1219   1204   
                    && matches!(
 1220   1205   
                        record.state,
 1221   1206   
                        WaiterState::DeliveryPending {
 1222   1207   
                            pending_result: None,
 1223   1208   
                            ..
 1224   1209   
                        } | WaiterState::ReadyToEstablish { .. }
 1225   1210   
                            | WaiterState::Launching { .. }
 1226   1211   
                    ))
 1227   1212   
                .then_some(*waiter)
 1228   1213   
            })
 1229   1214   
            .collect::<BTreeSet<_>>();
 1230   1215   
        assert_eq!(
 1231         -
            expected_h2_activation_candidates, self.h2_activation_candidates,
 1232         -
            "HTTP/2 acquisition candidates did not match waiter state"
        1216  +
            expected_h2_waiters, self.h2_compatible_waiters,
        1217  +
            "HTTP/2 compatible-waiter index did not match waiter state"
 1233   1218   
        );
 1234   1219   
    }
 1235   1220   
 1236   1221   
    #[cfg(test)]
 1237         -
    pub(super) fn snapshot(&self) -> CellSnapshot {
        1222  +
    pub(super) fn probe(&self) -> AcquisitionProbe {
 1238   1223   
        let (waiting, demand) = match &self.waiting {
 1239   1224   
            WaitingQueueState::Empty => (0, None),
 1240   1225   
            WaitingQueueState::Active { len, demand, .. } => (len.get(), Some(demand.id)),
 1241   1226   
        };
 1242         -
        CellSnapshot {
        1227  +
        AcquisitionProbe {
 1243   1228   
            waiting,
 1244   1229   
            retained: self.records.len(),
 1245   1230   
            demand,
 1246   1231   
        }
 1247   1232   
    }
 1248   1233   
}
 1249   1234   
 1250   1235   
/// Result of reserving a requesting waiter for one unlocked delivery.
 1251   1236   
pub(super) enum DeliveryReservation {
 1252   1237   
    /// The current head was reserved and may have a successor demand.
 1253   1238   
    Reserved {
 1254   1239   
        waiter: WaiterId,
 1255   1240   
        successor: Option<DemandSnapshot>,
 1256   1241   
    },
 1257   1242   
    /// The delivery no longer matches live cell demand.
 1258   1243   
    Rejected,
 1259   1244   
}
 1260   1245   
 1261   1246   
/// Values detached from cell state by cancellation.
 1262   1247   
pub(super) struct WaiterCancellation {
 1263   1248   
    /// Demand retirement and optional successor published after unlocking.
 1264   1249   
    pub(super) demand_updates: [Option<DemandSnapshot>; 2],
 1265   1250   
    /// Intermediate and terminal events returned for cleanup after unlocking.
 1266         -
    pub(super) returned_events: [Option<AcquisitionEvent>; 2],
        1251  +
    pub(super) returned_steps: [Option<AcquisitionStep>; 2],
 1267   1252   
}
 1268   1253   
 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>,
        1254  +
/// Values detached after offering a selected protocol result.
        1255  +
pub(super) struct WaiterResolution {
 1273   1256   
    /// Demand retirement and optional successor published after unlocking.
 1274   1257   
    pub(super) demand_updates: [Option<DemandSnapshot>; 2],
 1275   1258   
    /// Establishment permit or rejected result returned after unlocking.
 1276         -
    pub(super) returned_event: Option<AcquisitionEvent>,
        1259  +
    pub(super) returned_step: Option<AcquisitionStep>,
 1277   1260   
    /// Waiting task woken after demand publication.
 1278   1261   
    pub(super) waker: Option<Waker>,
 1279   1262   
}
 1280   1263   
 1281         -
impl WaiterInstall {
        1264  +
impl WaiterResolution {
 1282   1265   
    /// Preserves a result when no live attempt can accept HTTP/1.
 1283         -
    fn rejected(result: AcquisitionResult) -> Self {
        1266  +
    fn refused(result: AcquisitionOutcome) -> Self {
 1284   1267   
        Self {
 1285         -
            waiter: None,
 1286   1268   
            demand_updates: [None, None],
 1287         -
            returned_event: Some(AcquisitionEvent::Complete(result)),
        1269  +
            returned_step: Some(AcquisitionStep::Resolved(result)),
 1288   1270   
            waker: None,
 1289   1271   
        }
 1290   1272   
    }
 1291   1273   
 1292   1274   
    /// Reports that no live attempt can accept an HTTP/2 activation.
 1293   1275   
    fn empty() -> Self {
 1294   1276   
        Self {
 1295         -
            waiter: None,
 1296   1277   
            demand_updates: [None, None],
 1297         -
            returned_event: None,
        1278  +
            returned_step: None,
 1298   1279   
            waker: None,
 1299   1280   
        }
 1300   1281   
    }
 1301   1282   
}
 1302   1283   
 1303   1284   
/// Invalid state observed while a committed event was crossing to the cell.
 1304   1285   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 1305         -
pub(super) enum ResultInstallError {
        1286  +
pub(super) enum CellCommitError {
 1306   1287   
    /// The waiter reserved for the delivery no longer exists.
 1307   1288   
    MissingWaiter,
 1308   1289   
    /// The waiter exists but cannot receive the reserved delivery.
 1309   1290   
    UnexpectedState,
 1310   1291   
}
 1311   1292   
 1312         -
/// Values produced after installing or rejecting a committed event.
 1313         -
pub(super) struct ResultInstall {
 1314         -
    /// Events rejected by cancellation and returned after unlocking.
 1315         -
    ///
 1316         -
    /// When both are present, the H1 fallback precedes the capacity fallback
 1317         -
    /// so successor demand can reuse the connection before starting another.
 1318         -
    pub(super) returned_events: [Option<AcquisitionEvent>; 2],
 1319         -
    /// Waiting task woken after the delivery fence closes.
 1320         -
    pub(super) waker: Option<Waker>,
 1321         -
    /// Invalid state reported only after the returned result runs its fallback.
 1322         -
    pub(super) error: Option<ResultInstallError>,
 1323         -
    /// Whether cell state became authoritative for the incoming event.
 1324         -
    pub(super) accepted: bool,
        1293  +
/// Result of committing one exact acquisition step to a reserved waiter.
        1294  +
pub(super) enum CellCommitOutcome {
        1295  +
    /// Cell state accepted the step and became authoritative for it.
        1296  +
    Committed {
        1297  +
        /// Waiting task woken after assignment settlement.
        1298  +
        waker: Option<Waker>,
        1299  +
    },
        1300  +
    /// The waiter no longer accepted the step; fallback remains valid.
        1301  +
    Refused {
        1302  +
        /// Steps returned after unlocking.
        1303  +
        returned: [Option<AcquisitionStep>; 2],
        1304  +
        /// Waiting task woken after assignment settlement.
        1305  +
        waker: Option<Waker>,
        1306  +
    },
        1307  +
    /// The reserved waiter disappeared or entered an impossible state.
        1308  +
    Invalid {
        1309  +
        /// Steps returned before reporting the invariant failure.
        1310  +
        returned: [Option<AcquisitionStep>; 2],
        1311  +
        /// Waiting task, if one remained attached.
        1312  +
        waker: Option<Waker>,
        1313  +
        /// Invalid cell transition observed under the lock.
        1314  +
        error: CellCommitError,
        1315  +
    },
 1325   1316   
}
 1326   1317   
 1327         -
impl ResultInstall {
 1328         -
    /// Preserves the incoming result for unlocked cleanup after invalid state.
 1329         -
    fn invalid(event: AcquisitionEvent, error: ResultInstallError) -> Self {
 1330         -
        Self {
 1331         -
            returned_events: [Some(event), None],
        1318  +
impl CellCommitOutcome {
        1319  +
    /// Preserves the incoming step for unlocked cleanup after invalid state.
        1320  +
    fn invalid(step: AcquisitionStep, error: CellCommitError) -> Self {
        1321  +
        Self::Invalid {
        1322  +
            returned: [Some(step), None],
 1332   1323   
            waker: None,
 1333         -
            error: Some(error),
 1334         -
            accepted: false,
        1324  +
            error,
 1335   1325   
        }
 1336   1326   
    }
 1337   1327   
 1338         -
    /// Preserves a losing result for its ordinary unlocked fallback.
 1339         -
    fn rejected(event: AcquisitionEvent) -> Self {
 1340         -
        Self {
 1341         -
            returned_events: [Some(event), None],
        1328  +
    /// Preserves a losing step for its ordinary unlocked fallback.
        1329  +
    fn refused(step: AcquisitionStep) -> Self {
        1330  +
        Self::Refused {
        1331  +
            returned: [Some(step), None],
 1342   1332   
            waker: None,
 1343         -
            error: None,
 1344         -
            accepted: false,
 1345   1333   
        }
 1346   1334   
    }
 1347   1335   
}
 1348   1336   
 1349   1337   
/// FIFO state detached while advancing the active head.
 1350   1338   
struct RemovedHead {
 1351   1339   
    /// Identity of the unlinked waiter.
 1352   1340   
    waiter: WaiterId,
 1353   1341   
    /// Retired demand generation that represented this head.
 1354   1342   
    demand: DemandTicket,
 1355   1343   
    /// New demand published if another waiter became the head.
 1356   1344   
    successor: Option<DemandSnapshot>,
 1357   1345   
}
 1358   1346   
 1359   1347   
#[cfg(test)]
 1360   1348   
#[derive(Debug)]
 1361         -
pub(super) struct CellSnapshot {
        1349  +
pub(super) struct AcquisitionProbe {
 1362   1350   
    pub(super) waiting: usize,
 1363   1351   
    pub(super) retained: usize,
 1364   1352   
    pub(super) demand: Option<DemandId>,
 1365   1353   
}
 1366   1354   
 1367   1355   
#[cfg(all(test, not(smithy_http_client_loom)))]
 1368   1356   
mod tests {
 1369   1357   
    use super::*;
 1370   1358   
 1371   1359   
    #[test]
 1372         -
    fn h1_required_waiter_is_not_an_h2_candidate() {
        1360  +
    fn h1_required_waiter_is_not_h2_compatible() {
 1373   1361   
        let mut queue = AcquisitionQueue::default();
 1374   1362   
        let (waiter, snapshot) = queue.register_waiter(
 1375   1363   
            ProtocolRequirement::H1Required,
 1376   1364   
            &EligibilityGroup::Pool,
 1377   1365   
            false,
 1378   1366   
        );
 1379   1367   
 1380   1368   
        assert!(snapshot.is_none());
 1381         -
        assert_eq!(Some(waiter), queue.oldest_h1_candidate());
 1382         -
        assert_eq!(None, queue.oldest_h2_candidate());
        1369  +
        assert_eq!(Some(waiter), queue.oldest_h1_compatible_waiter());
        1370  +
        assert_eq!(None, queue.oldest_h2_compatible_waiter());
 1383   1371   
    }
 1384   1372   
 1385   1373   
    #[test]
 1386   1374   
    fn head_cancellation_uses_the_successors_protocol_requirement() {
 1387   1375   
        let mut queue = AcquisitionQueue::default();
 1388   1376   
        let (head, initial) = queue.register_waiter(
 1389   1377   
            ProtocolRequirement::H2Required,
 1390   1378   
            &EligibilityGroup::Pool,
 1391   1379   
            true,
 1392   1380   
        );