Skip to main content

aws_smithy_http_client/client/pool/
connection.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Protocol-independent connection lifetime.
7//!
8//! [`ConnectionState`] serializes dispatch commitment with logical close.
9//! [`DispatchGuard`] accounts for accepted requests.
10//! [`PhysicalConnectionGuard`] follows root I/O until the client releases its
11//! physical connection handle, including transfer through a protocol upgrade.
12//! The operating system may continue TCP teardown afterward. Logical close
13//! returns bounded capacity before physical completion except while an HTTP/1
14//! exchange may still transfer upgraded I/O.
15
16use super::admission::CapacityLease;
17use super::events::{LogicalCloseCause, SharedConnectionEventListener};
18use super::origin::OriginKey;
19#[cfg(test)]
20use super::partition::Spawn;
21use super::partition::{DriverSpawner, PartitionId};
22use super::stats::CellConnectionStats;
23use crate::client::connect::{ConnectPath, ConnectPathInner};
24use crate::sync::{Arc, Mutex};
25pub use aws_smithy_runtime_api::client::connection::ConnectionId;
26use aws_smithy_runtime_api::client::connection::{
27    ConnectionEstablishmentMetadata, ConnectionMetadata,
28};
29use http_1x::Extensions;
30use hyper::rt::{Read, ReadBufCursor, Write};
31use hyper_util::client::legacy::connect::{Connected, Connection, HttpInfo};
32use pin_project_lite::pin_project;
33use std::fmt;
34use std::io::{self, IoSlice};
35use std::net::SocketAddr;
36use std::pin::Pin;
37use std::sync::OnceLock;
38use std::task::{Context, Poll};
39
40/// Final protocol and ownership classification for a closed connection.
41///
42/// [`LogicalCloseCause`] reports the first stable reason that pool dispatch
43/// ended. This more specific classification is reported at physical close
44/// because HTTP/1 upgrade ownership may be resolved after logical close.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46#[non_exhaustive]
47pub enum CloseReason {
48    /// The connection exceeded its configured idle timeout.
49    IdleTimeout,
50    /// The connection was explicitly marked unsafe for reuse.
51    Poisoned,
52    /// The protocol driver or peer closed the connection.
53    ProtocolClosed,
54    /// HTTP/1 did not prove a complete reusable message boundary.
55    IncompleteH1Exchange,
56    /// The transport left HTTP/1 pool ownership through an upgrade.
57    Upgraded,
58    /// The connection closed to move bounded capacity to another cell.
59    Reclaimed,
60    /// The connection pool was dropped.
61    PoolDropped,
62    /// The runtime driving the connection shut down.
63    OwnerRuntimeShutdown,
64}
65
66/// Protocol selected for one installed physical connection.
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68#[non_exhaustive]
69pub enum ConnectionProtocol {
70    /// HTTP/1.1 with one exclusive request sender.
71    Http1,
72    /// HTTP/2 with a multiplexed request sender.
73    Http2,
74}
75
76/// Immutable identity and transport facts for one installed connection.
77///
78/// Protocol records, request metadata, tracing, and lifecycle events retain the
79/// same value. It remains valid after logical close so the final physical-close
80/// observation names the same connection.
81#[derive(Debug)]
82pub struct ConnectionInfo {
83    /// Stable identity assigned by the owning pool.
84    id: ConnectionId,
85    /// Canonical origin this connection may serve.
86    origin: OriginKey,
87    /// Partition that retains the transport and protocol driver.
88    owner_partition: PartitionId,
89    /// Protocol selected by configuration or ALPN.
90    protocol: ConnectionProtocol,
91    /// Local socket address reported by the connector, when available.
92    local_addr: Option<SocketAddr>,
93    /// Remote socket address reported by the connector, when available.
94    remote_addr: Option<SocketAddr>,
95    /// Connector-owned path state, including request-time proxy authorization.
96    connect_path: ConnectPathInner,
97    /// Connector metadata copied into every response on this connection.
98    connected: Connected,
99    /// Measurements frozen before this connection becomes visible to dispatch.
100    establishment: OnceLock<ConnectionEstablishmentMetadata>,
101}
102
103impl ConnectionInfo {
104    /// Captures immutable facts after transport negotiation selects a protocol.
105    pub(super) fn new(
106        id: ConnectionId,
107        origin: OriginKey,
108        owner_partition: PartitionId,
109        protocol: ConnectionProtocol,
110        connected: Connected,
111    ) -> Arc<Self> {
112        let mut extras = Extensions::new();
113        connected.get_extras(&mut extras);
114        let http_info = extras.get::<HttpInfo>();
115        let connect_path = ConnectPathInner::from_connected(&connected, &extras);
116        Arc::new(Self {
117            id,
118            origin,
119            owner_partition,
120            protocol,
121            local_addr: http_info.map(HttpInfo::local_addr),
122            remote_addr: http_info.map(HttpInfo::remote_addr),
123            connect_path,
124            connected,
125            establishment: OnceLock::new(),
126        })
127    }
128
129    /// Returns the pool-assigned physical connection identity.
130    pub fn id(&self) -> ConnectionId {
131        self.id
132    }
133
134    /// Returns the canonical origin this connection may serve.
135    pub fn origin(&self) -> &OriginKey {
136        &self.origin
137    }
138
139    /// Returns the partition that owns this connection's I/O and driver.
140    pub fn owner_partition(&self) -> PartitionId {
141        self.owner_partition
142    }
143
144    /// Returns the established HTTP protocol.
145    pub fn protocol(&self) -> ConnectionProtocol {
146        self.protocol
147    }
148
149    /// Returns the connector-reported local socket address, when available.
150    pub fn local_addr(&self) -> Option<SocketAddr> {
151        self.local_addr
152    }
153
154    /// Returns the connector-reported remote socket address, when available.
155    pub fn remote_addr(&self) -> Option<SocketAddr> {
156        self.remote_addr
157    }
158
159    /// Returns how this connection reaches its origin.
160    pub fn connect_path(&self) -> ConnectPath {
161        self.connect_path.public()
162    }
163
164    /// Returns whether an HTTP proxy participates in this connection path.
165    pub fn is_proxied(&self) -> bool {
166        self.connect_path().is_proxied()
167    }
168
169    /// Returns successful establishment measurements, when installation completed.
170    pub fn establishment(&self) -> Option<&ConnectionEstablishmentMetadata> {
171        self.establishment.get()
172    }
173
174    /// Freezes successful establishment measurements before pool publication.
175    pub(super) fn set_establishment(&self, metadata: ConnectionEstablishmentMetadata) {
176        assert!(
177            self.establishment.set(metadata).is_ok(),
178            "connection establishment metadata was set more than once"
179        );
180    }
181
182    /// Returns connector-owned request-path state.
183    pub(super) fn connect_path_inner(&self) -> &ConnectPathInner {
184        &self.connect_path
185    }
186
187    /// Copies connector-provided values into a response extension map.
188    pub(super) fn apply_connector_extras(&self, extensions: &mut Extensions) {
189        self.connected.get_extras(extensions);
190    }
191
192    /// Builds Smithy metadata with close authority for this H1 record.
193    pub(super) fn metadata(&self, close: super::cell::h1::H1CloseHandle) -> ConnectionMetadata {
194        let mut builder = ConnectionMetadata::builder()
195            .proxied(self.is_proxied())
196            .connection_id(self.id)
197            .poison_fn(move || {
198                close.close(CloseReason::Poisoned);
199            });
200        builder
201            .set_local_addr(self.local_addr)
202            .set_remote_addr(self.remote_addr)
203            .set_establishment(self.establishment().cloned());
204        builder.build()
205    }
206
207    /// Builds Smithy metadata with close authority for this H2 generation.
208    pub(super) fn h2_metadata(&self, close: super::cell::h2::H2CloseHandle) -> ConnectionMetadata {
209        let mut builder = ConnectionMetadata::builder()
210            .proxied(self.is_proxied())
211            .connection_id(self.id)
212            .poison_fn(move || {
213                close.close(CloseReason::Poisoned);
214            });
215        builder
216            .set_local_addr(self.local_addr)
217            .set_remote_addr(self.remote_addr)
218            .set_establishment(self.establishment().cloned());
219        builder.build()
220    }
221
222    /// Creates synthetic HTTP/1 information for state-machine tests.
223    #[cfg(test)]
224    pub(super) fn for_test(id: ConnectionId, owner_partition: PartitionId) -> Arc<Self> {
225        Self::new(
226            id,
227            OriginKey::from_parts(http_1x::uri::Scheme::HTTPS, "example.com", None)
228                .expect("synthetic test origin is valid"),
229            owner_partition,
230            ConnectionProtocol::Http1,
231            Connected::new(),
232        )
233    }
234}
235
236/// Shared connection ownership from negotiated transport through close.
237pub(super) struct ConnectionState {
238    /// Identity and transport facts shared with metadata and lifecycle events.
239    info: Arc<ConnectionInfo>,
240    /// Runtime that owns protocol and follow-up work for this connection.
241    owner_spawner: DriverSpawner,
242    /// Cell-owned counts for connection lifetimes that outlive protocol records.
243    stats: Arc<CellConnectionStats>,
244    /// Dispatch, logical-close, and physical-connection completion state.
245    lifecycle: Mutex<ConnectionLifecycle>,
246}
247
248#[cfg(test)]
249#[derive(Debug)]
250struct TestDriverSpawner;
251
252#[cfg(test)]
253impl Spawn for TestDriverSpawner {
254    fn spawn(&self, driver: Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>) {
255        drop(driver);
256    }
257}
258
259/// Connection lifetime state serialized with dispatch commitment and close.
260#[derive(Debug)]
261struct ConnectionLifecycle {
262    /// Dispatch eligibility and ownership of bounded capacity.
263    logical: LogicalState,
264    /// Requests that committed before logical close.
265    in_flight: usize,
266    /// Whether the client has released its physical connection handle.
267    physical_connection_complete: bool,
268    /// Protocol drain or upgrade count held until physical completion.
269    counted_close: Option<CountedClose>,
270    /// Progress through installed-connection event delivery.
271    events: LifecycleEventProgress,
272}
273
274/// Whether a connection may accept dispatch and still owns bounded capacity.
275#[derive(Debug)]
276enum LogicalState {
277    /// The connector returned connected I/O and selected the HTTP protocol,
278    /// but Hyper has not produced the request handle required for dispatch.
279    PendingOpen,
280    /// Dispatch may commit.
281    Open {
282        /// Bounded-origin slot released by logical close, when configured.
283        capacity: Option<CapacityLease>,
284    },
285    /// New dispatch is rejected while accepted work may still drain.
286    Closed {
287        /// Stable cause recorded by the first logical-close transition.
288        cause: LogicalCloseCause,
289        /// Protocol-specific final close classification.
290        disposition: CloseDisposition,
291        /// Capacity retained only while HTTP/1 may transfer upgraded I/O.
292        retained_capacity: Option<CapacityLease>,
293    },
294}
295
296/// Final close classification, including HTTP/1's upgrade handoff window.
297#[derive(Clone, Copy, Debug, Eq, PartialEq)]
298enum CloseDisposition {
299    /// HTTP/1 may require exchange-side classification after its driver ends.
300    Http1(H1CloseDisposition),
301    /// HTTP/2 has no post-driver ownership transfer.
302    Http2(CloseReason),
303}
304
305impl CloseDisposition {
306    /// Returns the final physical close reason once classification completes.
307    fn final_reason(self) -> Option<CloseReason> {
308        match self {
309            Self::Http1(H1CloseDisposition::AwaitingExchange { .. }) => None,
310            Self::Http1(H1CloseDisposition::Final(reason)) | Self::Http2(reason) => Some(reason),
311        }
312    }
313}
314
315/// HTTP/1 close classification around a possible upgrade handoff.
316#[derive(Clone, Copy, Debug, Eq, PartialEq)]
317enum H1CloseDisposition {
318    /// The driver ended while one accepted exchange may still prove an upgrade.
319    AwaitingExchange {
320        /// Final reason when physical ownership ends before an upgrade appears.
321        fallback: CloseReason,
322    },
323    /// Exchange classification is complete.
324    Final(CloseReason),
325}
326
327/// Relaxed protocol-lifetime count held by one closed physical connection.
328#[derive(Clone, Copy, Debug, Eq, PartialEq)]
329enum CountedClose {
330    H1Draining,
331    H1Upgraded,
332    H2Draining,
333}
334
335impl CountedClose {
336    fn start(self, stats: &CellConnectionStats) {
337        match self {
338            Self::H1Draining => stats.h1_drain_started(),
339            Self::H1Upgraded => stats.h1_upgrade_started(),
340            Self::H2Draining => stats.h2_drain_started(),
341        }
342    }
343
344    fn finish(self, stats: &CellConnectionStats) {
345        match self {
346            Self::H1Draining => stats.h1_drain_finished(),
347            Self::H1Upgraded => stats.h1_upgrade_finished(),
348            Self::H2Draining => stats.h2_drain_finished(),
349        }
350    }
351
352    fn h1_upgrade(self, stats: &CellConnectionStats) -> Self {
353        assert_eq!(Self::H1Draining, self, "non-HTTP/1 drain became an upgrade");
354        stats.h1_drain_upgraded();
355        Self::H1Upgraded
356    }
357}
358
359/// Progress through connection lifecycle event emission.
360///
361/// `OpenedPending` keeps close events behind the `Opened` callback. The
362/// waiting states name the next event and retain the listener after `Opened`
363/// returns. `emitting` grants one thread responsibility for emitting every
364/// event that becomes ready before it releases that responsibility.
365#[derive(Debug)]
366enum LifecycleEventProgress {
367    /// Pool installation completed, but the `Opened` callback has not returned.
368    OpenedPending,
369    /// Logical close is the next event.
370    WaitingForLogicalClose {
371        listener: SharedConnectionEventListener,
372        emitting: bool,
373    },
374    /// Physical close is the next event.
375    WaitingForPhysicalClose {
376        listener: SharedConnectionEventListener,
377        emitting: bool,
378    },
379    /// No further connection lifecycle event remains.
380    Complete,
381}
382
383/// Event data copied out of the lifecycle lock for listener invocation.
384enum ReadyLifecycleEvent {
385    LogicalClose(LogicalCloseCause),
386    PhysicalClose(CloseReason),
387}
388
389impl LifecycleEventProgress {
390    /// Transfers callback ownership from `Opened` to close-event emission.
391    fn opened_completed(&mut self, listener: Option<&SharedConnectionEventListener>) -> bool {
392        assert!(
393            matches!(self, Self::OpenedPending),
394            "connection opened event completed more than once"
395        );
396        let Some(listener) = listener else {
397            *self = Self::Complete;
398            return false;
399        };
400        *self = Self::WaitingForLogicalClose {
401            listener: listener.clone(),
402            emitting: true,
403        };
404        true
405    }
406
407    /// Claims responsibility for emitting newly ready lifecycle events.
408    fn claim_emission(&mut self) -> bool {
409        let emitting = match self {
410            Self::WaitingForLogicalClose { emitting, .. }
411            | Self::WaitingForPhysicalClose { emitting, .. } => emitting,
412            Self::OpenedPending | Self::Complete => return false,
413        };
414        if *emitting {
415            return false;
416        }
417        *emitting = true;
418        true
419    }
420}
421
422impl ConnectionLifecycle {
423    /// Takes the next ready event or releases emission responsibility.
424    fn take_ready_event(&mut self) -> Option<(SharedConnectionEventListener, ReadyLifecycleEvent)> {
425        match std::mem::replace(&mut self.events, LifecycleEventProgress::Complete) {
426            LifecycleEventProgress::OpenedPending => {
427                unreachable!("connection lifecycle event emission began before Opened completed")
428            }
429            LifecycleEventProgress::WaitingForLogicalClose { listener, emitting } => {
430                assert!(
431                    emitting,
432                    "connection lifecycle event emission was not claimed"
433                );
434                let cause = match &self.logical {
435                    LogicalState::Closed { cause, .. } => *cause,
436                    LogicalState::PendingOpen | LogicalState::Open { .. } => {
437                        self.events = LifecycleEventProgress::WaitingForLogicalClose {
438                            listener,
439                            emitting: false,
440                        };
441                        return None;
442                    }
443                };
444                self.events = LifecycleEventProgress::WaitingForPhysicalClose {
445                    listener: listener.clone(),
446                    emitting: true,
447                };
448                Some((listener, ReadyLifecycleEvent::LogicalClose(cause)))
449            }
450            LifecycleEventProgress::WaitingForPhysicalClose { listener, emitting } => {
451                assert!(
452                    emitting,
453                    "connection lifecycle event emission was not claimed"
454                );
455                if !self.physical_connection_complete {
456                    self.events = LifecycleEventProgress::WaitingForPhysicalClose {
457                        listener,
458                        emitting: false,
459                    };
460                    return None;
461                }
462                let reason = match &self.logical {
463                    LogicalState::Closed { disposition, .. } => disposition
464                        .final_reason()
465                        .expect("physical close completed before H1 classification"),
466                    LogicalState::PendingOpen | LogicalState::Open { .. } => {
467                        panic!("physical close followed an open connection")
468                    }
469                };
470                Some((listener, ReadyLifecycleEvent::PhysicalClose(reason)))
471            }
472            LifecycleEventProgress::Complete => None,
473        }
474    }
475}
476impl ConnectionState {
477    /// Creates state after transport establishment and protocol selection.
478    ///
479    /// For TLS transports, the connector has completed TLS and ALPN before this
480    /// call. Hyper protocol setup and cell installation have not occurred.
481    ///
482    /// The returned guard uniquely tracks the client's physical connection
483    /// handle.
484    /// After Hyper returns a request handle, [`Self::open`] attaches optional
485    /// bounded capacity before cell installation makes the connection
486    /// discoverable.
487    pub(super) fn pending_open(
488        info: Arc<ConnectionInfo>,
489        owner_spawner: DriverSpawner,
490        stats: Arc<CellConnectionStats>,
491    ) -> (Arc<Self>, PhysicalConnectionGuard) {
492        stats.physical_connection_started();
493        let connection = Arc::new(Self {
494            info,
495            owner_spawner,
496            stats,
497            lifecycle: Mutex::new(ConnectionLifecycle {
498                logical: LogicalState::PendingOpen,
499                in_flight: 0,
500                physical_connection_complete: false,
501                counted_close: None,
502                events: LifecycleEventProgress::OpenedPending,
503            }),
504        });
505        let physical = PhysicalConnectionGuard {
506            connection: connection.clone(),
507            active: true,
508        };
509        (connection, physical)
510    }
511
512    #[cfg(test)]
513    pub(super) fn pending_open_for_test(
514        info: Arc<ConnectionInfo>,
515    ) -> (Arc<Self>, PhysicalConnectionGuard) {
516        Self::pending_open(
517            info,
518            DriverSpawner::new(TestDriverSpawner),
519            Arc::new(CellConnectionStats::default()),
520        )
521    }
522
523    /// Returns the runtime that owns this installed connection.
524    pub(super) fn owner_spawner(&self) -> DriverSpawner {
525        self.owner_spawner.clone()
526    }
527
528    /// Opens dispatch commitment and transfers optional bounded capacity.
529    ///
530    /// The caller performs this transition after Hyper protocol setup and
531    /// before publishing the connection through its cell. Opening first
532    /// ensures that a newly visible request handle can commit dispatch.
533    ///
534    /// Returns `lease` when logical close won before opening.
535    pub(super) fn open(&self, lease: Option<CapacityLease>) -> Result<(), Option<CapacityLease>> {
536        let mut lifecycle = self.lifecycle.lock();
537        if !matches!(lifecycle.logical, LogicalState::PendingOpen) {
538            return Err(lease);
539        }
540        lifecycle.logical = LogicalState::Open { capacity: lease };
541        Ok(())
542    }
543
544    /// Creates a connection whose origin has no admission bound.
545    ///
546    /// The returned unique guard must move with the root I/O task.
547    #[cfg(test)]
548    pub(super) fn unbounded(info: Arc<ConnectionInfo>) -> (Arc<Self>, PhysicalConnectionGuard) {
549        let (connection, physical) = Self::pending_open_for_test(info);
550        connection
551            .open(None)
552            .expect("new unbounded connection could not open");
553        (connection, physical)
554    }
555
556    /// Creates a connection that takes ownership of one bounded-origin slot.
557    ///
558    /// The returned unique guard must move with the root I/O task. Logical
559    /// close returns `lease` independently of that guard.
560    #[cfg(test)]
561    pub(super) fn bounded(
562        info: Arc<ConnectionInfo>,
563        lease: CapacityLease,
564    ) -> (Arc<Self>, PhysicalConnectionGuard) {
565        let (connection, physical) = Self::pending_open_for_test(info);
566        connection
567            .open(Some(lease))
568            .expect("new bounded connection could not open");
569        (connection, physical)
570    }
571
572    /// Returns this connection's stable identity.
573    pub(super) fn id(&self) -> ConnectionId {
574        self.info.id()
575    }
576
577    /// Returns the partition that owns this connection's I/O and driver.
578    pub(super) fn owner_partition(&self) -> PartitionId {
579        self.info.owner_partition()
580    }
581
582    /// Returns immutable identity and transport facts for this connection.
583    pub(super) fn info(&self) -> &Arc<ConnectionInfo> {
584        &self.info
585    }
586
587    /// Attempts to commit one request against logical close.
588    ///
589    /// Returns a guard and increments the in-flight count while the connection
590    /// is open. Returns `None` without changing state after logical close.
591    pub(super) fn try_commit_dispatch(connection: &Arc<Self>) -> Option<DispatchGuard> {
592        let mut lifecycle = connection.lifecycle.lock();
593        if !matches!(lifecycle.logical, LogicalState::Open { .. }) {
594            return None;
595        }
596        lifecycle.in_flight = lifecycle
597            .in_flight
598            .checked_add(1)
599            .expect("in-flight dispatch count exhausted");
600        drop(lifecycle);
601
602        Some(DispatchGuard {
603            connection: connection.clone(),
604            active: true,
605        })
606    }
607
608    /// Completes the `Opened` callback and reports any close that occurred
609    /// while it was running.
610    ///
611    /// A listener may reenter the pool and close this connection from its
612    /// `Opened` callback. The lifecycle transition completes immediately, but
613    /// its callbacks are delayed until this method can preserve
614    /// `Opened`, logical close, and physical close in that order.
615    pub(super) fn complete_opened_event(&self, listener: Option<&SharedConnectionEventListener>) {
616        let emit = {
617            let mut lifecycle = self.lifecycle.lock();
618            assert!(
619                !matches!(lifecycle.logical, LogicalState::PendingOpen),
620                "connection opened event completed before the connection opened"
621            );
622            lifecycle.events.opened_completed(listener)
623        };
624
625        if emit {
626            self.emit_ready_events();
627        }
628    }
629
630    /// Emits every ready lifecycle event claimed by the current thread.
631    ///
632    /// Each event is taken in its own statement so the lifecycle lock is
633    /// released before the listener runs.
634    fn emit_ready_events(&self) {
635        loop {
636            let ready = self.lifecycle.lock().take_ready_event();
637            let Some((listener, event)) = ready else {
638                return;
639            };
640            match event {
641                ReadyLifecycleEvent::LogicalClose(cause) => {
642                    listener.logical_close(&self.info, cause);
643                }
644                ReadyLifecycleEvent::PhysicalClose(reason) => {
645                    listener.physical_close(&self.info, reason);
646                }
647            }
648        }
649    }
650
651    /// Performs the first logical-close transition.
652    ///
653    /// Returns `true` when this call closes the connection and records
654    /// `reason`. Returns `false` when another close already won; the original
655    /// logical cause remains unchanged.
656    ///
657    /// Capacity normally returns after the connection lock is released.
658    /// HTTP/1 retains it while an accepted exchange may still transfer
659    /// upgraded I/O.
660    pub(super) fn logical_close(&self, reason: CloseReason) -> bool {
661        let cause = LogicalCloseCause::from_reason(reason);
662        let (released_capacity, emit) = {
663            let mut lifecycle = self.lifecycle.lock();
664            let previous = std::mem::replace(&mut lifecycle.logical, LogicalState::PendingOpen);
665            let (disposition, retained_capacity, released_capacity, was_open) = match previous {
666                LogicalState::PendingOpen => {
667                    let disposition = match self.info.protocol() {
668                        ConnectionProtocol::Http1 => {
669                            CloseDisposition::Http1(H1CloseDisposition::Final(reason))
670                        }
671                        ConnectionProtocol::Http2 => CloseDisposition::Http2(reason),
672                    };
673                    (disposition, None, None, false)
674                }
675                LogicalState::Open { mut capacity } => match self.info.protocol() {
676                    ConnectionProtocol::Http1
677                        if lifecycle.in_flight != 0
678                            && !lifecycle.physical_connection_complete
679                            && matches!(
680                                reason,
681                                CloseReason::ProtocolClosed | CloseReason::OwnerRuntimeShutdown
682                            ) =>
683                    {
684                        (
685                            CloseDisposition::Http1(H1CloseDisposition::AwaitingExchange {
686                                fallback: reason,
687                            }),
688                            capacity.take(),
689                            None,
690                            true,
691                        )
692                    }
693                    ConnectionProtocol::Http1 => {
694                        let retain_for_upgrade = reason == CloseReason::Upgraded
695                            && !lifecycle.physical_connection_complete;
696                        let retained_capacity =
697                            retain_for_upgrade.then(|| capacity.take()).flatten();
698                        (
699                            CloseDisposition::Http1(H1CloseDisposition::Final(reason)),
700                            retained_capacity,
701                            capacity,
702                            true,
703                        )
704                    }
705                    ConnectionProtocol::Http2 => {
706                        (CloseDisposition::Http2(reason), None, capacity, true)
707                    }
708                },
709                closed @ LogicalState::Closed { .. } => {
710                    lifecycle.logical = closed;
711                    return false;
712                }
713            };
714            if was_open && !lifecycle.physical_connection_complete {
715                let counted_close = match self.info.protocol() {
716                    ConnectionProtocol::Http1 if reason == CloseReason::Upgraded => {
717                        CountedClose::H1Upgraded
718                    }
719                    ConnectionProtocol::Http1 => CountedClose::H1Draining,
720                    ConnectionProtocol::Http2 => CountedClose::H2Draining,
721                };
722                assert!(
723                    lifecycle.counted_close.is_none(),
724                    "connection close was counted more than once"
725                );
726                counted_close.start(&self.stats);
727                lifecycle.counted_close = Some(counted_close);
728            }
729            lifecycle.logical = LogicalState::Closed {
730                cause,
731                disposition,
732                retained_capacity,
733            };
734            let emit = lifecycle.events.claim_emission();
735            (released_capacity, emit)
736        };
737        drop(released_capacity);
738        if emit {
739            self.emit_ready_events();
740        }
741        tracing::debug!(
742            connection_id = %self.id(),
743            connection_partition = ?self.owner_partition(),
744            protocol = ?self.info.protocol(),
745            origin_scheme = %self.info.origin().scheme(),
746            origin_host = self.info.origin().host(),
747            origin_port = ?self.info.origin().port(),
748            close_reason = ?reason,
749            "connection logically closed"
750        );
751        true
752    }
753
754    /// Finalizes a driver-observed HTTP/1 close from exchange-side evidence.
755    ///
756    /// A confirmed upgrade retains bounded capacity until root I/O leaves the
757    /// client. Every other classification preserves the driver reason and
758    /// returns capacity immediately.
759    pub(super) fn complete_h1_exchange(&self, exchange_reason: CloseReason) -> bool {
760        let (released_capacity, final_reason) = {
761            let mut lifecycle = self.lifecycle.lock();
762            let physical_connection_complete = lifecycle.physical_connection_complete;
763            let (released_capacity, final_reason) = match &mut lifecycle.logical {
764                LogicalState::Closed {
765                    disposition: CloseDisposition::Http1(state),
766                    retained_capacity,
767                    ..
768                } => {
769                    let H1CloseDisposition::AwaitingExchange { fallback } = *state else {
770                        return false;
771                    };
772                    debug_assert!(
773                        !physical_connection_complete,
774                        "physical close left HTTP/1 exchange classification pending"
775                    );
776                    let final_reason = if exchange_reason == CloseReason::Upgraded {
777                        CloseReason::Upgraded
778                    } else {
779                        fallback
780                    };
781                    *state = H1CloseDisposition::Final(final_reason);
782                    let release_capacity = final_reason != CloseReason::Upgraded;
783                    (
784                        release_capacity.then(|| retained_capacity.take()).flatten(),
785                        final_reason,
786                    )
787                }
788                _ => return false,
789            };
790            if final_reason == CloseReason::Upgraded {
791                lifecycle.counted_close = Some(
792                    lifecycle
793                        .counted_close
794                        .expect("upgraded HTTP/1 connection had no draining count")
795                        .h1_upgrade(&self.stats),
796                );
797            }
798            (released_capacity, final_reason)
799        };
800        drop(released_capacity);
801        tracing::debug!(
802            connection_id = %self.id(),
803            connection_partition = ?self.owner_partition(),
804            origin_scheme = %self.info.origin().scheme(),
805            origin_host = self.info.origin().host(),
806            origin_port = ?self.info.origin().port(),
807            close_reason = ?final_reason,
808            exchange_reason = ?exchange_reason,
809            "HTTP/1 connection close classification completed"
810        );
811        true
812    }
813
814    /// Removes one dispatch previously committed by [`Self::try_commit_dispatch`].
815    fn release_dispatch(&self) {
816        let complete_reason = {
817            let mut lifecycle = self.lifecycle.lock();
818            lifecycle.in_flight = lifecycle
819                .in_flight
820                .checked_sub(1)
821                .expect("completed a dispatch that was not in flight");
822            if lifecycle.in_flight == 0 {
823                match lifecycle.logical {
824                    LogicalState::Closed {
825                        disposition:
826                            CloseDisposition::Http1(H1CloseDisposition::AwaitingExchange { fallback }),
827                        ..
828                    } => Some(fallback),
829                    _ => None,
830                }
831            } else {
832                None
833            }
834        };
835        if let Some(reason) = complete_reason {
836            self.complete_h1_exchange(reason);
837        }
838    }
839
840    /// Records that the client released its physical connection handle.
841    ///
842    /// # Panics
843    ///
844    /// Panics if physical connection ownership completes more than once.
845    fn complete_physical_connection(&self) {
846        let (released_capacity, emit) = {
847            let mut lifecycle = self.lifecycle.lock();
848            assert!(
849                !lifecycle.physical_connection_complete,
850                "physical connection ownership completed more than once"
851            );
852            lifecycle.physical_connection_complete = true;
853
854            let released_capacity = match &mut lifecycle.logical {
855                LogicalState::Closed {
856                    disposition,
857                    retained_capacity,
858                    ..
859                } => {
860                    if let CloseDisposition::Http1(H1CloseDisposition::AwaitingExchange {
861                        fallback,
862                    }) = *disposition
863                    {
864                        *disposition = CloseDisposition::Http1(H1CloseDisposition::Final(fallback));
865                    }
866                    retained_capacity.take()
867                }
868                LogicalState::PendingOpen | LogicalState::Open { .. } => None,
869            };
870            if let Some(counted_close) = lifecycle.counted_close.take() {
871                counted_close.finish(&self.stats);
872            }
873            self.stats.physical_connection_finished();
874            let emit = lifecycle.events.claim_emission();
875            (released_capacity, emit)
876        };
877        drop(released_capacity);
878        if emit {
879            self.emit_ready_events();
880        }
881        tracing::debug!(
882            connection_id = %self.id(),
883            connection_partition = ?self.owner_partition(),
884            origin_scheme = %self.info.origin().scheme(),
885            origin_host = self.info.origin().host(),
886            origin_port = ?self.info.origin().port(),
887            "physical connection ownership ended"
888        );
889    }
890
891    /// Returns a consistent lifecycle snapshot.
892    #[cfg(test)]
893    pub(super) fn probe(&self) -> ConnectionProbe {
894        let lifecycle = self.lifecycle.lock();
895        let (close_reason, awaiting_h1_exchange) = match &lifecycle.logical {
896            LogicalState::PendingOpen | LogicalState::Open { .. } => (None, false),
897            LogicalState::Closed { disposition, .. } => (
898                disposition.final_reason(),
899                matches!(
900                    disposition,
901                    CloseDisposition::Http1(H1CloseDisposition::AwaitingExchange { .. })
902                ),
903            ),
904        };
905        ConnectionProbe {
906            close_reason,
907            awaiting_h1_exchange,
908            in_flight: lifecycle.in_flight,
909            physical_connection_complete: lifecycle.physical_connection_complete,
910        }
911    }
912}
913
914impl fmt::Debug for ConnectionState {
915    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
916        f.debug_struct("ConnectionState")
917            .field("info", &self.info)
918            .field("lifecycle", &self.lifecycle)
919            .finish()
920    }
921}
922
923#[cfg(test)]
924/// Observable lifecycle state used by protocol coordination and focused tests.
925#[derive(Clone, Copy, Debug, Eq, PartialEq)]
926pub(super) struct ConnectionProbe {
927    /// Reason logical close won, or `None` while dispatch is accepted.
928    pub(super) close_reason: Option<CloseReason>,
929    /// Whether an accepted H1 exchange still owes final close classification.
930    pub(super) awaiting_h1_exchange: bool,
931    /// Number of dispatches that have not completed.
932    pub(super) in_flight: usize,
933    /// Whether the client released its physical connection handle.
934    pub(super) physical_connection_complete: bool,
935}
936
937/// One dispatch that committed before logical close.
938///
939/// Dropping the guard records request completion exactly once.
940#[derive(Debug)]
941pub(super) struct DispatchGuard {
942    /// Shared state whose in-flight count this guard owns.
943    connection: Arc<ConnectionState>,
944    /// Whether `Drop` still owes request completion.
945    active: bool,
946}
947
948impl DispatchGuard {
949    /// Returns the identity of the connection carrying this dispatch.
950    #[cfg(test)]
951    pub(super) fn connection_id(&self) -> ConnectionId {
952        self.connection.id()
953    }
954
955    /// Consumes the guard and records request completion immediately.
956    ///
957    /// Dropping an uncompleted guard performs the same accounting as a
958    /// cancellation fallback.
959    pub(super) fn release(mut self) {
960        self.active = false;
961        self.connection.release_dispatch();
962    }
963}
964
965impl Drop for DispatchGuard {
966    fn drop(&mut self) {
967        if self.active {
968            self.connection.release_dispatch();
969        }
970    }
971}
972
973/// Tracks the lifetime of the client's physical connection handle.
974///
975/// The guard is created with the connection and moves with root I/O through
976/// protocol drain or upgrade. Dropping it means the client no longer owns its
977/// transport handle; the operating system may continue TCP teardown.
978#[derive(Debug)]
979pub(super) struct PhysicalConnectionGuard {
980    /// Shared state whose physical connection lifetime this guard tracks.
981    connection: Arc<ConnectionState>,
982    /// Whether `Drop` still owes physical connection completion.
983    active: bool,
984}
985
986impl PhysicalConnectionGuard {
987    /// Consumes the guard and records physical connection completion.
988    ///
989    /// Dropping an uncompleted guard performs the same transition during task
990    /// cancellation or runtime shutdown.
991    #[cfg(test)]
992    pub(super) fn release(mut self) {
993        self.active = false;
994        self.connection.complete_physical_connection();
995    }
996}
997
998impl Drop for PhysicalConnectionGuard {
999    fn drop(&mut self) {
1000        if self.active {
1001            self.connection.complete_physical_connection();
1002        }
1003    }
1004}
1005
1006pin_project! {
1007    /// Root transport wrapper that ties physical connection ownership to I/O.
1008    ///
1009    /// The wrapper moves intact through Hyper's driver and H1 upgrade path.
1010    /// Logical close may happen earlier. Dropping the wrapper records that the
1011    /// client released its physical connection handle after the wrapped I/O
1012    /// was destroyed; the operating system may continue TCP teardown.
1013    pub(super) struct ConnectionIo<T> {
1014        #[pin]
1015        inner: T,
1016        // Declared after `inner` so transport destruction precedes the
1017        // physical-connection completion signal.
1018        physical: PhysicalConnectionGuard,
1019    }
1020}
1021
1022impl<T> ConnectionIo<T> {
1023    /// Attaches physical connection lifetime tracking to root transport I/O.
1024    pub(super) fn new(inner: T, physical: PhysicalConnectionGuard) -> Self {
1025        Self { inner, physical }
1026    }
1027
1028    /// Returns the wrapped transport.
1029    #[cfg(test)]
1030    pub(super) fn get_ref(&self) -> &T {
1031        &self.inner
1032    }
1033}
1034
1035impl<T> fmt::Debug for ConnectionIo<T>
1036where
1037    T: fmt::Debug,
1038{
1039    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1040        f.debug_struct("ConnectionIo")
1041            .field("inner", &self.inner)
1042            .finish_non_exhaustive()
1043    }
1044}
1045
1046impl<T> Connection for ConnectionIo<T>
1047where
1048    T: Connection,
1049{
1050    fn connected(&self) -> Connected {
1051        self.inner.connected()
1052    }
1053}
1054
1055impl<T> Read for ConnectionIo<T>
1056where
1057    T: Read,
1058{
1059    fn poll_read(
1060        self: Pin<&mut Self>,
1061        cx: &mut Context<'_>,
1062        buf: ReadBufCursor<'_>,
1063    ) -> Poll<io::Result<()>> {
1064        Read::poll_read(self.project().inner, cx, buf)
1065    }
1066}
1067
1068impl<T> Write for ConnectionIo<T>
1069where
1070    T: Write,
1071{
1072    fn poll_write(
1073        self: Pin<&mut Self>,
1074        cx: &mut Context<'_>,
1075        buf: &[u8],
1076    ) -> Poll<io::Result<usize>> {
1077        Write::poll_write(self.project().inner, cx, buf)
1078    }
1079
1080    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1081        Write::poll_flush(self.project().inner, cx)
1082    }
1083
1084    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1085        Write::poll_shutdown(self.project().inner, cx)
1086    }
1087
1088    fn is_write_vectored(&self) -> bool {
1089        self.inner.is_write_vectored()
1090    }
1091
1092    fn poll_write_vectored(
1093        self: Pin<&mut Self>,
1094        cx: &mut Context<'_>,
1095        bufs: &[IoSlice<'_>],
1096    ) -> Poll<io::Result<usize>> {
1097        Write::poll_write_vectored(self.project().inner, cx, bufs)
1098    }
1099}
1100
1101#[cfg(all(test, not(smithy_http_client_loom)))]
1102mod tests {
1103    use super::*;
1104    use crate::client::pool::admission::OriginAdmission;
1105    use crate::client::pool::events::{
1106        ConnectionEvent, ConnectionEvents, SharedConnectionEventListener,
1107    };
1108    use aws_smithy_async::time::SharedTimeSource;
1109    use std::num::NonZeroUsize;
1110    use std::sync::{Arc as StdArc, Mutex as StdMutex};
1111
1112    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1113    enum ObservedLifecycleEvent {
1114        Opened,
1115        LogicalClose(LogicalCloseCause),
1116        PhysicalClose(CloseReason),
1117    }
1118
1119    fn test_info(id: u64) -> Arc<ConnectionInfo> {
1120        test_info_for_protocol(id, ConnectionProtocol::Http1)
1121    }
1122
1123    fn test_info_for_protocol(id: u64, protocol: ConnectionProtocol) -> Arc<ConnectionInfo> {
1124        ConnectionInfo::new(
1125            ConnectionId::new(id),
1126            OriginKey::from_parts(http_1x::uri::Scheme::HTTPS, "example.com", None)
1127                .expect("synthetic test origin is valid"),
1128            PartitionId::from_index(0),
1129            protocol,
1130            Connected::new(),
1131        )
1132    }
1133
1134    fn attach_observer(
1135        connection: &Arc<ConnectionState>,
1136        observed: StdArc<StdMutex<Vec<ObservedLifecycleEvent>>>,
1137    ) {
1138        let events = ConnectionEvents::new(
1139            Some(SharedConnectionEventListener::new(
1140                move |event: &ConnectionEvent<'_>| {
1141                    let event = match event {
1142                        ConnectionEvent::Opened(_) => ObservedLifecycleEvent::Opened,
1143                        ConnectionEvent::LogicalClose(closed) => {
1144                            ObservedLifecycleEvent::LogicalClose(closed.cause())
1145                        }
1146                        ConnectionEvent::PhysicalClose(closed) => {
1147                            ObservedLifecycleEvent::PhysicalClose(closed.reason())
1148                        }
1149                        ConnectionEvent::EstablishmentFailed(failed) => {
1150                            panic!("installed connection failed: {failed:?}")
1151                        }
1152                    };
1153                    observed.lock().unwrap().push(event);
1154                },
1155            )),
1156            SharedTimeSource::default(),
1157        );
1158        let establishment = events.establishment_started(
1159            connection.info().origin(),
1160            connection.owner_partition(),
1161            connection.stats.clone(),
1162        );
1163        establishment.opened(connection);
1164    }
1165
1166    #[test]
1167    fn lifecycle_callbacks_observe_updated_connection_stats() {
1168        #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1169        enum EventKind {
1170            Opened,
1171            LogicalClose,
1172            PhysicalClose,
1173        }
1174
1175        let stats = Arc::new(CellConnectionStats::default());
1176        let observed = StdArc::new(StdMutex::new(Vec::new()));
1177        let events = ConnectionEvents::new(
1178            Some(SharedConnectionEventListener::new({
1179                let stats = stats.clone();
1180                let observed = observed.clone();
1181                move |event: &ConnectionEvent<'_>| {
1182                    let kind = match event {
1183                        ConnectionEvent::Opened(_) => EventKind::Opened,
1184                        ConnectionEvent::LogicalClose(_) => EventKind::LogicalClose,
1185                        ConnectionEvent::PhysicalClose(_) => EventKind::PhysicalClose,
1186                        ConnectionEvent::EstablishmentFailed(failed) => {
1187                            panic!("installed connection failed: {failed:?}")
1188                        }
1189                    };
1190                    let snapshot = stats.snapshot(0, 0, 0, 0, 0);
1191                    observed.lock().unwrap().push((
1192                        kind,
1193                        snapshot.establishing_connections(),
1194                        snapshot.h1().draining(),
1195                        snapshot.physically_live_connections(),
1196                    ));
1197                }
1198            })),
1199            SharedTimeSource::default(),
1200        );
1201        let mut establishment = events.establishment_started(
1202            test_info(1).origin(),
1203            PartitionId::from_index(0),
1204            stats.clone(),
1205        );
1206        establishment.protocol_selected(ConnectionProtocol::Http1);
1207        let (connection, physical) = ConnectionState::pending_open(
1208            test_info(1),
1209            DriverSpawner::new(TestDriverSpawner),
1210            stats,
1211        );
1212        connection.open(None).unwrap();
1213
1214        establishment.opened(&connection);
1215        assert!(connection.logical_close(CloseReason::ProtocolClosed));
1216        physical.release();
1217
1218        assert_eq!(
1219            &[
1220                (EventKind::Opened, 0, 0, 1),
1221                (EventKind::LogicalClose, 0, 1, 1),
1222                (EventKind::PhysicalClose, 0, 0, 0),
1223            ],
1224            observed.lock().unwrap().as_slice()
1225        );
1226    }
1227
1228    #[test]
1229    fn logical_close_releases_capacity_before_physical_connection_completion() {
1230        let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1231        let lease = OriginAdmission::lease_for_test(&origin);
1232        let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
1233
1234        assert!(connection.logical_close(CloseReason::Reclaimed));
1235        assert!(!connection.logical_close(CloseReason::PoolDropped));
1236        assert_eq!(1, origin.available_capacity_for_test());
1237        assert!(!connection.probe().physical_connection_complete);
1238        assert_eq!(
1239            Some(CloseReason::Reclaimed),
1240            connection.probe().close_reason
1241        );
1242
1243        drop(physical);
1244        assert!(connection.probe().physical_connection_complete);
1245    }
1246
1247    #[test]
1248    fn installed_events_are_ordered_for_h1_and_h2() {
1249        for (id, protocol) in [
1250            (1, ConnectionProtocol::Http1),
1251            (2, ConnectionProtocol::Http2),
1252        ] {
1253            let observed = StdArc::new(StdMutex::new(Vec::new()));
1254            let (connection, physical) =
1255                ConnectionState::unbounded(test_info_for_protocol(id, protocol));
1256            attach_observer(&connection, observed.clone());
1257
1258            physical.release();
1259            assert_eq!(
1260                &[ObservedLifecycleEvent::Opened],
1261                observed.lock().unwrap().as_slice()
1262            );
1263
1264            assert!(connection.logical_close(CloseReason::ProtocolClosed));
1265            assert_eq!(
1266                &[
1267                    ObservedLifecycleEvent::Opened,
1268                    ObservedLifecycleEvent::LogicalClose(LogicalCloseCause::ProtocolEnded),
1269                    ObservedLifecycleEvent::PhysicalClose(CloseReason::ProtocolClosed),
1270                ],
1271                observed.lock().unwrap().as_slice()
1272            );
1273        }
1274    }
1275
1276    #[test]
1277    fn close_before_opened_delivery_is_replayed_in_order() {
1278        let observed = StdArc::new(StdMutex::new(Vec::new()));
1279        let (connection, physical) = ConnectionState::unbounded(test_info(1));
1280
1281        assert!(connection.logical_close(CloseReason::PoolDropped));
1282        physical.release();
1283        assert!(observed.lock().unwrap().is_empty());
1284
1285        attach_observer(&connection, observed.clone());
1286        assert_eq!(
1287            &[
1288                ObservedLifecycleEvent::Opened,
1289                ObservedLifecycleEvent::LogicalClose(LogicalCloseCause::PoolDropped),
1290                ObservedLifecycleEvent::PhysicalClose(CloseReason::PoolDropped),
1291            ],
1292            observed.lock().unwrap().as_slice()
1293        );
1294    }
1295
1296    #[test]
1297    fn close_callbacks_may_reenter_connection_state() {
1298        let observed = StdArc::new(StdMutex::new(Vec::new()));
1299        let connection_slot = StdArc::new(StdMutex::new(None::<Arc<ConnectionState>>));
1300        let events = ConnectionEvents::new(
1301            Some(SharedConnectionEventListener::new({
1302                let observed = observed.clone();
1303                let connection_slot = connection_slot.clone();
1304                move |event: &ConnectionEvent<'_>| {
1305                    let event = match event {
1306                        ConnectionEvent::Opened(_) => return,
1307                        ConnectionEvent::LogicalClose(closed) => {
1308                            ObservedLifecycleEvent::LogicalClose(closed.cause())
1309                        }
1310                        ConnectionEvent::PhysicalClose(closed) => {
1311                            ObservedLifecycleEvent::PhysicalClose(closed.reason())
1312                        }
1313                        ConnectionEvent::EstablishmentFailed(failed) => {
1314                            panic!("installed connection failed: {failed:?}")
1315                        }
1316                    };
1317                    let connection = connection_slot
1318                        .lock()
1319                        .unwrap()
1320                        .clone()
1321                        .expect("connection installed before callback");
1322                    // Recorded after reentry so a panic swallowed by the
1323                    // listener boundary leaves the observation missing.
1324                    let closed_again = connection.logical_close(CloseReason::PoolDropped);
1325                    observed.lock().unwrap().push((event, closed_again));
1326                }
1327            })),
1328            SharedTimeSource::default(),
1329        );
1330        let (connection, physical) = ConnectionState::unbounded(test_info(1));
1331        *connection_slot.lock().unwrap() = Some(connection.clone());
1332        let establishment = events.establishment_started(
1333            connection.info().origin(),
1334            connection.owner_partition(),
1335            connection.stats.clone(),
1336        );
1337        establishment.opened(&connection);
1338
1339        assert!(connection.logical_close(CloseReason::Poisoned));
1340        physical.release();
1341        assert_eq!(
1342            &[
1343                (
1344                    ObservedLifecycleEvent::LogicalClose(LogicalCloseCause::Poisoned),
1345                    false
1346                ),
1347                (
1348                    ObservedLifecycleEvent::PhysicalClose(CloseReason::Poisoned),
1349                    false
1350                ),
1351            ],
1352            observed.lock().unwrap().as_slice()
1353        );
1354    }
1355
1356    #[test]
1357    fn panicking_close_listener_does_not_change_lifecycle_state() {
1358        let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1359        let lease = OriginAdmission::lease_for_test(&origin);
1360        let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
1361        let events = ConnectionEvents::new(
1362            Some(SharedConnectionEventListener::new(
1363                |_: &ConnectionEvent<'_>| panic!("listener failed"),
1364            )),
1365            SharedTimeSource::default(),
1366        );
1367        let establishment = events.establishment_started(
1368            connection.info().origin(),
1369            connection.owner_partition(),
1370            connection.stats.clone(),
1371        );
1372        establishment.opened(&connection);
1373
1374        assert!(connection.logical_close(CloseReason::Reclaimed));
1375        physical.release();
1376
1377        assert_eq!(1, origin.available_capacity_for_test());
1378        assert_eq!(
1379            Some(CloseReason::Reclaimed),
1380            connection.probe().close_reason
1381        );
1382        assert!(connection.probe().physical_connection_complete);
1383    }
1384    #[test]
1385    fn driver_first_upgrade_retains_capacity_until_physical_completion() {
1386        let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1387        let lease = OriginAdmission::lease_for_test(&origin);
1388        let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
1389        let dispatch = ConnectionState::try_commit_dispatch(&connection).unwrap();
1390
1391        assert!(connection.logical_close(CloseReason::ProtocolClosed));
1392        assert!(connection.probe().awaiting_h1_exchange);
1393        assert_eq!(0, origin.available_capacity_for_test());
1394
1395        assert!(connection.complete_h1_exchange(CloseReason::Upgraded));
1396        assert_eq!(Some(CloseReason::Upgraded), connection.probe().close_reason);
1397        assert_eq!(0, origin.available_capacity_for_test());
1398        dispatch.release();
1399
1400        physical.release();
1401        assert_eq!(1, origin.available_capacity_for_test());
1402    }
1403
1404    #[test]
1405    fn exchange_first_upgrade_retains_capacity_until_physical_completion() {
1406        let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1407        let lease = OriginAdmission::lease_for_test(&origin);
1408        let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
1409        let dispatch = ConnectionState::try_commit_dispatch(&connection).unwrap();
1410
1411        assert!(connection.logical_close(CloseReason::Upgraded));
1412        assert_eq!(Some(CloseReason::Upgraded), connection.probe().close_reason);
1413        assert_eq!(0, origin.available_capacity_for_test());
1414        dispatch.release();
1415
1416        physical.release();
1417        assert_eq!(1, origin.available_capacity_for_test());
1418    }
1419
1420    #[test]
1421    fn non_upgrade_exchange_returns_capacity_before_physical_completion() {
1422        let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1423        let lease = OriginAdmission::lease_for_test(&origin);
1424        let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
1425        let dispatch = ConnectionState::try_commit_dispatch(&connection).unwrap();
1426
1427        assert!(connection.logical_close(CloseReason::ProtocolClosed));
1428        assert!(connection.complete_h1_exchange(CloseReason::ProtocolClosed));
1429        assert_eq!(
1430            Some(CloseReason::ProtocolClosed),
1431            connection.probe().close_reason
1432        );
1433        assert_eq!(1, origin.available_capacity_for_test());
1434        assert!(!connection.probe().physical_connection_complete);
1435
1436        dispatch.release();
1437        physical.release();
1438    }
1439
1440    #[test]
1441    fn non_upgrade_exchange_preserves_owner_runtime_shutdown_reason() {
1442        let observed = StdArc::new(StdMutex::new(Vec::new()));
1443        let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1444        let lease = OriginAdmission::lease_for_test(&origin);
1445        let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
1446        attach_observer(&connection, observed.clone());
1447        let dispatch = ConnectionState::try_commit_dispatch(&connection).unwrap();
1448
1449        assert!(connection.logical_close(CloseReason::OwnerRuntimeShutdown));
1450        assert!(connection.complete_h1_exchange(CloseReason::ProtocolClosed));
1451        assert_eq!(
1452            Some(CloseReason::OwnerRuntimeShutdown),
1453            connection.probe().close_reason
1454        );
1455        assert_eq!(1, origin.available_capacity_for_test());
1456        dispatch.release();
1457
1458        physical.release();
1459        assert_eq!(
1460            &[
1461                ObservedLifecycleEvent::Opened,
1462                ObservedLifecycleEvent::LogicalClose(LogicalCloseCause::OwnerRuntimeShutdown,),
1463                ObservedLifecycleEvent::PhysicalClose(CloseReason::OwnerRuntimeShutdown,),
1464            ],
1465            observed.lock().unwrap().as_slice()
1466        );
1467    }
1468
1469    #[test]
1470    fn exchange_classification_does_not_replace_a_policy_close() {
1471        let (connection, _physical) = ConnectionState::unbounded(test_info(1));
1472        assert!(connection.logical_close(CloseReason::Poisoned));
1473        assert!(!connection.complete_h1_exchange(CloseReason::Upgraded));
1474        assert_eq!(Some(CloseReason::Poisoned), connection.probe().close_reason);
1475    }
1476    #[test]
1477    fn committed_dispatch_drains_after_logical_close() {
1478        let (connection, _physical) = ConnectionState::unbounded(test_info(1));
1479        let dispatch = ConnectionState::try_commit_dispatch(&connection).unwrap();
1480        assert_eq!(ConnectionId::new(1), dispatch.connection_id());
1481
1482        assert!(connection.logical_close(CloseReason::ProtocolClosed));
1483        assert!(ConnectionState::try_commit_dispatch(&connection).is_none());
1484        assert_eq!(1, connection.probe().in_flight);
1485
1486        dispatch.release();
1487        assert_eq!(0, connection.probe().in_flight);
1488    }
1489
1490    #[test]
1491    fn physical_connection_guard_is_created_once_with_the_connection() {
1492        let (connection, physical) = ConnectionState::unbounded(test_info(1));
1493        assert_eq!(ConnectionId::new(1), connection.id());
1494        assert_eq!(PartitionId::from_index(0), connection.owner_partition());
1495        assert!(!connection.probe().physical_connection_complete);
1496        physical.release();
1497        assert!(connection.probe().physical_connection_complete);
1498    }
1499
1500    #[test]
1501    fn connection_state_retains_immutable_connection_info() {
1502        #[derive(Clone, Debug, Eq, PartialEq)]
1503        struct ConnectorMarker(&'static str);
1504
1505        let origin =
1506            OriginKey::from_parts(http_1x::uri::Scheme::HTTPS, "example.com", None).unwrap();
1507        let info = ConnectionInfo::new(
1508            ConnectionId::new(7),
1509            origin.clone(),
1510            PartitionId::from_index(2),
1511            ConnectionProtocol::Http1,
1512            Connected::new()
1513                .proxy(true)
1514                .extra(ConnectorMarker("connector-extra")),
1515        );
1516        let (connection, _physical) = ConnectionState::unbounded(info);
1517
1518        assert_eq!(ConnectionId::new(7), connection.info().id());
1519        assert_eq!(&origin, connection.info().origin());
1520        assert_eq!(PartitionId::from_index(2), connection.owner_partition());
1521        assert_eq!(ConnectionProtocol::Http1, connection.info().protocol());
1522        assert_eq!(None, connection.info().local_addr());
1523        assert_eq!(None, connection.info().remote_addr());
1524        assert_eq!(ConnectPath::ForwardProxy, connection.info().connect_path());
1525        assert_eq!(None, connection.info().establishment());
1526        let mut extensions = Extensions::new();
1527        connection.info().apply_connector_extras(&mut extensions);
1528        assert_eq!(
1529            Some(&ConnectorMarker("connector-extra")),
1530            extensions.get::<ConnectorMarker>()
1531        );
1532    }
1533
1534    #[test]
1535    fn dropping_connection_io_completes_physical_connection_ownership() {
1536        let (connection, physical) = ConnectionState::unbounded(test_info(1));
1537        let io = ConnectionIo::new("transport", physical);
1538        assert_eq!(&"transport", io.get_ref());
1539        assert!(!connection.probe().physical_connection_complete);
1540
1541        drop(io);
1542
1543        assert!(connection.probe().physical_connection_complete);
1544    }
1545}
1546
1547#[cfg(all(test, smithy_http_client_loom))]
1548mod loom_tests {
1549    use super::*;
1550    use crate::client::pool::admission::OriginAdmission;
1551    use crate::client::pool::events::{
1552        ConnectionEvent, ConnectionEvents, SharedConnectionEventListener,
1553    };
1554    use aws_smithy_async::time::SharedTimeSource;
1555    use loom::sync::atomic::{AtomicUsize, Ordering};
1556    use std::num::NonZeroUsize;
1557
1558    fn test_info(id: u64) -> Arc<ConnectionInfo> {
1559        ConnectionInfo::for_test(ConnectionId::new(id), PartitionId::from_index(0))
1560    }
1561
1562    /// Races request dispatch commitment with logical close.
1563    ///
1564    /// Dispatch either commits before close and drains afterward, or close
1565    /// rejects it without incrementing the in-flight count.
1566    #[test]
1567    fn dispatch_commit_linearizes_against_close() {
1568        loom::model(|| {
1569            let (connection, _physical) = ConnectionState::unbounded(test_info(1));
1570
1571            let dispatch_connection = connection.clone();
1572            let dispatch = loom::thread::spawn(move || {
1573                ConnectionState::try_commit_dispatch(&dispatch_connection)
1574            });
1575            let close_connection = connection.clone();
1576            let close = loom::thread::spawn(move || {
1577                close_connection.logical_close(CloseReason::ProtocolClosed)
1578            });
1579
1580            let dispatch = dispatch.join().unwrap();
1581            assert!(close.join().unwrap());
1582            assert!(ConnectionState::try_commit_dispatch(&connection).is_none());
1583            match dispatch {
1584                Some(dispatch) => {
1585                    let probe = connection.probe();
1586                    assert_eq!(None, probe.close_reason);
1587                    assert!(probe.awaiting_h1_exchange);
1588                    assert_eq!(1, probe.in_flight);
1589                    drop(dispatch);
1590                    let probe = connection.probe();
1591                    assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
1592                    assert!(!probe.awaiting_h1_exchange);
1593                    assert_eq!(0, probe.in_flight);
1594                }
1595                None => {
1596                    let probe = connection.probe();
1597                    assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
1598                    assert!(!probe.awaiting_h1_exchange);
1599                    assert_eq!(0, probe.in_flight);
1600                }
1601            }
1602        });
1603    }
1604
1605    /// Races two independent logical-close signals for one bounded connection.
1606    ///
1607    /// Exactly one reason becomes authoritative and its capacity lease returns once.
1608    #[test]
1609    fn concurrent_logical_close_releases_one_capacity_lease() {
1610        loom::model(|| {
1611            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1612            let lease = OriginAdmission::lease_for_test(&origin);
1613            let (connection, _physical) = ConnectionState::bounded(test_info(1), lease);
1614            let first_connection = connection.clone();
1615            let first =
1616                loom::thread::spawn(move || first_connection.logical_close(CloseReason::Poisoned));
1617            let second_connection = connection.clone();
1618            let second = loom::thread::spawn(move || {
1619                second_connection.logical_close(CloseReason::PoolDropped)
1620            });
1621
1622            let first = first.join().unwrap();
1623            let second = second.join().unwrap();
1624            assert_ne!(first, second);
1625            assert_eq!(1, origin.available_capacity_for_test());
1626            let reason = connection.probe().close_reason.unwrap();
1627            assert!(matches!(
1628                reason,
1629                CloseReason::Poisoned | CloseReason::PoolDropped
1630            ));
1631        });
1632    }
1633
1634    /// Races HTTP/1 upgrade classification with physical connection completion.
1635    ///
1636    /// The winning transition determines the final reason while both paths
1637    /// converge on one capacity return and zero retained lifetime counts.
1638    #[test]
1639    fn h1_upgrade_classification_linearizes_against_physical_completion() {
1640        loom::model(|| {
1641            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1642            let lease = OriginAdmission::lease_for_test(&origin);
1643            let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
1644            let dispatch = ConnectionState::try_commit_dispatch(&connection)
1645                .expect("open HTTP/1 connection rejected dispatch");
1646            assert!(connection.logical_close(CloseReason::ProtocolClosed));
1647
1648            let classify_connection = connection.clone();
1649            let classify = loom::thread::spawn(move || {
1650                classify_connection.complete_h1_exchange(CloseReason::Upgraded)
1651            });
1652            let complete_physical = loom::thread::spawn(move || physical.release());
1653
1654            let classified_as_upgrade = classify.join().unwrap();
1655            complete_physical.join().unwrap();
1656            dispatch.release();
1657
1658            let probe = connection.probe();
1659            assert_eq!(
1660                Some(if classified_as_upgrade {
1661                    CloseReason::Upgraded
1662                } else {
1663                    CloseReason::ProtocolClosed
1664                }),
1665                probe.close_reason
1666            );
1667            assert!(!probe.awaiting_h1_exchange);
1668            assert_eq!(0, probe.in_flight);
1669            assert!(probe.physical_connection_complete);
1670            assert_eq!(1, origin.available_capacity_for_test());
1671
1672            let stats = connection.stats.snapshot(0, 0, 0, 0, 0);
1673            assert_eq!(0, stats.h1().draining());
1674            assert_eq!(0, stats.h1().upgraded());
1675            assert_eq!(0, stats.physically_live_connections());
1676        });
1677    }
1678
1679    /// Races `Opened`, accepted HTTP/1 completion, and both close transitions.
1680    ///
1681    /// Each installed event must be delivered once in
1682    /// `Opened -> LogicalClose -> PhysicalClose` order.
1683    #[test]
1684    fn installed_events_remain_ordered_across_concurrent_close() {
1685        let mut model = loom::model::Builder::new();
1686        model.preemption_bound.get_or_insert(3);
1687        model.check(|| {
1688            let event_sequence = Arc::new(AtomicUsize::new(0));
1689            let observed = event_sequence.clone();
1690            let events = ConnectionEvents::new(
1691                Some(SharedConnectionEventListener::new(
1692                    move |event: &ConnectionEvent<'_>| {
1693                        let code = match event {
1694                            ConnectionEvent::Opened(_) => 1,
1695                            ConnectionEvent::LogicalClose(_) => 2,
1696                            ConnectionEvent::PhysicalClose(_) => 3,
1697                            ConnectionEvent::EstablishmentFailed(_) => 4,
1698                        };
1699                        observed
1700                            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |sequence| {
1701                                Some(sequence * 4 + code)
1702                            })
1703                            .expect("event sequence update is infallible");
1704                    },
1705                )),
1706                SharedTimeSource::default(),
1707            );
1708            let origin = OriginAdmission::for_test(NonZeroUsize::new(1).unwrap());
1709            let lease = OriginAdmission::lease_for_test(&origin);
1710            let (connection, physical) = ConnectionState::bounded(test_info(1), lease);
1711            let dispatch = ConnectionState::try_commit_dispatch(&connection)
1712                .expect("open HTTP/1 connection rejected dispatch");
1713            let establishment = events.establishment_started(
1714                connection.info().origin(),
1715                connection.owner_partition(),
1716                connection.stats.clone(),
1717            );
1718
1719            let opened_connection = connection.clone();
1720            let opened = loom::thread::spawn(move || establishment.opened(&opened_connection));
1721            let close_connection = connection.clone();
1722            let close = loom::thread::spawn(move || {
1723                close_connection.logical_close(CloseReason::ProtocolClosed)
1724            });
1725            let complete_physical = loom::thread::spawn(move || physical.release());
1726            let complete_dispatch = loom::thread::spawn(move || dispatch.release());
1727
1728            opened.join().unwrap();
1729            assert!(close.join().unwrap());
1730            complete_physical.join().unwrap();
1731            complete_dispatch.join().unwrap();
1732
1733            assert_eq!(
1734                27,
1735                event_sequence.load(Ordering::SeqCst),
1736                "installed connection events were missing, duplicated, or reordered"
1737            );
1738            assert_eq!(1, origin.available_capacity_for_test());
1739            let probe = connection.probe();
1740            assert_eq!(Some(CloseReason::ProtocolClosed), probe.close_reason);
1741            assert!(!probe.awaiting_h1_exchange);
1742            assert_eq!(0, probe.in_flight);
1743            assert!(probe.physical_connection_complete);
1744        });
1745    }
1746}