1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! Connection state tracking for pooled connections.
|
7 + |
|
8 + | use std::net::SocketAddr;
|
9 + | use std::sync::atomic::{AtomicBool, Ordering};
|
10 + | use std::sync::{Arc, Mutex};
|
11 + | use std::task::{Context, Poll};
|
12 + | use std::time::{Duration, Instant};
|
13 + |
|
14 + | use aws_smithy_async::rt::sleep::SharedAsyncSleep;
|
15 + | use aws_smithy_runtime_api::box_error::BoxError;
|
16 + | use aws_smithy_runtime_api::client::connection::{ConnectionId, ConnectionMetadata};
|
17 + | use aws_smithy_types::body::SdkBody;
|
18 + | use pin_project_lite::pin_project;
|
19 + | use tokio::sync::OwnedSemaphorePermit;
|
20 + | use tower::Service;
|
21 + |
|
22 + | use super::cache;
|
23 + | use super::handshake::H1SendRequest;
|
24 + |
|
25 + | /// Authority of a pooled connection (host with optional port).
|
26 + | #[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
27 + | pub struct Authority(Arc<str>);
|
28 + |
|
29 + | impl Authority {
|
30 + | pub(crate) fn new(s: impl Into<Arc<str>>) -> Self {
|
31 + | Self(s.into())
|
32 + | }
|
33 + |
|
34 + | /// Construct an authority key for a host, as `host` or `host:port`.
|
35 + | ///
|
36 + | /// Used to query connection counts via [`SharedPool::stats`]. The
|
37 + | /// pool keys connection state by the authority component of each
|
38 + | /// request's URI, compared as an exact byte string. The value passed
|
39 + | /// here must match that form for a lookup to hit: notably, a port is
|
40 + | /// present only when the URI carried a non-default port (an HTTPS URI
|
41 + | /// to `example.com` keys as `example.com`, not `example.com:443`), and
|
42 + | /// the host is matched case-sensitively. A value that does not match
|
43 + | /// any keyed authority yields empty stats rather than an error.
|
44 + | ///
|
45 + | /// [`SharedPool::stats`]: crate::client::pool::SharedPool::stats
|
46 + | pub fn from_host(host: impl Into<Arc<str>>) -> Self {
|
47 + | Self(host.into())
|
48 + | }
|
49 + |
|
50 + | /// The authority as a string slice.
|
51 + | pub fn as_str(&self) -> &str {
|
52 + | &self.0
|
53 + | }
|
54 + | }
|
55 + |
|
56 + | impl AsRef<str> for Authority {
|
57 + | fn as_ref(&self) -> &str {
|
58 + | &self.0
|
59 + | }
|
60 + | }
|
61 + |
|
62 + | impl std::fmt::Display for Authority {
|
63 + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
64 + | self.0.fmt(f)
|
65 + | }
|
66 + | }
|
67 + |
|
68 + | /// Protocol negotiated for a connection.
|
69 + | #[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
70 + | #[non_exhaustive]
|
71 + | pub enum NegotiatedProtocol {
|
72 + | /// HTTP/1.1
|
73 + | Http1,
|
74 + | /// HTTP/2
|
75 + | Http2,
|
76 + | }
|
77 + |
|
78 + | /// Why a connection was removed from the pool.
|
79 + | #[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
80 + | #[non_exhaustive]
|
81 + | pub enum CloseReason {
|
82 + | /// Idle longer than the configured pool idle timeout.
|
83 + | IdleTimeout,
|
84 + | /// Marked as poisoned (unhealthy) by the SDK or pool.
|
85 + | Poisoned,
|
86 + | /// Connection found dead at checkout (e.g., server closed the
|
87 + | /// connection while it was idle in the pool). The associated error
|
88 + | /// on the event carries specifics.
|
89 + | Unusable,
|
90 + | /// The pool itself was dropped.
|
91 + | PoolDropped,
|
92 + | /// Dropped to free a connection permit for another partition under
|
93 + | /// cap pressure (cross-partition active reclaim). Distinct from
|
94 + | /// `IdleTimeout`: the connection was still within its idle window but
|
95 + | /// was reclaimed because a starved partition needed the capacity.
|
96 + | Reclaimed,
|
97 + | }
|
98 + |
|
99 + | /// Timing breakdown for connection establishment.
|
100 + | #[derive(Clone, Copy, Debug)]
|
101 + | #[non_exhaustive]
|
102 + | pub struct ConnectionTiming {
|
103 + | /// TCP connect + TLS handshake combined. Measured from connector call
|
104 + | /// start to connected IO stream returned.
|
105 + | connect_duration: Duration,
|
106 + | }
|
107 + |
|
108 + | impl ConnectionTiming {
|
109 + | pub(crate) fn new(connect_duration: Duration) -> Self {
|
110 + | Self { connect_duration }
|
111 + | }
|
112 + |
|
113 + | /// Total time to establish the transport (TCP + TLS).
|
114 + | pub fn connect_duration(&self) -> Duration {
|
115 + | self.connect_duration
|
116 + | }
|
117 + | }
|
118 + |
|
119 + | /// Emitted when a new connection is established (TCP + TLS + HTTP handshake).
|
120 + | #[derive(Debug)]
|
121 + | #[non_exhaustive]
|
122 + | pub struct ConnectionCreatedEvent {
|
123 + | conn_id: ConnectionId,
|
124 + | authority: Authority,
|
125 + | remote_addr: Option<SocketAddr>,
|
126 + | protocol: NegotiatedProtocol,
|
127 + | timing: ConnectionTiming,
|
128 + | }
|
129 + |
|
130 + | impl ConnectionCreatedEvent {
|
131 + | pub(crate) fn new(
|
132 + | conn_id: ConnectionId,
|
133 + | authority: Authority,
|
134 + | remote_addr: Option<SocketAddr>,
|
135 + | protocol: NegotiatedProtocol,
|
136 + | timing: ConnectionTiming,
|
137 + | ) -> Self {
|
138 + | Self {
|
139 + | conn_id,
|
140 + | authority,
|
141 + | remote_addr,
|
142 + | protocol,
|
143 + | timing,
|
144 + | }
|
145 + | }
|
146 + |
|
147 + | /// The pool-assigned connection identifier.
|
148 + | pub fn conn_id(&self) -> ConnectionId {
|
149 + | self.conn_id
|
150 + | }
|
151 + |
|
152 + | /// The authority (host:port) this connection is for.
|
153 + | pub fn authority(&self) -> &Authority {
|
154 + | &self.authority
|
155 + | }
|
156 + |
|
157 + | /// Remote address of the peer, if known.
|
158 + | pub fn remote_addr(&self) -> Option<SocketAddr> {
|
159 + | self.remote_addr
|
160 + | }
|
161 + |
|
162 + | /// Negotiated protocol.
|
163 + | pub fn protocol(&self) -> NegotiatedProtocol {
|
164 + | self.protocol
|
165 + | }
|
166 + |
|
167 + | /// Timing breakdown for connection establishment.
|
168 + | pub fn timing(&self) -> &ConnectionTiming {
|
169 + | &self.timing
|
170 + | }
|
171 + | }
|
172 + |
|
173 + | /// Emitted when an existing idle connection is checked out from the pool.
|
174 + | #[derive(Debug)]
|
175 + | #[non_exhaustive]
|
176 + | pub struct ConnectionReusedEvent {
|
177 + | conn_id: ConnectionId,
|
178 + | authority: Authority,
|
179 + | }
|
180 + |
|
181 + | impl ConnectionReusedEvent {
|
182 + | pub(crate) fn new(conn_id: ConnectionId, authority: Authority) -> Self {
|
183 + | Self { conn_id, authority }
|
184 + | }
|
185 + |
|
186 + | /// The pool-assigned connection identifier.
|
187 + | pub fn conn_id(&self) -> ConnectionId {
|
188 + | self.conn_id
|
189 + | }
|
190 + |
|
191 + | /// The authority (host:port) this connection is for.
|
192 + | pub fn authority(&self) -> &Authority {
|
193 + | &self.authority
|
194 + | }
|
195 + | }
|
196 + |
|
197 + | /// Emitted when a connection is removed from the pool.
|
198 + | #[non_exhaustive]
|
199 + | pub struct ConnectionClosedEvent {
|
200 + | conn_id: ConnectionId,
|
201 + | authority: Authority,
|
202 + | remote_addr: Option<SocketAddr>,
|
203 + | reason: CloseReason,
|
204 + | error: Option<BoxError>,
|
205 + | }
|
206 + |
|
207 + | impl std::fmt::Debug for ConnectionClosedEvent {
|
208 + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
209 + | let mut s = f.debug_struct("ConnectionClosedEvent");
|
210 + | s.field("conn_id", &self.conn_id)
|
211 + | .field("authority", &self.authority)
|
212 + | .field("remote_addr", &self.remote_addr)
|
213 + | .field("reason", &self.reason);
|
214 + | if let Some(ref e) = self.error {
|
215 + | s.field("error", &format_args!("{e}"));
|
216 + | }
|
217 + | s.finish()
|
218 + | }
|
219 + | }
|
220 + |
|
221 + | impl ConnectionClosedEvent {
|
222 + | pub(crate) fn new(
|
223 + | conn_id: ConnectionId,
|
224 + | authority: Authority,
|
225 + | remote_addr: Option<SocketAddr>,
|
226 + | reason: CloseReason,
|
227 + | error: Option<BoxError>,
|
228 + | ) -> Self {
|
229 + | Self {
|
230 + | conn_id,
|
231 + | authority,
|
232 + | remote_addr,
|
233 + | reason,
|
234 + | error,
|
235 + | }
|
236 + | }
|
237 + |
|
238 + | /// The pool-assigned connection identifier.
|
239 + | pub fn conn_id(&self) -> ConnectionId {
|
240 + | self.conn_id
|
241 + | }
|
242 + |
|
243 + | /// The authority (host:port) this connection was for.
|
244 + | pub fn authority(&self) -> &Authority {
|
245 + | &self.authority
|
246 + | }
|
247 + |
|
248 + | /// Remote address of the peer, if known.
|
249 + | pub fn remote_addr(&self) -> Option<SocketAddr> {
|
250 + | self.remote_addr
|
251 + | }
|
252 + |
|
253 + | /// Why the connection was closed.
|
254 + | pub fn reason(&self) -> CloseReason {
|
255 + | self.reason
|
256 + | }
|
257 + |
|
258 + | /// The error associated with this close, if any. Present for
|
259 + | /// server-initiated closes; `None` for policy-driven closes
|
260 + | /// (idle timeout, poisoning).
|
261 + | pub fn error(&self) -> Option<&(dyn std::error::Error + Send + Sync)> {
|
262 + | self.error.as_ref().map(|e| e.as_ref())
|
263 + | }
|
264 + | }
|
265 + |
|
266 + | /// Emitted when a connection attempt fails before completing the handshake.
|
267 + | #[non_exhaustive]
|
268 + | pub struct ConnectionFailedEvent {
|
269 + | authority: Authority,
|
270 + | remote_addr: Option<SocketAddr>,
|
271 + | error: BoxError,
|
272 + | }
|
273 + |
|
274 + | impl std::fmt::Debug for ConnectionFailedEvent {
|
275 + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
276 + | f.debug_struct("ConnectionFailedEvent")
|
277 + | .field("authority", &self.authority)
|
278 + | .field("remote_addr", &self.remote_addr)
|
279 + | .field("error", &format_args!("{}", self.error))
|
280 + | .finish()
|
281 + | }
|
282 + | }
|
283 + |
|
284 + | impl ConnectionFailedEvent {
|
285 + | pub(crate) fn new(
|
286 + | authority: Authority,
|
287 + | remote_addr: Option<SocketAddr>,
|
288 + | error: BoxError,
|
289 + | ) -> Self {
|
290 + | Self {
|
291 + | authority,
|
292 + | remote_addr,
|
293 + | error,
|
294 + | }
|
295 + | }
|
296 + |
|
297 + | /// The authority (host:port) the connection was attempting to reach.
|
298 + | pub fn authority(&self) -> &Authority {
|
299 + | &self.authority
|
300 + | }
|
301 + |
|
302 + | /// Remote address of the peer, if known. `None` when the failure occurred
|
303 + | /// before a peer address was established (e.g., DNS resolution failure or
|
304 + | /// connection refused before address binding).
|
305 + | pub fn remote_addr(&self) -> Option<SocketAddr> {
|
306 + | self.remote_addr
|
307 + | }
|
308 + |
|
309 + | /// The error that caused the connection attempt to fail.
|
310 + | pub fn error(&self) -> &(dyn std::error::Error + Send + Sync) {
|
311 + | self.error.as_ref()
|
312 + | }
|
313 + | }
|
314 + |
|
315 + | /// Callback for connection lifecycle events within the pool.
|
316 + | ///
|
317 + | /// Implementations receive notifications when connections are created,
|
318 + | /// reused from the pool, closed, or fail to establish.
|
319 + | ///
|
320 + | /// Implementations must be non-blocking. Defer expensive work to a
|
321 + | /// background task.
|
322 + | pub trait ConnectionEventListener: Send + Sync + 'static {
|
323 + | /// A new connection was established.
|
324 + | fn on_created(&self, _event: &ConnectionCreatedEvent) {}
|
325 + | /// An existing idle connection was checked out from the pool.
|
326 + | fn on_reused(&self, _event: &ConnectionReusedEvent) {}
|
327 + | /// A connection was removed from the pool.
|
328 + | fn on_closed(&self, _event: &ConnectionClosedEvent) {}
|
329 + | /// A connection attempt failed before completing the handshake.
|
330 + | fn on_connection_failed(&self, _event: &ConnectionFailedEvent) {}
|
331 + | }
|
332 + |
|
333 + | /// A duration paired with the sleep implementation used to realize it.
|
334 + | ///
|
335 + | /// This type makes "timeout without sleep impl" unrepresentable: you cannot
|
336 + | /// construct one without committing to a way to actually wait. Created at the
|
337 + | /// adapter layer from `HttpConnectorSettings` + `RuntimeComponents::sleep_impl()`
|
338 + | /// and passed down the pool stack where timeouts are applied.
|
339 + | #[derive(Clone, Debug)]
|
340 + | pub(crate) struct TimeoutContext {
|
341 + | pub(crate) duration: Duration,
|
342 + | pub(crate) sleep_impl: SharedAsyncSleep,
|
343 + | }
|
344 + |
|
345 + | impl TimeoutContext {
|
346 + | pub(crate) fn new(duration: Duration, sleep_impl: SharedAsyncSleep) -> Self {
|
347 + | Self {
|
348 + | duration,
|
349 + | sleep_impl,
|
350 + | }
|
351 + | }
|
352 + | }
|
353 + |
|
354 + | /// Target type for the connect portion of the pool stack.
|
355 + | ///
|
356 + | /// Replaces bare `Uri` so per-operation connect metadata flows through the
|
357 + | /// composable pool types (Map → Negotiate → Cache → handshake → ConnectionLimit
|
358 + | /// → TCP connector) to the layers that need them.
|
359 + | #[derive(Clone, Debug)]
|
360 + | pub(crate) struct ConnectCtx {
|
361 + | /// Target URI: its authority is the pool key, and the full URI is
|
362 + | /// passed to the TCP connector.
|
363 + | pub(crate) uri: http_1x::Uri,
|
364 + | /// Bounds new-connection establishment (TCP + TLS handshake). `None`
|
365 + | /// means no connect timeout; cached connections skip the connector
|
366 + | /// entirely so this is automatically a no-op on cache hit.
|
367 + | pub(crate) connect_timeout: Option<TimeoutContext>,
|
368 + | /// How the connect path behaves when a permit cannot be acquired.
|
369 + | pub(crate) mode: AcquireMode,
|
370 + | }
|
371 + |
|
372 + | /// Behavior of the connect path when the connection cap is reached.
|
373 + | ///
|
374 + | /// Selected per request (carried on [`ConnectCtx`]) because the same
|
375 + | /// partition stack is exercised twice under `PreferLocal`: once
|
376 + | /// `NonBlocking` to probe for local capacity, then `Blocking` as the
|
377 + | /// fallback after a peer-borrow miss.
|
378 + | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
379 + | pub(crate) enum AcquireMode {
|
380 + | /// Block on the semaphore until a permit is free (active reclaim, then
|
381 + | /// FIFO wait). The authoritative take.
|
382 + | #[default]
|
383 + | Blocking,
|
384 + | /// Return [`CapBound`] immediately on `NoPermits` instead of blocking,
|
385 + | /// so the caller can try borrowing a peer's connection first.
|
386 + | NonBlocking,
|
387 + | }
|
388 + |
|
389 + | /// Sentinel error from the connect path under [`AcquireMode::NonBlocking`]:
|
390 + | /// the connection cap is reached and no permit is available. Distinct from
|
391 + | /// any real connect failure, and distinct from negotiate's internal
|
392 + | /// `UseOther` sentinel, so it propagates verbatim up the stack.
|
393 + | #[derive(Debug)]
|
394 + | pub(crate) struct CapBound;
|
395 + |
|
396 + | impl std::fmt::Display for CapBound {
|
397 + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
398 + | f.write_str("connection cap reached (non-blocking acquire)")
|
399 + | }
|
400 + | }
|
401 + |
|
402 + | impl std::error::Error for CapBound {}
|
403 + |
|
404 + | impl CapBound {
|
405 + | /// Whether `err` is a `CapBound` sentinel anywhere in its chain.
|
406 + | pub(crate) fn is(err: &(dyn std::error::Error + 'static)) -> bool {
|
407 + | let mut e: Option<&(dyn std::error::Error + 'static)> = Some(err);
|
408 + | while let Some(cur) = e {
|
409 + | if cur.is::<CapBound>() {
|
410 + | return true;
|
411 + | }
|
412 + | e = cur.source();
|
413 + | }
|
414 + | false
|
415 + | }
|
416 + | }
|
417 + |
|
418 + | impl ConnectCtx {
|
419 + | pub(crate) fn new(uri: http_1x::Uri, connect_timeout: Option<TimeoutContext>) -> Self {
|
420 + | Self {
|
421 + | uri,
|
422 + | connect_timeout,
|
423 + | mode: AcquireMode::Blocking,
|
424 + | }
|
425 + | }
|
426 + |
|
427 + | /// Set the acquire mode (defaults to [`AcquireMode::Blocking`]).
|
428 + | pub(crate) fn with_mode(mut self, mode: AcquireMode) -> Self {
|
429 + | self.mode = mode;
|
430 + | self
|
431 + | }
|
432 + | }
|
433 + |
|
434 + | /// Request extension set by the adapter to hint a read timeout to the
|
435 + | /// checkout services (`H{1,2}Checkout`).
|
436 + | ///
|
437 + | /// `Some(...)` means: once the connection is selected (cache hit or fresh
|
438 + | /// handshake), wrap `conn.call(req)` with this timeout. Bounds request-write
|
439 + | /// + response-headers-wait only. Does NOT include pool acquire or connect
|
440 + | /// establishment (those have their own timeouts).
|
441 + | #[derive(Clone, Debug)]
|
442 + | pub(crate) struct ReadTimeoutHint(pub(crate) TimeoutContext);
|
443 + |
|
444 + | /// Permits acquired from connection limit semaphores.
|
445 + | /// Held for the lifetime of the connection; dropped when the connection is dropped.
|
446 + | pub(crate) struct ConnectionPermit {
|
447 + | _global: Option<OwnedSemaphorePermit>,
|
448 + | _per_host: Option<OwnedSemaphorePermit>,
|
449 + | }
|
450 + |
|
451 + | impl ConnectionPermit {
|
452 + | pub(crate) fn new(
|
453 + | global: Option<OwnedSemaphorePermit>,
|
454 + | per_host: Option<OwnedSemaphorePermit>,
|
455 + | ) -> Self {
|
456 + | Self {
|
457 + | _global: global,
|
458 + | _per_host: per_host,
|
459 + | }
|
460 + | }
|
461 + | }
|
462 + |
|
463 + | /// Outcome of establishing a new transport connection (TCP + TLS).
|
464 + | ///
|
465 + | /// Carries the IO handle and the connection permit acquired during
|
466 + | /// connection establishment. The IO handle drives the protocol
|
467 + | /// handshake; the permit holds the connection's slot in the pool's
|
468 + | /// limit semaphores until the connection is dropped.
|
469 + | pub(crate) struct EstablishedConnection<IO> {
|
470 + | pub(crate) io: IO,
|
471 + | pub(crate) permit: Arc<ConnectionPermit>,
|
472 + | pub(crate) establishing: super::stats::EstablishingGuard,
|
473 + | }
|
474 + |
|
475 + | /// Error returned by `ManagedConnection::poll_ready` when the connection
|
476 + | /// has been marked poisoned and should not be reused.
|
477 + | #[derive(Debug)]
|
478 + | pub(crate) struct PoisonedError;
|
479 + |
|
480 + | impl std::fmt::Display for PoisonedError {
|
481 + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
482 + | f.write_str("connection poisoned")
|
483 + | }
|
484 + | }
|
485 + |
|
486 + | impl std::error::Error for PoisonedError {}
|
487 + |
|
488 + | /// Metadata about a connection captured at establishment time.
|
489 + | ///
|
490 + | /// Captured between TLS connector output and protocol handshake (the last point
|
491 + | /// where the raw transport stream is accessible).
|
492 + | #[derive(Debug, Clone)]
|
493 + | pub(crate) struct ConnectionInfo {
|
494 + | /// Remote address of the peer. `None` when the underlying connector
|
495 + | /// did not attach `HttpInfo` to its `Connected` extras.
|
496 + | pub(crate) remote_addr: Option<SocketAddr>,
|
497 + | /// Local address of this end of the connection.
|
498 + | pub(crate) local_addr: Option<SocketAddr>,
|
499 + | /// `true` when this connection is to an HTTP proxy server (rather than
|
500 + | /// directly to the origin). Drives request-target form selection: H1
|
501 + | /// requests dispatched on a proxied connection use absolute-form URIs;
|
502 + | /// direct connections use origin-form. Populated from
|
503 + | /// [`hyper_util::client::legacy::connect::Connected::is_proxied`] at
|
504 + | /// handshake.
|
505 + | pub(crate) is_proxied: bool,
|
506 + | /// The authority (host:port) this connection is for. Populated from the
|
507 + | /// URI at handshake time.
|
508 + | pub(crate) authority: Authority,
|
509 + | }
|
510 + |
|
511 + | /// A one-shot "this connection is dead, don't reuse it" flag.
|
512 + | ///
|
513 + | /// Shared via `Arc`; all clones observe and control the same flag.
|
514 + | /// `ManagedConnection` holds one and hands out clones (via `metadata()`)
|
515 + | /// that let the adapter layer mark the connection poisoned through
|
516 + | /// smithy's `ConnectionMetadata::poison_fn`. On next checkout or return,
|
517 + | /// the pool sees the flag set and drops the connection instead of
|
518 + | /// reusing it.
|
519 + | #[derive(Debug, Clone, Default)]
|
520 + | pub(crate) struct PoisonPill {
|
521 + | flag: Arc<AtomicBool>,
|
522 + | }
|
523 + |
|
524 + | impl PoisonPill {
|
525 + | /// Create a fresh, non-poisoned pill.
|
526 + | pub(crate) fn healthy() -> Self {
|
527 + | Self::default()
|
528 + | }
|
529 + |
|
530 + | /// Mark the connection as poisoned.
|
531 + | pub(crate) fn poison(&self) {
|
532 + | self.flag.store(true, Ordering::Release);
|
533 + | }
|
534 + |
|
535 + | /// Whether the connection has been poisoned.
|
536 + | pub(crate) fn is_poisoned(&self) -> bool {
|
537 + | self.flag.load(Ordering::Acquire)
|
538 + | }
|
539 + | }
|
540 + |
|
541 + | /// A connection with SDK-owned lifecycle metadata.
|
542 + | ///
|
543 + | /// Wraps the inner service (typically `SendRequest<SdkBody>`) with state needed
|
544 + | /// for pool management: poisoning, connection identity, and permit lifetime.
|
545 + | ///
|
546 + | /// Clone is supported when the inner service is Clone (e.g., HTTP/2 multiplexed
|
547 + | /// connections). Clones share the same poison pill and connection info, so
|
548 + | /// poisoning one clone poisons all of them.
|
549 + | pub(crate) struct ManagedConnection<S> {
|
550 + | inner: S,
|
551 + | pub(crate) info: ConnectionInfo,
|
552 + | /// Stable identifier for this physical connection, unique within the
|
553 + | /// owning pool. Shared across `Clone`s (an H2 connection's multiplexed
|
554 + | /// request handles all carry the same `conn_id`). Used in tracing and
|
555 + | /// surfaced through `ConnectionMetadata` for cross-layer correlation.
|
556 + | conn_id: ConnectionId,
|
557 + | created_at: Instant,
|
558 + | /// Timestamp the connection last became idle (or its creation time, if
|
559 + | /// it has never been returned to the pool).
|
560 + | ///
|
561 + | /// - **H1**: stamped by [`CachedConnection::drop`] on the unpoisoned
|
562 + | /// return-to-pool path. Presence in the cache implies idle.
|
563 + | /// - **H2**: stamped by [`SingletonConnection::drop`] on the
|
564 + | /// `active_streams` 1 → 0 transition. Combined with
|
565 + | /// `active_streams > 0` in the H2 retain predicate, this prevents
|
566 + | /// eviction of a multiplexed connection that's still serving streams.
|
567 + | ///
|
568 + | /// `Arc<Mutex<_>>` because all `Clone`s of a `ManagedConnection`
|
569 + | /// observe the same timestamp (last-write-wins). `Mutex` over an
|
570 + | /// atomic-encoded `u64` because writes happen at most once per
|
571 + | /// request and reads at most once per eviction tick.
|
572 + | idle_at: Arc<Mutex<Instant>>,
|
573 + | /// In-flight request count, used by the H2 retain predicate to keep
|
574 + | /// actively-multiplexed connections alive regardless of `idle_at`.
|
575 + | ///
|
576 + | /// - **H1**: never incremented (always 0). H1 is serial; the cache's
|
577 + | /// idle set is the authoritative idleness signal.
|
578 + | /// - **H2**: incremented by [`SingletonConnection::call`] on dispatch,
|
579 + | /// decremented by [`SingletonConnection::drop`] when the body guard
|
580 + | /// releases.
|
581 + | ///
|
582 + | /// Arc-shared so all `Clone`s of an H2 `ManagedConnection` mutate the
|
583 + | /// same counter.
|
584 + | active_streams: Arc<std::sync::atomic::AtomicUsize>,
|
585 + | poison: PoisonPill,
|
586 + | _permit: Arc<ConnectionPermit>,
|
587 + | /// Fires `established--` when the last clone drops. Arc-shared so H2
|
588 + | /// clones share one guard that drops once.
|
589 + | _established: Arc<super::stats::EstablishedGuard>,
|
590 + | }
|
591 + |
|
592 + | impl<S> ManagedConnection<S> {
|
593 + | /// Create a new managed connection wrapping the given service.
|
594 + | pub(crate) fn new(
|
595 + | inner: S,
|
596 + | info: ConnectionInfo,
|
597 + | conn_id: ConnectionId,
|
598 + | permit: Arc<ConnectionPermit>,
|
599 + | established: super::stats::EstablishedGuard,
|
600 + | ) -> Self {
|
601 + | let now = Instant::now();
|
602 + | Self {
|
603 + | inner,
|
604 + | info,
|
605 + | conn_id,
|
606 + | created_at: now,
|
607 + | idle_at: Arc::new(Mutex::new(now)),
|
608 + | active_streams: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
609 + | poison: PoisonPill::healthy(),
|
610 + | _permit: permit,
|
611 + | _established: Arc::new(established),
|
612 + | }
|
613 + | }
|
614 + |
|
615 + | /// Stable identifier for this physical connection. Shared across
|
616 + | /// clones (H2 multiplexing).
|
617 + | pub(crate) fn conn_id(&self) -> ConnectionId {
|
618 + | self.conn_id
|
619 + | }
|
620 + |
|
621 + | /// Whether this connection has been poisoned.
|
622 + | pub(crate) fn is_poisoned(&self) -> bool {
|
623 + | self.poison.is_poisoned()
|
624 + | }
|
625 + |
|
626 + | /// Connection info (remote/local addresses).
|
627 + | #[allow(dead_code)] // accessor for telemetry/debugging consumers
|
628 + | pub(crate) fn info(&self) -> &ConnectionInfo {
|
629 + | &self.info
|
630 + | }
|
631 + |
|
632 + | /// When this connection was established.
|
633 + | #[allow(dead_code)] // accessor for connection age (telemetry/debugging)
|
634 + | pub(crate) fn created_at(&self) -> Instant {
|
635 + | self.created_at
|
636 + | }
|
637 + |
|
638 + | /// Timestamp of the last return-to-idle (or creation).
|
639 + | ///
|
640 + | /// Guaranteed meaningful for any connection sitting in the pool: the
|
641 + | /// initial value is `created_at`, and every return-to-pool overwrites
|
642 + | /// it via [`Self::mark_idle`].
|
643 + | pub(crate) fn idle_at(&self) -> Instant {
|
644 + | *self.idle_at.lock().expect("idle_at lock poisoned")
|
645 + | }
|
646 + |
|
647 + | /// Stamp the return-to-idle moment. Called by [`CachedConnection::drop`]
|
648 + | /// on the unpoisoned path.
|
649 + | pub(crate) fn mark_idle(&self) {
|
650 + | *self.idle_at.lock().expect("idle_at lock poisoned") = Instant::now();
|
651 + | }
|
652 + |
|
653 + | /// Current in-flight stream count. See [`Self::active_streams`]
|
654 + | /// (field doc) for semantics.
|
655 + | pub(crate) fn active_streams_count(&self) -> usize {
|
656 + | self.active_streams
|
657 + | .load(std::sync::atomic::Ordering::Acquire)
|
658 + | }
|
659 + |
|
660 + | /// Clone of the Arc'd stream counter, for publication through the H2
|
661 + | /// side-channel so `SingletonConnection` can increment/decrement it
|
662 + | /// without reaching through the opaque `Singled<…>`.
|
663 + | pub(crate) fn active_streams_ref(&self) -> Arc<std::sync::atomic::AtomicUsize> {
|
664 + | self.active_streams.clone()
|
665 + | }
|
666 + |
|
667 + | /// Clone of the Arc'd idle timestamp, for publication through the H2
|
668 + | /// side-channel so `SingletonConnection::drop` can stamp it on the
|
669 + | /// `active_streams` 1 → 0 transition.
|
670 + | pub(crate) fn idle_at_ref(&self) -> Arc<Mutex<Instant>> {
|
671 + | self.idle_at.clone()
|
672 + | }
|
673 + |
|
674 + | /// Mutable access to the inner service.
|
675 + | pub(crate) fn inner_mut(&mut self) -> &mut S {
|
676 + | &mut self.inner
|
677 + | }
|
678 + |
|
679 + | /// Build a smithy `ConnectionMetadata` for this connection.
|
680 + | ///
|
681 + | /// The returned metadata captures a clone of the `PoisonPill`, so
|
682 + | /// calling `ConnectionMetadata::poison()` flips this connection's
|
683 + | /// poison flag (the same flag the pool checks on checkout/return).
|
684 + | /// Address fields are copied.
|
685 + | pub(crate) fn metadata(&self) -> ConnectionMetadata {
|
686 + | let poison = self.poison.clone();
|
687 + | let conn_id = self.conn_id;
|
688 + | let remote = self.info.remote_addr;
|
689 + | let mut builder = ConnectionMetadata::builder()
|
690 + | .proxied(self.info.is_proxied)
|
691 + | .connection_id(self.conn_id)
|
692 + | .poison_fn(move || {
|
693 + | tracing::debug!(conn_id = %conn_id, ?remote, "pool: connection poisoned");
|
694 + | poison.poison();
|
695 + | });
|
696 + | builder
|
697 + | .set_remote_addr(self.info.remote_addr)
|
698 + | .set_local_addr(self.info.local_addr);
|
699 + | builder.build()
|
700 + | }
|
701 + |
|
702 + | /// `true` when the underlying connection is to an HTTP proxy. Used by
|
703 + | /// H1 dispatch to choose absolute-form URIs over origin-form.
|
704 + | pub(crate) fn is_proxied(&self) -> bool {
|
705 + | self.info.is_proxied
|
706 + | }
|
707 + | }
|
708 + |
|
709 + | impl<S: Clone> Clone for ManagedConnection<S> {
|
710 + | fn clone(&self) -> Self {
|
711 + | Self {
|
712 + | inner: self.inner.clone(),
|
713 + | info: self.info.clone(),
|
714 + | conn_id: self.conn_id,
|
715 + | created_at: self.created_at,
|
716 + | idle_at: self.idle_at.clone(),
|
717 + | active_streams: self.active_streams.clone(),
|
718 + | poison: self.poison.clone(),
|
719 + | _permit: self._permit.clone(),
|
720 + | _established: self._established.clone(),
|
721 + | }
|
722 + | }
|
723 + | }
|
724 + |
|
725 + | impl<S> Service<http_1x::Request<SdkBody>> for ManagedConnection<S>
|
726 + | where
|
727 + | S: Service<http_1x::Request<SdkBody>>,
|
728 + | S::Error: Into<BoxError>,
|
729 + | S::Future: Send + 'static,
|
730 + | S::Response: 'static,
|
731 + | {
|
732 + | type Response = S::Response;
|
733 + | type Error = BoxError;
|
734 + | type Future = std::pin::Pin<
|
735 + | Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
|
736 + | >;
|
737 + |
|
738 + | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
739 + | if self.is_poisoned() {
|
740 + | return Poll::Ready(Err(PoisonedError.into()));
|
741 + | }
|
742 + | self.inner_mut().poll_ready(cx).map_err(Into::into)
|
743 + | }
|
744 + |
|
745 + | fn call(&mut self, req: http_1x::Request<SdkBody>) -> Self::Future {
|
746 + | let fut = self.inner_mut().call(req);
|
747 + | Box::pin(async move { fut.await.map_err(Into::into) })
|
748 + | }
|
749 + | }
|
750 + |
|
751 + | /// A connection checked out from the H1 cache.
|
752 + | ///
|
753 + | /// Wraps `cache::Cached<ManagedConnection<S>>` so that poisoned connections
|
754 + | /// are dropped from the pool (via `Cached::discard`) instead of being
|
755 + | /// returned for reuse when dropped. Healthy connections return to the pool
|
756 + | /// normally through `Cached::Drop`.
|
757 + | pub(crate) struct CachedConnection<S> {
|
758 + | inner: Option<cache::Cached<ManagedConnection<S>>>,
|
759 + | listener: Option<Arc<dyn ConnectionEventListener>>,
|
760 + | counters: Arc<super::stats::ConnectionCounters>,
|
761 + | }
|
762 + |
|
763 + | impl<S> CachedConnection<S> {
|
764 + | pub(crate) fn new(
|
765 + | cached: cache::Cached<ManagedConnection<S>>,
|
766 + | listener: Option<Arc<dyn ConnectionEventListener>>,
|
767 + | counters: Arc<super::stats::ConnectionCounters>,
|
768 + | ) -> Self {
|
769 + | counters.incr_active();
|
770 + | let is_reuse = *cached.inner().idle_at.lock().unwrap() > cached.inner().created_at;
|
771 + | if is_reuse {
|
772 + | let conn_id = cached.inner().conn_id();
|
773 + | let authority = cached.inner().info.authority.clone();
|
774 + | tracing::trace!(conn_id = %conn_id, "pool: connection reused");
|
775 + | if let Some(ref l) = listener {
|
776 + | l.on_reused(&ConnectionReusedEvent::new(conn_id, authority));
|
777 + | }
|
778 + | }
|
779 + | Self {
|
780 + | inner: Some(cached),
|
781 + | listener,
|
782 + | counters,
|
783 + | }
|
784 + | }
|
785 + |
|
786 + | /// Build a smithy `ConnectionMetadata` for the underlying H1 connection.
|
787 + | ///
|
788 + | /// See [`ManagedConnection::metadata`].
|
789 + | ///
|
790 + | /// Panics if called after the inner cached handle has been consumed.
|
791 + | pub(crate) fn metadata(&self) -> ConnectionMetadata {
|
792 + | self.inner
|
793 + | .as_ref()
|
794 + | .expect("CachedConnection metadata after drop")
|
795 + | .inner()
|
796 + | .metadata()
|
797 + | }
|
798 + |
|
799 + | /// Forwards [`ManagedConnection::is_proxied`].
|
800 + | pub(crate) fn is_proxied(&self) -> bool {
|
801 + | self.inner
|
802 + | .as_ref()
|
803 + | .expect("CachedConnection is_proxied after drop")
|
804 + | .inner()
|
805 + | .is_proxied()
|
806 + | }
|
807 + |
|
808 + | /// Forwards [`ManagedConnection::conn_id`].
|
809 + | pub(crate) fn conn_id(&self) -> ConnectionId {
|
810 + | self.inner
|
811 + | .as_ref()
|
812 + | .expect("CachedConnection conn_id after drop")
|
813 + | .inner()
|
814 + | .conn_id()
|
815 + | }
|
816 + | }
|
817 + |
|
818 + | impl<S, Req> Service<Req> for CachedConnection<S>
|
819 + | where
|
820 + | cache::Cached<ManagedConnection<S>>: Service<Req>,
|
821 + | {
|
822 + | type Response = <cache::Cached<ManagedConnection<S>> as Service<Req>>::Response;
|
823 + | type Error = <cache::Cached<ManagedConnection<S>> as Service<Req>>::Error;
|
824 + | type Future = <cache::Cached<ManagedConnection<S>> as Service<Req>>::Future;
|
825 + |
|
826 + | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
827 + | match self.inner.as_mut().unwrap().poll_ready(cx) {
|
828 + | Poll::Ready(Err(e)) => {
|
829 + | // Connection is dead. If poisoned, Drop handles the event.
|
830 + | // If not poisoned, this is an unusable connection (server
|
831 + | // closed it while idle, driver died, etc.).
|
832 + | let managed = self.inner.as_ref().unwrap().inner();
|
833 + | if !managed.is_poisoned() {
|
834 + | if let Some(ref l) = self.listener {
|
835 + | l.on_closed(&ConnectionClosedEvent::new(
|
836 + | managed.conn_id,
|
837 + | managed.info.authority.clone(),
|
838 + | managed.info.remote_addr,
|
839 + | CloseReason::Unusable,
|
840 + | None,
|
841 + | ));
|
842 + | }
|
843 + | }
|
844 + | Poll::Ready(Err(e))
|
845 + | }
|
846 + | other => other,
|
847 + | }
|
848 + | }
|
849 + |
|
850 + | fn call(&mut self, req: Req) -> Self::Future {
|
851 + | self.inner.as_mut().unwrap().call(req)
|
852 + | }
|
853 + | }
|
854 + |
|
855 + | impl<S> Drop for CachedConnection<S> {
|
856 + | fn drop(&mut self) {
|
857 + | self.counters.decr_active();
|
858 + | if let Some(cached) = self.inner.take() {
|
859 + | let managed = cached.inner();
|
860 + | let conn_id = managed.conn_id;
|
861 + | if managed.is_poisoned() {
|
862 + | tracing::debug!(conn_id = %conn_id, "pool: connection discarded (poisoned)");
|
863 + | if let Some(ref listener) = self.listener {
|
864 + | listener.on_closed(&ConnectionClosedEvent::new(
|
865 + | managed.conn_id(),
|
866 + | managed.info.authority.clone(),
|
867 + | managed.info.remote_addr,
|
868 + | CloseReason::Poisoned,
|
869 + | None,
|
870 + | ));
|
871 + | }
|
872 + | cached.discard();
|
873 + | } else {
|
874 + | managed.mark_idle();
|
875 + | tracing::trace!(conn_id = %conn_id, "pool: connection returned to idle");
|
876 + | }
|
877 + | }
|
878 + | }
|
879 + | }
|
880 + |
|
881 + | /// State an H2 checkout (`SingletonConnection`) needs but cannot reach
|
882 + | /// through the opaque `Singled<…>` to read from the underlying
|
883 + | /// `ManagedConnection`. Published by the H2 handshake on each new
|
884 + | /// connection; consumed by `SingletonConnection::new` as a snapshot.
|
885 + | ///
|
886 + | /// `active_streams` and `idle_at` are clones of the Arcs held by the
|
887 + | /// `ManagedConnection`, so updates through this state are visible to the
|
888 + | /// retain predicate operating on the connection directly.
|
889 + | #[derive(Clone)]
|
890 + | pub(crate) struct H2ConnectionState {
|
891 + | pub(crate) metadata: ConnectionMetadata,
|
892 + | pub(crate) active_streams: Arc<std::sync::atomic::AtomicUsize>,
|
893 + | pub(crate) idle_at: Arc<Mutex<Instant>>,
|
894 + | }
|
895 + |
|
896 + | /// Per-host side-channel between H2 handshake (writer) and H2 checkout
|
897 + | /// (reader).
|
898 + | ///
|
899 + | /// `hyper_util::client::pool::singleton::Singled<…>` is opaque, so the
|
900 + | /// checkout side cannot reach through it to the underlying
|
901 + | /// `ManagedConnection`. This ref carries everything the checkout side
|
902 + | /// needs (stamped fresh on each handshake): the user-facing
|
903 + | /// `ConnectionMetadata` for poison and address surfacing, plus the
|
904 + | /// Arc-shared `active_streams` counter and `idle_at` timestamp that the
|
905 + | /// retain predicate consults and `SingletonConnection` mutates.
|
906 + | ///
|
907 + | /// On re-handshake (poisoning, GOAWAY, etc.), the entire state is
|
908 + | /// replaced last-writer-wins. A `SingletonConnection` constructed against
|
909 + | /// the previous handshake holds its own snapshot, so its
|
910 + | /// increments/decrements still target the right counter.
|
911 + | #[derive(Clone, Default)]
|
912 + | pub(crate) struct H2ConnectionRef {
|
913 + | inner: Arc<std::sync::Mutex<Option<H2ConnectionState>>>,
|
914 + | }
|
915 + |
|
916 + | impl H2ConnectionRef {
|
917 + | pub(crate) fn new() -> Self {
|
918 + | Self::default()
|
919 + | }
|
920 + |
|
921 + | /// Replace the current state with a freshly-handshaked connection's
|
922 + | /// state (last-writer-wins).
|
923 + | pub(crate) fn publish(&self, state: H2ConnectionState) {
|
924 + | *self.inner.lock().unwrap() = Some(state);
|
925 + | }
|
926 + |
|
927 + | /// Snapshot of the published state. `None` until the first
|
928 + | /// successful handshake for this host.
|
929 + | pub(crate) fn current(&self) -> Option<H2ConnectionState> {
|
930 + | self.inner.lock().unwrap().clone()
|
931 + | }
|
932 + | }
|
933 + |
|
934 + | /// RAII guard for one in-flight dispatch against an H2 connection.
|
935 + | ///
|
936 + | /// Existence reflects "this checkout has dispatched a request; the
|
937 + | /// connection is busy on its behalf." Constructed by
|
938 + | /// [`SingletonConnection::call`] when a request is dispatched; dropped
|
939 + | /// when the response body's guard releases.
|
940 + | ///
|
941 + | /// Drop decrements `active_streams` and, on the 1 → 0 transition, stamps
|
942 + | /// `idle_at` (the moment the connection becomes truly idle).
|
943 + | struct DispatchGuard {
|
944 + | active_streams: Arc<std::sync::atomic::AtomicUsize>,
|
945 + | idle_at: Arc<Mutex<Instant>>,
|
946 + | counters: Arc<super::stats::ConnectionCounters>,
|
947 + | }
|
948 + |
|
949 + | impl DispatchGuard {
|
950 + | /// Start a dispatch against the connection described by `state`.
|
951 + | /// Increments `active_streams`; the returned guard releases the
|
952 + | /// increment on drop.
|
953 + | fn start(state: &H2ConnectionState, counters: Arc<super::stats::ConnectionCounters>) -> Self {
|
954 + | state
|
955 + | .active_streams
|
956 + | .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
|
957 + | counters.incr_active();
|
958 + | Self {
|
959 + | active_streams: state.active_streams.clone(),
|
960 + | idle_at: state.idle_at.clone(),
|
961 + | counters,
|
962 + | }
|
963 + | }
|
964 + | }
|
965 + |
|
966 + | impl Drop for DispatchGuard {
|
967 + | fn drop(&mut self) {
|
968 + | self.counters.decr_active();
|
969 + | let prev = self
|
970 + | .active_streams
|
971 + | .fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
|
972 + | if prev == 1 {
|
973 + | if let Ok(mut idle_at) = self.idle_at.lock() {
|
974 + | *idle_at = Instant::now();
|
975 + | }
|
976 + | }
|
977 + | }
|
978 + | }
|
979 + |
|
980 + | /// A checked-out H2 connection ready to dispatch requests.
|
981 + | ///
|
982 + | /// H2 is multiplexed: the underlying `ManagedConnection` stays resident in
|
983 + | /// `Singleton` for the duration of every concurrent request, so "presence
|
984 + | /// in a cache" (the idleness signal we use for H1) doesn't apply.
|
985 + | /// Instead, `SingletonConnection` mints a [`DispatchGuard`] on each
|
986 + | /// `call`; the guard's lifetime tracks one in-flight request and its
|
987 + | /// drop releases the corresponding `active_streams` slot, stamping
|
988 + | /// `idle_at` on the final release. The H2 retain predicate keeps the
|
989 + | /// connection alive while `active_streams > 0` regardless of `idle_at`.
|
990 + | ///
|
991 + | /// Lifecycle:
|
992 + | /// - Constructed in the H2 upgrade `map_response` after `Singleton::call`
|
993 + | /// completes. Snapshots the current [`H2ConnectionState`] and holds it
|
994 + | /// for the checkout's lifetime.
|
995 + | /// - [`Self::call`] starts a [`DispatchGuard`] before delegating to
|
996 + | /// `Singled::call`. The guard is held in `dispatch` for the rest of
|
997 + | /// this `SingletonConnection`'s lifetime.
|
998 + | /// - On drop, the guard's drop releases the active-stream count and may
|
999 + | /// stamp `idle_at`. If `call` was never invoked (e.g. the surrounding
|
1000 + | /// checkout was abandoned by the post-checkout `poll_ready` retry
|
1001 + | /// loop), `dispatch` is `None` and drop is a no-op.
|
1002 + | pub(crate) struct SingletonConnection<T> {
|
1003 + | inner: T,
|
1004 + | /// Snapshot of the H2 side-channel state taken at construction.
|
1005 + | ///
|
1006 + | /// Held for the lifetime of this checkout so dispatch guards always
|
1007 + | /// target the connection this checkout was issued against, even if a
|
1008 + | /// re-handshake replaces the underlying `H2ConnectionRef`'s published
|
1009 + | /// state in the meantime.
|
1010 + | state: Option<H2ConnectionState>,
|
1011 + | /// Active-stream guard. `Some` between [`Self::call`] and drop.
|
1012 + | /// `None` if `call` was never invoked or before `call` runs.
|
1013 + | dispatch: Option<DispatchGuard>,
|
1014 + | /// Pool-level warmth counters for this (partition, authority) cell.
|
1015 + | counters: Arc<super::stats::ConnectionCounters>,
|
1016 + | }
|
1017 + |
|
1018 + | impl<T> SingletonConnection<T> {
|
1019 + | pub(crate) fn new(
|
1020 + | inner: T,
|
1021 + | h2_ref: H2ConnectionRef,
|
1022 + | counters: Arc<super::stats::ConnectionCounters>,
|
1023 + | ) -> Self {
|
1024 + | let state = h2_ref.current();
|
1025 + | Self {
|
1026 + | inner,
|
1027 + | state,
|
1028 + | dispatch: None,
|
1029 + | counters,
|
1030 + | }
|
1031 + | }
|
1032 + |
|
1033 + | /// Metadata for the H2 connection this checkout was issued against.
|
1034 + | ///
|
1035 + | /// Returns `None` only if `SingletonConnection` was constructed before
|
1036 + | /// any handshake had published (which doesn't happen in normal flow,
|
1037 + | /// since `Singleton::call` always completes a handshake before
|
1038 + | /// yielding the `Singled` we wrap).
|
1039 + | pub(crate) fn metadata(&self) -> Option<ConnectionMetadata> {
|
1040 + | self.state.as_ref().map(|s| s.metadata.clone())
|
1041 + | }
|
1042 + | }
|
1043 + |
|
1044 + | impl<T, Req> Service<Req> for SingletonConnection<T>
|
1045 + | where
|
1046 + | T: Service<Req>,
|
1047 + | {
|
1048 + | type Response = T::Response;
|
1049 + | type Error = T::Error;
|
1050 + | type Future = T::Future;
|
1051 + |
|
1052 + | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
1053 + | self.inner.poll_ready(cx)
|
1054 + | }
|
1055 + |
|
1056 + | fn call(&mut self, req: Req) -> Self::Future {
|
1057 + | if let Some(state) = self.state.as_ref() {
|
1058 + | self.dispatch = Some(DispatchGuard::start(state, self.counters.clone()));
|
1059 + | }
|
1060 + | self.inner.call(req)
|
1061 + | }
|
1062 + | }
|
1063 + |
|
1064 + | pin_project! {
|
1065 + | /// A response body that keeps the originating pool checkout alive until
|
1066 + | /// the body is fully consumed.
|
1067 + | ///
|
1068 + | /// # Why this exists
|
1069 + | ///
|
1070 + | /// When a checked-out pool connection's `Service::call` returns, the
|
1071 + | /// response head is available but the body may still be streaming over
|
1072 + | /// the same underlying HTTP connection. For H1 this is critical: if the
|
1073 + | /// `CachedConnection` drops at that point, the connection returns to the
|
1074 + | /// pool mid-body-stream and the next checkout would be handed a still-busy
|
1075 + | /// connection.
|
1076 + | ///
|
1077 + | /// `GuardedBody` holds the checkout (`CachedConnection` for H1,
|
1078 + | /// `SingletonConnection` for H2) in its guard field until the body is
|
1079 + | /// fully dropped. Body streaming continues through the held inner
|
1080 + | /// `Incoming`; when the `GuardedBody` is dropped the guard drops, which
|
1081 + | /// for H1 triggers `CachedConnection::Drop` (return-to-pool or `discard`
|
1082 + | /// if poisoned), and for H2 releases the stream's `DispatchGuard`
|
1083 + | /// (decrementing `active_streams`, and stamping `idle_at` on the final
|
1084 + | /// release).
|
1085 + | ///
|
1086 + | /// The H2 variant carries a generic type parameter because
|
1087 + | /// `SingletonConnection<T>`'s inner `T` is `hyper_util::client::pool::
|
1088 + | /// singleton::Singled<...>`, which is unnameable outside hyper-util.
|
1089 + | /// The H1 variant is fully concrete because `cache::Cached<...>` is
|
1090 + | /// nameable.
|
1091 + | pub(crate) struct GuardedBody<H2Unnameable> {
|
1092 + | #[pin]
|
1093 + | inner: hyper::body::Incoming,
|
1094 + | _guard: ConnectionGuard<H2Unnameable>,
|
1095 + | }
|
1096 + | }
|
1097 + |
|
1098 + | /// What a `GuardedBody` holds alive while the body streams.
|
1099 + | ///
|
1100 + | /// Explicit per-leg variants so H1 vs H2 bifurcation is visible at the
|
1101 + | /// type level and in debugger output.
|
1102 + | pub(crate) enum ConnectionGuard<H2Unnameable> {
|
1103 + | H1(CachedConnection<H1SendRequest>),
|
1104 + | H2(SingletonConnection<H2Unnameable>),
|
1105 + | }
|
1106 + |
|
1107 + | impl<H2Unnameable> GuardedBody<H2Unnameable> {
|
1108 + | pub(crate) fn new(inner: hyper::body::Incoming, guard: ConnectionGuard<H2Unnameable>) -> Self {
|
1109 + | Self {
|
1110 + | inner,
|
1111 + | _guard: guard,
|
1112 + | }
|
1113 + | }
|
1114 + | }
|
1115 + |
|
1116 + | impl<H2Unnameable> hyper::body::Body for GuardedBody<H2Unnameable> {
|
1117 + | type Data = <hyper::body::Incoming as hyper::body::Body>::Data;
|
1118 + | type Error = <hyper::body::Incoming as hyper::body::Body>::Error;
|
1119 + |
|
1120 + | fn poll_frame(
|
1121 + | self: std::pin::Pin<&mut Self>,
|
1122 + | cx: &mut Context<'_>,
|
1123 + | ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
|
1124 + | self.project().inner.poll_frame(cx)
|
1125 + | }
|
1126 + |
|
1127 + | fn is_end_stream(&self) -> bool {
|
1128 + | self.inner.is_end_stream()
|
1129 + | }
|
1130 + |
|
1131 + | fn size_hint(&self) -> hyper::body::SizeHint {
|
1132 + | self.inner.size_hint()
|
1133 + | }
|
1134 + | }
|
1135 + |
|
1136 + | /// The response type both H1 and H2 pool checkouts produce.
|
1137 + | ///
|
1138 + | /// Carries `GuardedBody<PoolUnnameable>` so the H2 leg can hold its
|
1139 + | /// checkout guard (`Singled<…>`, type-erased through `PoolUnnameable`)
|
1140 + | /// for the response body's lifetime. `Negotiate` requires uniform
|
1141 + | /// response types across its legs; this alias is the uniform type that
|
1142 + | /// consumers above the Negotiate composition point work with.
|
1143 + | pub(crate) type CheckoutResponse<PoolUnnameable> = http_1x::Response<GuardedBody<PoolUnnameable>>;
|
1144 + |
|
1145 + | /// Wire-level IO wrapper sitting below TLS in the connector stack. Pure
|
1146 + | /// passthrough; never modifies data or buffers.
|
1147 + | ///
|
1148 + | /// TODO(pool): instrument TCP-connect vs. TLS-handshake timing separately
|
1149 + | /// here (this wrapper is the seam below TLS where the TCP-only duration is
|
1150 + | /// observable); surface the split on `ConnectionTiming`.
|
1151 + | pub(crate) struct TransportIo<IO> {
|
1152 + | inner: IO,
|
1153 + | }
|
1154 + |
|
1155 + | impl<IO> TransportIo<IO> {
|
1156 + | fn new(inner: IO) -> Self {
|
1157 + | Self { inner }
|
1158 + | }
|
1159 + | }
|
1160 + |
|
1161 + | impl<IO: Unpin> Unpin for TransportIo<IO> {}
|
1162 + |
|
1163 + | impl<IO: hyper::rt::Read + Unpin> hyper::rt::Read for TransportIo<IO> {
|
1164 + | fn poll_read(
|
1165 + | mut self: std::pin::Pin<&mut Self>,
|
1166 + | cx: &mut std::task::Context<'_>,
|
1167 + | buf: hyper::rt::ReadBufCursor<'_>,
|
1168 + | ) -> Poll<std::io::Result<()>> {
|
1169 + | std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
|
1170 + | }
|
1171 + | }
|
1172 + |
|
1173 + | impl<IO: hyper::rt::Write + Unpin> hyper::rt::Write for TransportIo<IO> {
|
1174 + | fn poll_write(
|
1175 + | mut self: std::pin::Pin<&mut Self>,
|
1176 + | cx: &mut std::task::Context<'_>,
|
1177 + | buf: &[u8],
|
1178 + | ) -> Poll<std::io::Result<usize>> {
|
1179 + | std::pin::Pin::new(&mut self.inner).poll_write(cx, buf)
|
1180 + | }
|
1181 + |
|
1182 + | fn poll_flush(
|
1183 + | mut self: std::pin::Pin<&mut Self>,
|
1184 + | cx: &mut std::task::Context<'_>,
|
1185 + | ) -> Poll<std::io::Result<()>> {
|
1186 + | std::pin::Pin::new(&mut self.inner).poll_flush(cx)
|
1187 + | }
|
1188 + |
|
1189 + | fn poll_shutdown(
|
1190 + | mut self: std::pin::Pin<&mut Self>,
|
1191 + | cx: &mut std::task::Context<'_>,
|
1192 + | ) -> Poll<std::io::Result<()>> {
|
1193 + | std::pin::Pin::new(&mut self.inner).poll_shutdown(cx)
|
1194 + | }
|
1195 + | }
|
1196 + |
|
1197 + | impl<IO: hyper_util::client::legacy::connect::Connection>
|
1198 + | hyper_util::client::legacy::connect::Connection for TransportIo<IO>
|
1199 + | {
|
1200 + | fn connected(&self) -> hyper_util::client::legacy::connect::Connected {
|
1201 + | self.inner.connected()
|
1202 + | }
|
1203 + | }
|
1204 + |
|
1205 + | /// Wraps a TCP connector with the [`TransportIo`] seam below the TLS layer.
|
1206 + | ///
|
1207 + | /// The wrapper is the point at which transport-level (TCP) timing and byte
|
1208 + | /// accounting can be observed independently of the TLS handshake above it.
|
1209 + | pub(crate) struct TimingConnector<C> {
|
1210 + | inner: C,
|
1211 + | }
|
1212 + |
|
1213 + | impl<C> TimingConnector<C> {
|
1214 + | pub(crate) fn new(inner: C) -> Self {
|
1215 + | Self { inner }
|
1216 + | }
|
1217 + | }
|
1218 + |
|
1219 + | impl<C: Clone> Clone for TimingConnector<C> {
|
1220 + | fn clone(&self) -> Self {
|
1221 + | Self {
|
1222 + | inner: self.inner.clone(),
|
1223 + | }
|
1224 + | }
|
1225 + | }
|
1226 + |
|
1227 + | impl<C, IO> tower::Service<http_1x::Uri> for TimingConnector<C>
|
1228 + | where
|
1229 + | C: tower::Service<http_1x::Uri, Response = IO>,
|
1230 + | C::Error: Into<BoxError>,
|
1231 + | C::Future: Send + 'static,
|
1232 + | IO: Send + 'static,
|
1233 + | {
|
1234 + | type Response = TransportIo<IO>;
|
1235 + | type Error = BoxError;
|
1236 + | type Future = std::pin::Pin<
|
1237 + | Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
|
1238 + | >;
|
1239 + |
|
1240 + | fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
|
1241 + | self.inner.poll_ready(cx).map_err(Into::into)
|
1242 + | }
|
1243 + |
|
1244 + | fn call(&mut self, uri: http_1x::Uri) -> Self::Future {
|
1245 + | let fut = self.inner.call(uri);
|
1246 + | Box::pin(async move {
|
1247 + | let io = fut.await.map_err(Into::into)?;
|
1248 + | Ok(TransportIo::new(io))
|
1249 + | })
|
1250 + | }
|
1251 + | }
|
1252 + |
|
1253 + | #[cfg(test)]
|
1254 + | mod tests {
|
1255 + | //! Unit tests for H2 active-stream tracking.
|
1256 + | //!
|
1257 + | //! The test harness (`ConnectionTestHarness`) is plain HTTP only, without
|
1258 + | //! ALPN, so the full H2 pipeline is not exercised end-to-end here. These
|
1259 + | //! tests verify the atomic transitions directly, which is the
|
1260 + | //! architectural correctness property we need: an in-flight H2 connection
|
1261 + | //! must not be evicted by the retain predicate, and the connection's
|
1262 + | //! `idle_at` must be stamped only on the `active_streams` 1 → 0
|
1263 + | //! transition.
|
1264 + | use super::*;
|
1265 + | use std::future::Future;
|
1266 + | use std::sync::atomic::{AtomicUsize, Ordering};
|
1267 + | use tower::Service as _;
|
1268 + |
|
1269 + | /// A minimal `Service<()>` that returns `Ok(())` (stand-in for
|
1270 + | /// `Singled::call`). We only care about the wrapper's stream-count
|
1271 + | /// side effects.
|
1272 + | #[derive(Clone, Default)]
|
1273 + | struct OkService;
|
1274 + | impl Service<()> for OkService {
|
1275 + | type Response = ();
|
1276 + | type Error = BoxError;
|
1277 + | type Future = std::pin::Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>;
|
1278 + | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
1279 + | Poll::Ready(Ok(()))
|
1280 + | }
|
1281 + | fn call(&mut self, _: ()) -> Self::Future {
|
1282 + | Box::pin(async { Ok(()) })
|
1283 + | }
|
1284 + | }
|
1285 + |
|
1286 + | fn publish_state(h2_ref: &H2ConnectionRef) -> (Arc<AtomicUsize>, Arc<Mutex<Instant>>) {
|
1287 + | use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
|
1288 + | let active = Arc::new(AtomicUsize::new(0));
|
1289 + | let idle_at = Arc::new(Mutex::new(Instant::now()));
|
1290 + | h2_ref.publish(H2ConnectionState {
|
1291 + | metadata: ConnectionMetadata::builder()
|
1292 + | .proxied(false)
|
1293 + | .poison_fn(|| {})
|
1294 + | .build(),
|
1295 + | active_streams: active.clone(),
|
1296 + | idle_at: idle_at.clone(),
|
1297 + | });
|
1298 + | (active, idle_at)
|
1299 + | }
|
1300 + |
|
1301 + | /// A round-trip through `SingletonConnection::call` mints a
|
1302 + | /// `DispatchGuard` that releases on drop. Counter goes 0 → 1 → 0.
|
1303 + | #[tokio::test]
|
1304 + | async fn singleton_dispatch_round_trip_is_net_zero() {
|
1305 + | let h2_ref = H2ConnectionRef::new();
|
1306 + | let (active, _) = publish_state(&h2_ref);
|
1307 + | assert_eq!(active.load(Ordering::Acquire), 0);
|
1308 + |
|
1309 + | let counters = Arc::new(super::super::stats::ConnectionCounters::default());
|
1310 + | let mut sc = SingletonConnection::new(OkService, h2_ref, counters);
|
1311 + | let _ = sc.call(()).await;
|
1312 + | assert_eq!(
|
1313 + | active.load(Ordering::Acquire),
|
1314 + | 1,
|
1315 + | "call should mint a DispatchGuard, incrementing active_streams"
|
1316 + | );
|
1317 + | drop(sc);
|
1318 + | assert_eq!(
|
1319 + | active.load(Ordering::Acquire),
|
1320 + | 0,
|
1321 + | "DispatchGuard's Drop should release the active_streams slot"
|
1322 + | );
|
1323 + | }
|
1324 + |
|
1325 + | /// Constructing without dispatching leaves no `DispatchGuard`, so
|
1326 + | /// dropping is a no-op. The post-checkout `poll_ready` retry loop
|
1327 + | /// relies on this: a checkout discarded before `call` must not
|
1328 + | /// underflow the counter.
|
1329 + | #[tokio::test]
|
1330 + | async fn singleton_drop_without_dispatch_is_noop() {
|
1331 + | let h2_ref = H2ConnectionRef::new();
|
1332 + | let (active, _) = publish_state(&h2_ref);
|
1333 + | active.store(5, Ordering::Release);
|
1334 + |
|
1335 + | let counters = Arc::new(super::super::stats::ConnectionCounters::default());
|
1336 + | let sc = SingletonConnection::new(OkService, h2_ref, counters);
|
1337 + | drop(sc);
|
1338 + | assert_eq!(
|
1339 + | active.load(Ordering::Acquire),
|
1340 + | 5,
|
1341 + | "uncalled SingletonConnection must not release a DispatchGuard"
|
1342 + | );
|
1343 + | }
|
1344 + |
|
1345 + | /// Concurrent dispatches against the same H2 connection (simulating
|
1346 + | /// multiplexed requests) all land on the same counter. `idle_at` is
|
1347 + | /// stamped only on the LAST `DispatchGuard` drop (the 1 → 0
|
1348 + | /// transition).
|
1349 + | #[tokio::test]
|
1350 + | async fn idle_at_stamped_only_on_last_dispatch_release() {
|
1351 + | let h2_ref = H2ConnectionRef::new();
|
1352 + | let (active, idle_at) = publish_state(&h2_ref);
|
1353 + |
|
1354 + | // Capture the stamp from construction to detect updates.
|
1355 + | let original_idle = *idle_at.lock().unwrap();
|
1356 + |
|
1357 + | let mut a = SingletonConnection::new(
|
1358 + | OkService,
|
1359 + | h2_ref.clone(),
|
1360 + | Arc::new(super::super::stats::ConnectionCounters::default()),
|
1361 + | );
|
1362 + | let mut b = SingletonConnection::new(
|
1363 + | OkService,
|
1364 + | h2_ref.clone(),
|
1365 + | Arc::new(super::super::stats::ConnectionCounters::default()),
|
1366 + | );
|
1367 + | let mut c = SingletonConnection::new(
|
1368 + | OkService,
|
1369 + | h2_ref,
|
1370 + | Arc::new(super::super::stats::ConnectionCounters::default()),
|
1371 + | );
|
1372 + |
|
1373 + | let _ = a.call(()).await;
|
1374 + | let _ = b.call(()).await;
|
1375 + | let _ = c.call(()).await;
|
1376 + | assert_eq!(active.load(Ordering::Acquire), 3);
|
1377 + |
|
1378 + | // Sleep so any stamp is distinguishable from construction time.
|
1379 + | tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
1380 + |
|
1381 + | // Release two; idle_at must NOT yet be stamped (counter > 0).
|
1382 + | drop(a);
|
1383 + | drop(b);
|
1384 + | assert_eq!(active.load(Ordering::Acquire), 1);
|
1385 + | assert_eq!(
|
1386 + | *idle_at.lock().unwrap(),
|
1387 + | original_idle,
|
1388 + | "idle_at must not be stamped while dispatches are in flight"
|
1389 + | );
|
1390 + |
|
1391 + | // Last release: 1 → 0 transition. idle_at must be stamped.
|
1392 + | drop(c);
|
1393 + | assert_eq!(active.load(Ordering::Acquire), 0);
|
1394 + | let final_idle = *idle_at.lock().unwrap();
|
1395 + | assert!(
|
1396 + | final_idle > original_idle,
|
1397 + | "idle_at should be stamped when the last DispatchGuard releases"
|
1398 + | );
|
1399 + | }
|
1400 + | }
|