Skip to main content

aws_smithy_http_client/client/pool/
events.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Connection establishment and installed-lifetime observations.
7//!
8//! Event payloads borrow transition-owned values for one synchronous callback.
9//! Listener invocation occurs after pool state transitions release their locks.
10//! One observed establishment emits either [`ConnectionEvent::EstablishmentFailed`]
11//! or [`ConnectionEvent::Opened`]. An opened connection then emits
12//! [`ConnectionEvent::LogicalClose`] before [`ConnectionEvent::PhysicalClose`].
13
14use super::connection::{CloseReason, ConnectionInfo, ConnectionProtocol, ConnectionState};
15use super::origin::OriginKey;
16use super::partition::PartitionId;
17use super::stats::CellConnectionStats;
18use crate::sync::Arc as PoolArc;
19use aws_smithy_async::time::SharedTimeSource;
20use aws_smithy_runtime_api::client::result::ConnectorError;
21use std::error::Error;
22use std::fmt;
23use std::net::SocketAddr;
24use std::sync::atomic::{AtomicU64, Ordering};
25use std::sync::Arc;
26use std::time::{Duration, SystemTime};
27
28/// Receives connection lifecycle observations from one pool.
29///
30/// Callbacks run synchronously on the task that performs the observed
31/// transition. They must not block on work that requires progress from the
32/// same task. Pool locks are released before invocation. On unwind-capable
33/// builds, a listener panic is caught after the authoritative transition and
34/// cannot roll back pool state or cleanup.
35pub trait ConnectionEventListener: Send + Sync + 'static {
36    /// Observes one completed lifecycle transition.
37    fn on_event(&self, event: &ConnectionEvent<'_>);
38}
39
40impl<F> ConnectionEventListener for F
41where
42    F: for<'a> Fn(&ConnectionEvent<'a>) + Send + Sync + 'static,
43{
44    fn on_event(&self, event: &ConnectionEvent<'_>) {
45        self(event);
46    }
47}
48
49/// Cloneable, type-erased connection event listener.
50///
51/// Use this wrapper with mutable builder configuration or when several pools
52/// share one listener.
53#[derive(Clone)]
54pub struct SharedConnectionEventListener(Arc<dyn ConnectionEventListener>);
55
56impl SharedConnectionEventListener {
57    /// Creates a shared listener from one concrete implementation.
58    pub fn new(listener: impl ConnectionEventListener) -> Self {
59        Self(Arc::new(listener))
60    }
61
62    /// Invokes the listener without allowing observer panics to alter pool state.
63    fn notify(&self, event: &ConnectionEvent<'_>) {
64        #[cfg(panic = "unwind")]
65        if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.on_event(event))).is_err()
66        {
67            tracing::warn!("connection event listener panicked");
68        }
69
70        #[cfg(not(panic = "unwind"))]
71        self.on_event(event);
72    }
73
74    /// Reports the first transition that rejects new dispatch.
75    pub(super) fn logical_close(
76        &self,
77        connection: &PoolArc<ConnectionInfo>,
78        cause: LogicalCloseCause,
79    ) {
80        self.notify(&ConnectionEvent::LogicalClose(ConnectionLogicalClose {
81            connection,
82            cause,
83        }));
84    }
85
86    /// Reports release of the client's root transport ownership.
87    pub(super) fn physical_close(&self, connection: &PoolArc<ConnectionInfo>, reason: CloseReason) {
88        self.notify(&ConnectionEvent::PhysicalClose(ConnectionPhysicalClose {
89            connection,
90            reason,
91        }));
92    }
93}
94
95impl ConnectionEventListener for SharedConnectionEventListener {
96    fn on_event(&self, event: &ConnectionEvent<'_>) {
97        self.0.on_event(event);
98    }
99}
100
101impl fmt::Debug for SharedConnectionEventListener {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.write_str("SharedConnectionEventListener")
104    }
105}
106
107/// One connection establishment or installed-lifetime observation.
108///
109/// Payload references are valid for the synchronous callback. Clone or copy
110/// the individual values needed after the callback returns.
111#[derive(Debug)]
112#[non_exhaustive]
113pub enum ConnectionEvent<'a> {
114    /// An establishment ended without installing a connection.
115    EstablishmentFailed(ConnectionEstablishmentFailed<'a>),
116    /// An establishment installed a connection for dispatch.
117    Opened(ConnectionOpened<'a>),
118    /// An installed connection stopped accepting new dispatch.
119    LogicalClose(ConnectionLogicalClose<'a>),
120    /// The client released its root transport ownership.
121    PhysicalClose(ConnectionPhysicalClose<'a>),
122}
123
124/// Opaque identity for one connection establishment.
125///
126/// Establishment IDs are unique within their assigning pool. A retry that
127/// starts another establishment receives another ID.
128#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
129pub struct ConnectionEstablishmentId(u64);
130
131impl fmt::Display for ConnectionEstablishmentId {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        self.0.fmt(f)
134    }
135}
136
137/// Stable identity and routing context for one pool-level establishment.
138///
139/// Address fallback performed inside a connector remains part of this one
140/// establishment.
141#[derive(Debug)]
142pub struct ConnectionEstablishmentInfo {
143    id: ConnectionEstablishmentId,
144    origin: OriginKey,
145    partition: PartitionId,
146}
147
148impl ConnectionEstablishmentInfo {
149    /// Returns this establishment's pool-assigned identity.
150    pub fn id(&self) -> ConnectionEstablishmentId {
151        self.id
152    }
153
154    /// Returns the canonical origin being connected.
155    pub fn origin(&self) -> &OriginKey {
156        &self.origin
157    }
158
159    /// Returns the partition that owns the establishment task.
160    pub fn partition(&self) -> PartitionId {
161        self.partition
162    }
163}
164
165/// Measurements collected during one pool-level connection establishment.
166///
167/// A later retry receives a different [`ConnectionEstablishmentId`] and its own
168/// measurements.
169#[derive(Clone, Copy, Debug)]
170#[non_exhaustive]
171pub struct ConnectionEstablishmentStats {
172    total_duration: Duration,
173    transport_duration: Duration,
174    protocol_handshake_duration: Option<Duration>,
175}
176
177impl ConnectionEstablishmentStats {
178    /// Returns elapsed time through successful protocol installation or terminal failure.
179    ///
180    /// Successful measurements are frozen before the connection is published
181    /// to waiting pool demand.
182    pub fn total_duration(&self) -> Duration {
183        self.total_duration
184    }
185
186    /// Returns elapsed time spent in the configured transport connector.
187    ///
188    /// This may include DNS, socket, proxy, and TLS work. Custom connectors may
189    /// perform a different set of transport operations.
190    pub fn transport_duration(&self) -> Duration {
191        self.transport_duration
192    }
193
194    /// Returns elapsed time spent in the Hyper protocol handshake, when started.
195    ///
196    /// Failures before protocol handshake return `None`.
197    pub fn protocol_handshake_duration(&self) -> Option<Duration> {
198        self.protocol_handshake_duration
199    }
200
201    fn connection_metadata(
202        &self,
203    ) -> aws_smithy_runtime_api::client::connection::ConnectionEstablishmentMetadata {
204        let mut builder =
205            aws_smithy_runtime_api::client::connection::ConnectionEstablishmentMetadata::builder()
206                .total_duration(self.total_duration)
207                .transport_duration(self.transport_duration);
208        builder.set_protocol_handshake_duration(self.protocol_handshake_duration);
209        builder.build()
210    }
211}
212
213/// Stage that terminated one failed connection establishment.
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215#[non_exhaustive]
216pub enum ConnectionEstablishmentStage {
217    /// Work performed by the configured transport connector.
218    Transport,
219    /// Selection of an HTTP protocol compatible with the request.
220    ProtocolSelection,
221    /// Hyper's HTTP/1 or HTTP/2 client handshake.
222    ProtocolHandshake,
223    /// Transfer of a handshaken connection into pool-owned protocol state.
224    PoolInstallation,
225}
226
227/// A terminal failure before a connection became available for pool dispatch.
228#[derive(Debug)]
229pub struct ConnectionEstablishmentFailed<'a> {
230    establishment: &'a ConnectionEstablishmentInfo,
231    stats: &'a ConnectionEstablishmentStats,
232    stage: ConnectionEstablishmentStage,
233    remote_addr: Option<SocketAddr>,
234    protocol: Option<ConnectionProtocol>,
235    error: &'a (dyn Error + Send + Sync),
236}
237
238impl<'a> ConnectionEstablishmentFailed<'a> {
239    /// Returns the failed establishment's identity and routing context.
240    pub fn establishment(&self) -> &'a ConnectionEstablishmentInfo {
241        self.establishment
242    }
243
244    /// Returns measurements collected before the failure.
245    pub fn stats(&self) -> &'a ConnectionEstablishmentStats {
246        self.stats
247    }
248
249    /// Returns the stage that terminated the establishment.
250    pub fn stage(&self) -> ConnectionEstablishmentStage {
251        self.stage
252    }
253
254    /// Returns the connector-reported remote address, when transport completed
255    /// before a later stage failed.
256    pub fn remote_addr(&self) -> Option<SocketAddr> {
257        self.remote_addr
258    }
259
260    /// Returns the selected HTTP protocol, when selection completed.
261    pub fn protocol(&self) -> Option<ConnectionProtocol> {
262        self.protocol
263    }
264
265    /// Returns the terminal error delivered to connection acquisition.
266    pub fn error(&self) -> &'a (dyn Error + Send + Sync) {
267        self.error
268    }
269}
270
271/// A connection installed and available for pool dispatch.
272#[derive(Debug)]
273pub struct ConnectionOpened<'a> {
274    establishment: &'a ConnectionEstablishmentInfo,
275    stats: &'a ConnectionEstablishmentStats,
276    connection: &'a PoolArc<ConnectionInfo>,
277}
278
279impl<'a> ConnectionOpened<'a> {
280    /// Returns the establishment that created this connection.
281    pub fn establishment(&self) -> &'a ConnectionEstablishmentInfo {
282        self.establishment
283    }
284
285    /// Returns measurements frozen after protocol installation and before pool publication.
286    pub fn stats(&self) -> &'a ConnectionEstablishmentStats {
287        self.stats
288    }
289
290    /// Returns immutable identity and transport facts for the connection.
291    pub fn connection(&self) -> &'a PoolArc<ConnectionInfo> {
292        self.connection
293    }
294}
295
296/// Stable cause recorded when pool dispatch ownership ends.
297///
298/// The later physical-close event may carry a more specific [`CloseReason`].
299#[derive(Clone, Copy, Debug, Eq, PartialEq)]
300#[non_exhaustive]
301pub enum LogicalCloseCause {
302    /// The connection exceeded its configured idle timeout.
303    IdleTimeout,
304    /// The connection was explicitly marked unsafe for reuse.
305    Poisoned,
306    /// Hyper's protocol dispatch ownership ended.
307    ProtocolEnded,
308    /// HTTP/1 did not prove a complete reusable message boundary.
309    IncompleteH1Exchange,
310    /// The connection closed to move bounded capacity to another cell.
311    Reclaimed,
312    /// The connection pool was dropped.
313    PoolDropped,
314    /// The runtime driving the connection shut down.
315    OwnerRuntimeShutdown,
316}
317
318impl LogicalCloseCause {
319    /// Maps the first pool close classification to its stable logical cause.
320    pub(super) fn from_reason(reason: CloseReason) -> Self {
321        match reason {
322            CloseReason::IdleTimeout => Self::IdleTimeout,
323            CloseReason::Poisoned => Self::Poisoned,
324            CloseReason::ProtocolClosed | CloseReason::Upgraded => Self::ProtocolEnded,
325            CloseReason::IncompleteH1Exchange => Self::IncompleteH1Exchange,
326            CloseReason::Reclaimed => Self::Reclaimed,
327            CloseReason::PoolDropped => Self::PoolDropped,
328            CloseReason::OwnerRuntimeShutdown => Self::OwnerRuntimeShutdown,
329        }
330    }
331}
332
333/// Observation that an installed connection stopped accepting new dispatch.
334///
335/// Accepted HTTP/2 streams, an HTTP/1 exchange, or root transport I/O may
336/// remain live after this event.
337#[derive(Debug)]
338pub struct ConnectionLogicalClose<'a> {
339    connection: &'a PoolArc<ConnectionInfo>,
340    cause: LogicalCloseCause,
341}
342
343impl<'a> ConnectionLogicalClose<'a> {
344    /// Returns immutable identity and transport facts for the connection.
345    pub fn connection(&self) -> &'a PoolArc<ConnectionInfo> {
346        self.connection
347    }
348
349    /// Returns the first cause that ended pool dispatch ownership.
350    pub fn cause(&self) -> LogicalCloseCause {
351        self.cause
352    }
353}
354
355/// Observation that the client released root transport ownership.
356///
357/// This is the final event for an installed connection. It does not assert
358/// that peer, kernel, or TCP teardown has completed.
359#[derive(Debug)]
360pub struct ConnectionPhysicalClose<'a> {
361    connection: &'a PoolArc<ConnectionInfo>,
362    reason: CloseReason,
363}
364
365impl<'a> ConnectionPhysicalClose<'a> {
366    /// Returns immutable identity and transport facts for the connection.
367    pub fn connection(&self) -> &'a PoolArc<ConnectionInfo> {
368        self.connection
369    }
370
371    /// Returns the final protocol and ownership close classification.
372    pub fn reason(&self) -> CloseReason {
373        self.reason
374    }
375}
376
377/// Pool-owned connection event source.
378///
379/// This owner retains callback delivery, establishment timing, and public
380/// establishment identity allocation. Timing is collected for successful
381/// connection metadata even when callback delivery is disabled.
382#[derive(Debug)]
383pub(super) struct ConnectionEvents {
384    listener: Option<SharedConnectionEventListener>,
385    time_source: SharedTimeSource,
386    next_establishment_id: AtomicU64,
387}
388
389impl ConnectionEvents {
390    /// Creates the event source retained by one connection pool.
391    pub(super) fn new(
392        listener: Option<SharedConnectionEventListener>,
393        time_source: SharedTimeSource,
394    ) -> Self {
395        Self {
396            listener,
397            time_source,
398            next_establishment_id: AtomicU64::new(0),
399        }
400    }
401
402    /// Records the start of one connection establishment.
403    pub(super) fn establishment_started(
404        &self,
405        origin: &OriginKey,
406        partition: PartitionId,
407        connection_stats: PoolArc<CellConnectionStats>,
408    ) -> ConnectionEstablishment {
409        connection_stats.establishment_started();
410        let timing = EstablishmentTiming {
411            time_source: self.time_source.clone(),
412            started_at: self.time_source.now(),
413            protocol_handshake_started_at: None,
414            transport_duration: None,
415            protocol_handshake_duration: None,
416        };
417        let observation = self
418            .listener
419            .as_ref()
420            .map(|listener| EstablishmentObservation {
421                listener: listener.clone(),
422                info: ConnectionEstablishmentInfo {
423                    id: ConnectionEstablishmentId(
424                        self.next_establishment_id.fetch_add(1, Ordering::Relaxed),
425                    ),
426                    origin: origin.clone(),
427                    partition,
428                },
429                stage: ConnectionEstablishmentStage::Transport,
430                remote_addr: None,
431                protocol: None,
432            });
433        ConnectionEstablishment {
434            timing,
435            observation,
436            successful_stats: None,
437            connection_stats: Some(connection_stats),
438        }
439    }
440}
441
442/// Tracks measurements and optional callback state for one establishment.
443pub(super) struct ConnectionEstablishment {
444    timing: EstablishmentTiming,
445    observation: Option<EstablishmentObservation>,
446    successful_stats: Option<ConnectionEstablishmentStats>,
447    connection_stats: Option<PoolArc<CellConnectionStats>>,
448}
449
450/// Timing collected for every connection establishment.
451struct EstablishmentTiming {
452    time_source: SharedTimeSource,
453    started_at: SystemTime,
454    protocol_handshake_started_at: Option<SystemTime>,
455    transport_duration: Option<Duration>,
456    protocol_handshake_duration: Option<Duration>,
457}
458
459/// Callback state retained only when a listener is configured.
460struct EstablishmentObservation {
461    listener: SharedConnectionEventListener,
462    info: ConnectionEstablishmentInfo,
463    stage: ConnectionEstablishmentStage,
464    remote_addr: Option<SocketAddr>,
465    protocol: Option<ConnectionProtocol>,
466}
467
468impl ConnectionEstablishment {
469    /// Records completion of the transport stage.
470    pub(super) fn transport_completed(&mut self, remote_addr: Option<SocketAddr>) {
471        self.timing.transport_duration = Some(self.timing.elapsed_since(self.timing.started_at));
472        if let Some(observation) = &mut self.observation {
473            observation.remote_addr = remote_addr;
474            observation.stage = ConnectionEstablishmentStage::ProtocolSelection;
475        }
476    }
477
478    /// Records the protocol selected from connector metadata.
479    pub(super) fn protocol_selected(&mut self, protocol: ConnectionProtocol) {
480        if let Some(observation) = &mut self.observation {
481            observation.protocol = Some(protocol);
482        }
483    }
484
485    /// Records entry into Hyper's protocol handshake.
486    pub(super) fn protocol_handshake_started(&mut self) {
487        self.timing.protocol_handshake_started_at = Some(self.timing.time_source.now());
488        if let Some(observation) = &mut self.observation {
489            observation.stage = ConnectionEstablishmentStage::ProtocolHandshake;
490        }
491    }
492
493    /// Records a failed Hyper handshake before emitting its terminal event.
494    pub(super) fn protocol_handshake_failed(&mut self) {
495        self.timing.finish_protocol_handshake();
496    }
497
498    /// Records successful Hyper handshake before pool installation.
499    pub(super) fn protocol_handshake_completed(&mut self) {
500        self.timing.finish_protocol_handshake();
501        if let Some(observation) = &mut self.observation {
502            observation.stage = ConnectionEstablishmentStage::PoolInstallation;
503        }
504    }
505
506    /// Freezes successful measurements before the connection becomes visible.
507    pub(super) fn installed(&mut self, connection: &PoolArc<ConnectionState>) {
508        let stats = self.timing.stats();
509        connection
510            .info()
511            .set_establishment(stats.connection_metadata());
512        assert!(
513            self.successful_stats.replace(stats).is_none(),
514            "connection establishment was installed more than once"
515        );
516    }
517
518    /// Emits the terminal failure for this establishment.
519    pub(super) fn failed(mut self, error: &ConnectorError) {
520        let stats = self.timing.stats();
521        self.finish_connection_stats();
522        let Some(observation) = self.observation.take() else {
523            return;
524        };
525        observation.notify_failure(error, &stats);
526    }
527
528    /// Reports successful installation and enables ordered close observations.
529    pub(super) fn opened(mut self, connection: &PoolArc<ConnectionState>) {
530        let stats = self.successful_stats.take().unwrap_or_else(|| {
531            let stats = self.timing.stats();
532            connection
533                .info()
534                .set_establishment(stats.connection_metadata());
535            stats
536        });
537        self.finish_connection_stats();
538        let listener = self.observation.take().map(|observation| {
539            observation
540                .listener
541                .notify(&ConnectionEvent::Opened(ConnectionOpened {
542                    establishment: &observation.info,
543                    stats: &stats,
544                    connection: connection.info(),
545                }));
546            observation.listener
547        });
548        connection.complete_opened_event(listener.as_ref());
549    }
550
551    /// Ends an establishment whose transport lost to existing HTTP/2 supply.
552    pub(super) fn superseded(mut self) {
553        self.finish_connection_stats();
554        self.observation.take();
555    }
556
557    fn finish_connection_stats(&mut self) {
558        if let Some(stats) = self.connection_stats.take() {
559            stats.establishment_finished();
560        }
561    }
562}
563
564impl Drop for ConnectionEstablishment {
565    fn drop(&mut self) {
566        let stats = self.successful_stats.unwrap_or_else(|| self.timing.stats());
567        self.finish_connection_stats();
568        let Some(observation) = self.observation.take() else {
569            return;
570        };
571        let error = ConnectorError::io("connection establishment task was dropped".into());
572        observation.notify_failure(&error, &stats);
573    }
574}
575
576impl EstablishmentObservation {
577    fn notify_failure(&self, error: &ConnectorError, stats: &ConnectionEstablishmentStats) {
578        self.listener.notify(&ConnectionEvent::EstablishmentFailed(
579            ConnectionEstablishmentFailed {
580                establishment: &self.info,
581                stats,
582                stage: self.stage,
583                remote_addr: self.remote_addr,
584                protocol: self.protocol,
585                error,
586            },
587        ));
588    }
589}
590
591impl EstablishmentTiming {
592    fn stats(&self) -> ConnectionEstablishmentStats {
593        let total_duration = self.elapsed_since(self.started_at);
594        ConnectionEstablishmentStats {
595            total_duration,
596            transport_duration: self.transport_duration.unwrap_or(total_duration),
597            protocol_handshake_duration: self.protocol_handshake_duration.or_else(|| {
598                self.protocol_handshake_started_at
599                    .map(|started_at| self.elapsed_since(started_at))
600            }),
601        }
602    }
603
604    fn finish_protocol_handshake(&mut self) {
605        self.protocol_handshake_duration = self
606            .protocol_handshake_started_at
607            .take()
608            .map(|started_at| self.elapsed_since(started_at));
609    }
610
611    fn elapsed_since(&self, started_at: SystemTime) -> Duration {
612        match self.time_source.now().duration_since(started_at) {
613            Ok(duration) => duration,
614            Err(error) => {
615                tracing::warn!(?error, "connection establishment clock moved backwards");
616                Duration::ZERO
617            }
618        }
619    }
620}
621
622#[cfg(all(test, not(smithy_http_client_loom)))]
623mod tests {
624    use super::*;
625    use aws_smithy_async::{test_util::ManualTimeSource, time::StaticTimeSource};
626    use std::sync::atomic::AtomicUsize;
627    use std::sync::Mutex;
628    use std::time::UNIX_EPOCH;
629
630    fn origin() -> OriginKey {
631        OriginKey::from_parts(http_1x::uri::Scheme::HTTPS, "example.com", None).unwrap()
632    }
633
634    fn events(listener: Option<SharedConnectionEventListener>) -> ConnectionEvents {
635        ConnectionEvents::new(listener, SharedTimeSource::default())
636    }
637
638    fn establishment(events: &ConnectionEvents) -> ConnectionEstablishment {
639        events.establishment_started(
640            &origin(),
641            PartitionId::from_index(1),
642            PoolArc::new(CellConnectionStats::default()),
643        )
644    }
645
646    #[test]
647    fn disabled_callbacks_do_not_allocate_establishment_ids() {
648        let events = events(None);
649        events.next_establishment_id.store(7, Ordering::Relaxed);
650
651        establishment(&events).superseded();
652
653        assert_eq!(7, events.next_establishment_id.load(Ordering::Relaxed));
654    }
655
656    #[test]
657    fn listener_panic_is_isolated() {
658        let events = events(Some(SharedConnectionEventListener::new(
659            |_: &ConnectionEvent<'_>| panic!("listener failed"),
660        )));
661        let error = ConnectorError::io("synthetic transport failure".into());
662
663        establishment(&events).failed(&error);
664    }
665
666    #[test]
667    fn dropping_active_establishment_emits_transport_failure() {
668        let observed = Arc::new(Mutex::new(None));
669        let events = events(Some(SharedConnectionEventListener::new({
670            let observed = observed.clone();
671            move |event: &ConnectionEvent<'_>| {
672                let ConnectionEvent::EstablishmentFailed(failed) = event else {
673                    panic!("unexpected event: {event:?}");
674                };
675                *observed.lock().unwrap() = Some((failed.establishment().id(), failed.stage()));
676            }
677        })));
678
679        drop(establishment(&events));
680
681        assert_eq!(
682            Some((
683                ConnectionEstablishmentId(0),
684                ConnectionEstablishmentStage::Transport
685            )),
686            *observed.lock().unwrap()
687        );
688    }
689
690    #[test]
691    fn failed_callback_observes_finished_establishment_count() {
692        let stats = PoolArc::new(CellConnectionStats::default());
693        let observed = Arc::new(Mutex::new(None));
694        let events = events(Some(SharedConnectionEventListener::new({
695            let stats = stats.clone();
696            let observed = observed.clone();
697            move |event: &ConnectionEvent<'_>| {
698                let ConnectionEvent::EstablishmentFailed(_) = event else {
699                    panic!("unexpected event: {event:?}");
700                };
701                *observed.lock().unwrap() =
702                    Some(stats.snapshot(0, 0, 0, 0, 0).establishing_connections());
703            }
704        })));
705        let establishment =
706            events.establishment_started(&origin(), PartitionId::from_index(1), stats.clone());
707        assert_eq!(1, stats.snapshot(0, 0, 0, 0, 0).establishing_connections());
708        let error = ConnectorError::io("synthetic transport failure".into());
709
710        establishment.failed(&error);
711
712        assert_eq!(Some(0), *observed.lock().unwrap());
713    }
714
715    #[test]
716    fn failed_event_carries_recorded_stage_and_protocol() {
717        let observed = Arc::new(Mutex::new(None));
718        let events = events(Some(SharedConnectionEventListener::new({
719            let observed = observed.clone();
720            move |event: &ConnectionEvent<'_>| {
721                let ConnectionEvent::EstablishmentFailed(failed) = event else {
722                    panic!("unexpected event: {event:?}");
723                };
724                *observed.lock().unwrap() = Some((
725                    failed.establishment().id(),
726                    failed.stage(),
727                    failed.protocol(),
728                    failed.stats().protocol_handshake_duration().is_some(),
729                ));
730            }
731        })));
732        let mut establishment = establishment(&events);
733        establishment.transport_completed(None);
734        establishment.protocol_selected(ConnectionProtocol::Http2);
735        establishment.protocol_handshake_started();
736        establishment.protocol_handshake_failed();
737        let error = ConnectorError::io("synthetic handshake failure".into());
738
739        establishment.failed(&error);
740
741        assert_eq!(
742            Some((
743                ConnectionEstablishmentId(0),
744                ConnectionEstablishmentStage::ProtocolHandshake,
745                Some(ConnectionProtocol::Http2),
746                true,
747            )),
748            *observed.lock().unwrap()
749        );
750    }
751
752    #[test]
753    fn establishment_stats_measure_recorded_phases() {
754        let time = ManualTimeSource::new(UNIX_EPOCH);
755        let observed = Arc::new(Mutex::new(None));
756        let events = ConnectionEvents::new(
757            Some(SharedConnectionEventListener::new({
758                let observed = observed.clone();
759                move |event: &ConnectionEvent<'_>| {
760                    let ConnectionEvent::EstablishmentFailed(failed) = event else {
761                        panic!("unexpected event: {event:?}");
762                    };
763                    *observed.lock().unwrap() = Some(*failed.stats());
764                }
765            })),
766            SharedTimeSource::new(time.clone()),
767        );
768        let mut establishment = establishment(&events);
769
770        time.advance(Duration::from_secs(2));
771        establishment.transport_completed(None);
772        establishment.protocol_selected(ConnectionProtocol::Http2);
773        establishment.protocol_handshake_started();
774        time.advance(Duration::from_secs(3));
775        establishment.protocol_handshake_completed();
776        time.advance(Duration::from_secs(2));
777        establishment.failed(&ConnectorError::io("synthetic installation failure".into()));
778
779        let stats = observed.lock().unwrap().expect("establishment stats");
780        assert_eq!(stats.total_duration(), Duration::from_secs(7));
781        assert_eq!(stats.transport_duration(), Duration::from_secs(2));
782        assert_eq!(
783            stats.protocol_handshake_duration(),
784            Some(Duration::from_secs(3))
785        );
786        let metadata = stats.connection_metadata();
787        assert_eq!(metadata.total_duration(), Duration::from_secs(7));
788        assert_eq!(metadata.transport_duration(), Duration::from_secs(2));
789        assert_eq!(
790            metadata.protocol_handshake_duration(),
791            Some(Duration::from_secs(3))
792        );
793    }
794
795    #[test]
796    fn backwards_clock_saturates_establishment_durations() {
797        let timing = EstablishmentTiming {
798            time_source: StaticTimeSource::new(UNIX_EPOCH).into(),
799            started_at: UNIX_EPOCH + Duration::from_secs(1),
800            protocol_handshake_started_at: Some(UNIX_EPOCH + Duration::from_secs(1)),
801            transport_duration: None,
802            protocol_handshake_duration: None,
803        };
804
805        let stats = timing.stats();
806
807        assert_eq!(stats.total_duration(), Duration::ZERO);
808        assert_eq!(stats.transport_duration(), Duration::ZERO);
809        assert_eq!(stats.protocol_handshake_duration(), Some(Duration::ZERO));
810    }
811
812    #[test]
813    fn superseded_establishment_emits_no_public_event() {
814        let observed = Arc::new(AtomicUsize::new(0));
815        let events = events(Some(SharedConnectionEventListener::new({
816            let observed = observed.clone();
817            move |_: &ConnectionEvent<'_>| {
818                observed.fetch_add(1, Ordering::Relaxed);
819            }
820        })));
821
822        establishment(&events).superseded();
823
824        assert_eq!(0, observed.load(Ordering::Relaxed));
825        assert_eq!(1, events.next_establishment_id.load(Ordering::Relaxed));
826    }
827
828    #[test]
829    fn closure_listener_receives_one_terminal_event() {
830        let observed = Arc::new(AtomicUsize::new(0));
831        let events = events(Some(SharedConnectionEventListener::new({
832            let observed = observed.clone();
833            move |event: &ConnectionEvent<'_>| {
834                assert!(matches!(event, ConnectionEvent::EstablishmentFailed(_)));
835                observed.fetch_add(1, Ordering::Relaxed);
836            }
837        })));
838        let error = ConnectorError::io("synthetic transport failure".into());
839
840        establishment(&events).failed(&error);
841
842        assert_eq!(1, observed.load(Ordering::Relaxed));
843    }
844
845    #[test]
846    fn logical_close_causes_cover_every_close_reason() {
847        assert_eq!(
848            [
849                LogicalCloseCause::IdleTimeout,
850                LogicalCloseCause::Poisoned,
851                LogicalCloseCause::ProtocolEnded,
852                LogicalCloseCause::ProtocolEnded,
853                LogicalCloseCause::IncompleteH1Exchange,
854                LogicalCloseCause::Reclaimed,
855                LogicalCloseCause::PoolDropped,
856                LogicalCloseCause::OwnerRuntimeShutdown,
857            ],
858            [
859                CloseReason::IdleTimeout,
860                CloseReason::Poisoned,
861                CloseReason::ProtocolClosed,
862                CloseReason::Upgraded,
863                CloseReason::IncompleteH1Exchange,
864                CloseReason::Reclaimed,
865                CloseReason::PoolDropped,
866                CloseReason::OwnerRuntimeShutdown,
867            ]
868            .map(LogicalCloseCause::from_reason)
869        );
870    }
871}