1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! Tower service adapters for hyper's HTTP protocol handshake.
|
7 + | //!
|
8 + | //! - `ConnectionLimit`: wraps a connector, acquires semaphore permits before
|
9 + | //! connecting, and returns an `EstablishedConnection`.
|
10 + | //! - `H1ConnectAndHandshake` / `H2ConnectAndHandshake`: perform the protocol
|
11 + | //! handshake on an already-connected IO stream, producing a `ManagedConnection`.
|
12 + |
|
13 + | use std::future::Future;
|
14 + | use std::pin::Pin;
|
15 + | use std::sync::Arc;
|
16 + | use std::task::{Context, Poll};
|
17 + | use std::time::Instant;
|
18 + |
|
19 + | use aws_smithy_runtime_api::box_error::BoxError;
|
20 + | use aws_smithy_runtime_api::client::connection::ConnectionId;
|
21 + | use aws_smithy_types::body::SdkBody;
|
22 + | use hyper_util::client::legacy::connect::{Connection, HttpInfo};
|
23 + | use hyper_util::rt::TokioExecutor;
|
24 + | use tokio::sync::Semaphore;
|
25 + | use tower::Service;
|
26 + |
|
27 + | use super::connection::{
|
28 + | Authority, ConnectCtx, ConnectionCreatedEvent, ConnectionFailedEvent, ConnectionInfo,
|
29 + | ConnectionPermit, ConnectionTiming, EstablishedConnection, ManagedConnection,
|
30 + | NegotiatedProtocol,
|
31 + | };
|
32 + | use super::partition::DriverSpawner;
|
33 + |
|
34 + | /// Pool-scoped instrumentation primitives shared across layers in the
|
35 + | /// pool stack. Held by `ConnectionPool` for the pool's lifetime and
|
36 + | /// cloned into individual layers (the H1/H2 handshake services) at
|
37 + | /// construction.
|
38 + | ///
|
39 + | /// Cheap to clone (one `Arc` per primitive).
|
40 + | #[derive(Clone)]
|
41 + | pub(crate) struct PoolHooks {
|
42 + | conn_id_counter: Arc<std::sync::atomic::AtomicU64>,
|
43 + | pub(crate) listener: Option<Arc<dyn super::connection::ConnectionEventListener>>,
|
44 + | }
|
45 + |
|
46 + | impl PoolHooks {
|
47 + | pub(crate) fn new(
|
48 + | listener: Option<Arc<dyn super::connection::ConnectionEventListener>>,
|
49 + | ) -> Self {
|
50 + | Self {
|
51 + | conn_id_counter: Arc::new(std::sync::atomic::AtomicU64::new(0)),
|
52 + | listener,
|
53 + | }
|
54 + | }
|
55 + |
|
56 + | /// Mint the next connection id. Stable for the connection's lifetime;
|
57 + | /// the underlying counter wraps at `u64::MAX`.
|
58 + | pub(crate) fn next_conn_id(&self) -> ConnectionId {
|
59 + | ConnectionId::new(
|
60 + | self.conn_id_counter
|
61 + | .fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
62 + | )
|
63 + | }
|
64 + |
|
65 + | /// Fire the listener's connection-created callback, if a listener is set.
|
66 + | pub(crate) fn on_created(&self, event: &ConnectionCreatedEvent) {
|
67 + | if let Some(ref l) = self.listener {
|
68 + | l.on_created(event);
|
69 + | }
|
70 + | }
|
71 + |
|
72 + | /// Fire the listener's connection-failed callback, if a listener is set.
|
73 + | pub(crate) fn on_connection_failed(&self, event: &ConnectionFailedEvent) {
|
74 + | if let Some(ref l) = self.listener {
|
75 + | l.on_connection_failed(event);
|
76 + | }
|
77 + | }
|
78 + |
|
79 + | /// Fire the listener's connection-reused callback, if a listener is set.
|
80 + | pub(crate) fn on_reused(&self, event: &super::connection::ConnectionReusedEvent) {
|
81 + | if let Some(ref l) = self.listener {
|
82 + | l.on_reused(event);
|
83 + | }
|
84 + | }
|
85 + |
|
86 + | /// Fire the listener's connection-closed callback, if a listener is set.
|
87 + | pub(crate) fn on_closed(&self, event: &super::connection::ConnectionClosedEvent) {
|
88 + | if let Some(ref l) = self.listener {
|
89 + | l.on_closed(event);
|
90 + | }
|
91 + | }
|
92 + | }
|
93 + |
|
94 + | /// Wraps a connector service, acquiring semaphore permits before connecting.
|
95 + | ///
|
96 + | /// Returns an [`EstablishedConnection`] so the permit can be stored on
|
97 + | /// `ManagedConnection` and held for the connection's lifetime.
|
98 + | ///
|
99 + | /// Target type is `ConnectCtx`: the inner TCP connector is `Service<Uri>`,
|
100 + | /// so this layer extracts `ctx.uri` to pass through. Per-operation timeouts
|
101 + | /// on the context are applied here: `connect_timeout` wraps the inner
|
102 + | /// connector call (which includes TCP + TLS since the inner is the
|
103 + | /// TLS-wrapped connector). Cache hits skip this layer entirely, so
|
104 + | /// `connect_timeout` is automatically new-connection-only.
|
105 + | pub(crate) struct ConnectionLimit<C> {
|
106 + | inner: C,
|
107 + | global: Option<Arc<Semaphore>>,
|
108 + | per_host: Option<Arc<Semaphore>>,
|
109 + | counters: Arc<super::stats::ConnectionCounters>,
|
110 + | /// Cross-partition active-reclaim handle. `None` when no reclaim peer
|
111 + | /// exists or the pool is mid-teardown; the cap path is then a plain
|
112 + | /// blocking acquire.
|
113 + | reclaim: Option<super::PeerReclaimHandle>,
|
114 + | }
|
115 + |
|
116 + | impl<C> ConnectionLimit<C> {
|
117 + | pub(crate) fn new(
|
118 + | inner: C,
|
119 + | global: Option<Arc<Semaphore>>,
|
120 + | per_host: Option<Arc<Semaphore>>,
|
121 + | counters: Arc<super::stats::ConnectionCounters>,
|
122 + | reclaim: Option<super::PeerReclaimHandle>,
|
123 + | ) -> Self {
|
124 + | Self {
|
125 + | inner,
|
126 + | global,
|
127 + | per_host,
|
128 + | counters,
|
129 + | reclaim,
|
130 + | }
|
131 + | }
|
132 + | }
|
133 + |
|
134 + | impl<C: Clone> Clone for ConnectionLimit<C> {
|
135 + | fn clone(&self) -> Self {
|
136 + | Self {
|
137 + | inner: self.inner.clone(),
|
138 + | global: self.global.clone(),
|
139 + | per_host: self.per_host.clone(),
|
140 + | counters: self.counters.clone(),
|
141 + | reclaim: self.reclaim.clone(),
|
142 + | }
|
143 + | }
|
144 + | }
|
145 + |
|
146 + | impl<C, IO> Service<ConnectCtx> for ConnectionLimit<C>
|
147 + | where
|
148 + | C: Service<http_1x::Uri, Response = IO> + Clone + Send + 'static,
|
149 + | C::Error: Into<BoxError> + 'static,
|
150 + | C::Future: Send + 'static,
|
151 + | IO: Send + 'static,
|
152 + | {
|
153 + | type Response = EstablishedConnection<IO>;
|
154 + | type Error = BoxError;
|
155 + | type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
156 + |
|
157 + | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
158 + | self.inner.poll_ready(cx).map_err(Into::into)
|
159 + | }
|
160 + |
|
161 + | fn call(&mut self, ctx: ConnectCtx) -> Self::Future {
|
162 + | let mut inner = self.inner.clone();
|
163 + | let global = self.global.clone();
|
164 + | let per_host = self.per_host.clone();
|
165 + | let counters = self.counters.clone();
|
166 + | let reclaim = self.reclaim.clone();
|
167 + | Box::pin(async move {
|
168 + | let mode = ctx.mode;
|
169 + | // Per-host before global: never hold a global permit while
|
170 + | // waiting on a per-host permit.
|
171 + | let per_host_permit = match &per_host {
|
172 + | Some(sem) => Some(
|
173 + | acquire_or_reclaim(sem, reclaim.as_ref(), mode, || {
|
174 + | // `send_request` validated the URI has scheme+authority
|
175 + | // before dispatching, so this cannot fail here.
|
176 + | let key = super::PoolKey::from_uri(&ctx.uri)
|
177 + | .expect("connect URI has scheme+authority");
|
178 + | super::BindingConstraint::PerHost(key)
|
179 + | })
|
180 + | .await?,
|
181 + | ),
|
182 + | None => None,
|
183 + | };
|
184 + | let global_permit = match &global {
|
185 + | Some(sem) => Some(
|
186 + | acquire_or_reclaim(sem, reclaim.as_ref(), mode, || {
|
187 + | super::BindingConstraint::Global
|
188 + | })
|
189 + | .await?,
|
190 + | ),
|
191 + | None => None,
|
192 + | };
|
193 + | let permit = Arc::new(ConnectionPermit::new(global_permit, per_host_permit));
|
194 + | let establishing = super::stats::EstablishingGuard::new(counters);
|
195 + |
|
196 + | std::future::poll_fn(|cx| inner.poll_ready(cx))
|
197 + | .await
|
198 + | .map_err(Into::into)?;
|
199 + |
|
200 + | // Apply connect_timeout only around the actual connector call
|
201 + | // (TCP + TLS). If `ctx.connect_timeout` is `None`, this is a
|
202 + | // plain `inner.call(uri).await`.
|
203 + | let uri = ctx.uri;
|
204 + | let connect_fut = inner.call(uri);
|
205 + | let io = super::super::timeout::maybe_timeout_future(
|
206 + | connect_fut,
|
207 + | ctx.connect_timeout.as_ref().map(|t| t.duration),
|
208 + | ctx.connect_timeout.as_ref().map(|t| &t.sleep_impl),
|
209 + | super::super::timeout::TimeoutKind::Connect,
|
210 + | )
|
211 + | .await?;
|
212 + | Ok(EstablishedConnection {
|
213 + | io,
|
214 + | permit,
|
215 + | establishing,
|
216 + | })
|
217 + | })
|
218 + | }
|
219 + | }
|
220 + |
|
221 + | /// Acquire one owned permit. Fast path is `try_acquire_owned`. On
|
222 + | /// `NoPermits`: under [`AcquireMode::NonBlocking`] return [`CapBound`]
|
223 + | /// immediately (the caller will try a peer borrow); under
|
224 + | /// [`AcquireMode::Blocking`] free one peer's idle connection for
|
225 + | /// `constraint` (inline, best-effort) then blocking-acquire — the blocking
|
226 + | /// acquire is the authoritative take regardless of whether reclaim freed a
|
227 + | /// permit. `constraint` is built only on the blocking cap-bound branch.
|
228 + | async fn acquire_or_reclaim(
|
229 + | sem: &Arc<Semaphore>,
|
230 + | reclaim: Option<&super::PeerReclaimHandle>,
|
231 + | mode: super::connection::AcquireMode,
|
232 + | constraint: impl FnOnce() -> super::BindingConstraint,
|
233 + | ) -> Result<tokio::sync::OwnedSemaphorePermit, BoxError> {
|
234 + | match sem.clone().try_acquire_owned() {
|
235 + | Ok(permit) => Ok(permit),
|
236 + | Err(tokio::sync::TryAcquireError::NoPermits) => {
|
237 + | if mode == super::connection::AcquireMode::NonBlocking {
|
238 + | return Err(super::connection::CapBound.into());
|
239 + | }
|
240 + | if let Some(reclaim) = reclaim {
|
241 + | reclaim.try_free_under_load(&constraint());
|
242 + | }
|
243 + | sem.clone()
|
244 + | .acquire_owned()
|
245 + | .await
|
246 + | .map_err(|_| "pool closed".into())
|
247 + | }
|
248 + | Err(tokio::sync::TryAcquireError::Closed) => Err("pool closed".into()),
|
249 + | }
|
250 + | }
|
251 + |
|
252 + | // ---------------------------------------------------------------------------
|
253 + | // Tower Service wrappers for hyper's SendRequest
|
254 + | // ---------------------------------------------------------------------------
|
255 + |
|
256 + | /// Tower `Service` adapter for `hyper::client::conn::http1::SendRequest`.
|
257 + | pub(crate) struct H1SendRequest {
|
258 + | inner: hyper::client::conn::http1::SendRequest<SdkBody>,
|
259 + | }
|
260 + |
|
261 + | impl H1SendRequest {
|
262 + | pub(crate) fn new(inner: hyper::client::conn::http1::SendRequest<SdkBody>) -> Self {
|
263 + | Self { inner }
|
264 + | }
|
265 + | }
|
266 + |
|
267 + | impl Service<http_1x::Request<SdkBody>> for H1SendRequest {
|
268 + | type Response = http_1x::Response<hyper::body::Incoming>;
|
269 + | type Error = hyper::Error;
|
270 + | type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
271 + |
|
272 + | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
273 + | self.inner.poll_ready(cx)
|
274 + | }
|
275 + |
|
276 + | fn call(&mut self, req: http_1x::Request<SdkBody>) -> Self::Future {
|
277 + | Box::pin(self.inner.send_request(req))
|
278 + | }
|
279 + | }
|
280 + |
|
281 + | /// Tower `Service` adapter for `hyper::client::conn::http2::SendRequest`.
|
282 + | ///
|
283 + | /// Clone is supported because HTTP/2 multiplexes requests over a single connection.
|
284 + | #[derive(Clone)]
|
285 + | pub(crate) struct H2SendRequest {
|
286 + | inner: hyper::client::conn::http2::SendRequest<SdkBody>,
|
287 + | }
|
288 + |
|
289 + | impl H2SendRequest {
|
290 + | pub(crate) fn new(inner: hyper::client::conn::http2::SendRequest<SdkBody>) -> Self {
|
291 + | Self { inner }
|
292 + | }
|
293 + | }
|
294 + |
|
295 + | impl Service<http_1x::Request<SdkBody>> for H2SendRequest {
|
296 + | type Response = http_1x::Response<hyper::body::Incoming>;
|
297 + | type Error = hyper::Error;
|
298 + | type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
299 + |
|
300 + | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
301 + | self.inner.poll_ready(cx)
|
302 + | }
|
303 + |
|
304 + | fn call(&mut self, req: http_1x::Request<SdkBody>) -> Self::Future {
|
305 + | Box::pin(self.inner.send_request(req))
|
306 + | }
|
307 + | }
|
308 + |
|
309 + | // ---------------------------------------------------------------------------
|
310 + | // Connect-and-handshake services
|
311 + | // ---------------------------------------------------------------------------
|
312 + |
|
313 + | /// Extract `ConnectionInfo` from a just-connected IO stream.
|
314 + | fn capture_info<IO: Connection>(io: &IO, authority: Authority) -> ConnectionInfo {
|
315 + | let connected = io.connected();
|
316 + | let is_proxied = connected.is_proxied();
|
317 + | let mut extras = http_1x::Extensions::new();
|
318 + | connected.get_extras(&mut extras);
|
319 + | let http_info = extras.get::<HttpInfo>();
|
320 + | ConnectionInfo {
|
321 + | remote_addr: http_info.map(|i| i.remote_addr()),
|
322 + | local_addr: http_info.map(|i| i.local_addr()),
|
323 + | is_proxied,
|
324 + | authority,
|
325 + | }
|
326 + | }
|
327 + |
|
328 + | /// Connects and performs an HTTP/1.1 handshake, spawning the connection
|
329 + | /// driver onto the partition's runtime via the captured [`DriverSpawner`].
|
330 + | ///
|
331 + | /// The connector is expected to return an [`EstablishedConnection`], typically
|
332 + | /// produced by [`ConnectionLimit`] wrapping a TCP/TLS connector.
|
333 + | pub(crate) struct H1ConnectAndHandshake<C> {
|
334 + | connector: C,
|
335 + | hooks: PoolHooks,
|
336 + | spawner: Arc<dyn DriverSpawner>,
|
337 + | }
|
338 + |
|
339 + | impl<C> H1ConnectAndHandshake<C> {
|
340 + | pub(crate) fn new(connector: C, hooks: PoolHooks, spawner: Arc<dyn DriverSpawner>) -> Self {
|
341 + | Self {
|
342 + | connector,
|
343 + | hooks,
|
344 + | spawner,
|
345 + | }
|
346 + | }
|
347 + | }
|
348 + |
|
349 + | impl<C: Clone> Clone for H1ConnectAndHandshake<C> {
|
350 + | fn clone(&self) -> Self {
|
351 + | Self {
|
352 + | connector: self.connector.clone(),
|
353 + | hooks: self.hooks.clone(),
|
354 + | spawner: self.spawner.clone(),
|
355 + | }
|
356 + | }
|
357 + | }
|
358 + |
|
359 + | impl<C, IO> Service<ConnectCtx> for H1ConnectAndHandshake<C>
|
360 + | where
|
361 + | C: Service<ConnectCtx, Response = EstablishedConnection<IO>>,
|
362 + | C::Error: Into<BoxError> + 'static,
|
363 + | C::Future: Send + 'static,
|
364 + | IO: hyper::rt::Read + hyper::rt::Write + Connection + Unpin + Send + 'static,
|
365 + | {
|
366 + | type Response = ManagedConnection<H1SendRequest>;
|
367 + | type Error = BoxError;
|
368 + | type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
369 + |
|
370 + | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
371 + | self.connector.poll_ready(cx).map_err(Into::into)
|
372 + | }
|
373 + |
|
374 + | fn call(&mut self, ctx: ConnectCtx) -> Self::Future {
|
375 + | let hooks = self.hooks.clone();
|
376 + | let spawner = self.spawner.clone();
|
377 + | let authority = Authority::new(
|
378 + | ctx.uri
|
379 + | .authority()
|
380 + | .expect("request URI has authority")
|
381 + | .as_str(),
|
382 + | );
|
383 + | let fut = self.connector.call(ctx);
|
384 + | Box::pin(async move {
|
385 + | let connect_start = Instant::now();
|
386 + | let EstablishedConnection {
|
387 + | io,
|
388 + | permit,
|
389 + | establishing,
|
390 + | } = match fut.await.map_err(Into::into) {
|
391 + | Ok(v) => v,
|
392 + | Err(e) => {
|
393 + | hooks.on_connection_failed(&ConnectionFailedEvent::new(
|
394 + | authority,
|
395 + | None,
|
396 + | Into::into(e.to_string()),
|
397 + | ));
|
398 + | return Err(e);
|
399 + | }
|
400 + | };
|
401 + | let connect_duration = connect_start.elapsed();
|
402 + | let info = capture_info(&io, authority.clone());
|
403 + | let conn_id = hooks.next_conn_id();
|
404 + |
|
405 + | let (tx, conn) = match hyper::client::conn::http1::Builder::new()
|
406 + | .handshake(io)
|
407 + | .await
|
408 + | {
|
409 + | Ok(v) => v,
|
410 + | Err(e) => {
|
411 + | let boxed: BoxError = Box::new(e);
|
412 + | hooks.on_connection_failed(&ConnectionFailedEvent::new(
|
413 + | authority,
|
414 + | info.remote_addr,
|
415 + | Into::into(boxed.to_string()),
|
416 + | ));
|
417 + | return Err(boxed);
|
418 + | }
|
419 + | };
|
420 + |
|
421 + | tracing::debug!(
|
422 + | conn_id = %conn_id,
|
423 + | protocol = "h1",
|
424 + | remote = ?info.remote_addr,
|
425 + | local = ?info.local_addr,
|
426 + | "pool: connection established"
|
427 + | );
|
428 + |
|
429 + | hooks.on_created(&ConnectionCreatedEvent::new(
|
430 + | conn_id,
|
431 + | authority,
|
432 + | info.remote_addr,
|
433 + | NegotiatedProtocol::Http1,
|
434 + | ConnectionTiming::new(connect_duration),
|
435 + | ));
|
436 + |
|
437 + | let established = establishing.promote(super::stats::PROTO_H1);
|
438 + |
|
439 + | spawner.spawn(Box::pin({
|
440 + | let remote_addr = info.remote_addr;
|
441 + | let local_addr = info.local_addr;
|
442 + | async move {
|
443 + | if let Err(e) = conn.with_upgrades().await {
|
444 + | tracing::debug!(
|
445 + | conn_id = %conn_id,
|
446 + | protocol = "h1",
|
447 + | ?remote_addr,
|
448 + | ?local_addr,
|
449 + | error = %e,
|
450 + | "pool: connection driver error"
|
451 + | );
|
452 + | }
|
453 + | }
|
454 + | }));
|
455 + |
|
456 + | Ok(ManagedConnection::new(
|
457 + | H1SendRequest::new(tx),
|
458 + | info,
|
459 + | conn_id,
|
460 + | permit,
|
461 + | established,
|
462 + | ))
|
463 + | })
|
464 + | }
|
465 + | }
|
466 + |
|
467 + | /// Connects and performs an HTTP/2 handshake, spawning the connection
|
468 + | /// driver onto the partition's runtime via the captured [`DriverSpawner`].
|
469 + | ///
|
470 + | /// Pinned to `Service<()>` because this service sits in the Negotiate
|
471 + | /// upgrade path: the connection is already established via the shared
|
472 + | /// `Inspected` slot.
|
473 + | pub(crate) struct H2ConnectAndHandshake<C> {
|
474 + | connector: C,
|
475 + | h2_ref: super::connection::H2ConnectionRef,
|
476 + | hooks: PoolHooks,
|
477 + | authority: Authority,
|
478 + | spawner: Arc<dyn DriverSpawner>,
|
479 + | }
|
480 + |
|
481 + | impl<C> H2ConnectAndHandshake<C> {
|
482 + | /// Create an H2 handshake service that publishes each newly established
|
483 + | /// connection's state — `ConnectionMetadata` plus the shared
|
484 + | /// `active_streams` counter and `idle_at` timestamp — into `h2_ref`. The
|
485 + | /// same ref is held on the read side by `SingletonConnection` (clones of
|
486 + | /// the ref share the underlying slot), so the H2 checkout path can expose
|
487 + | /// connection metadata and poison support to the adapter layer, and track
|
488 + | /// stream occupancy, even though `Singled<…>` itself is opaque.
|
489 + | pub(crate) fn new(
|
490 + | connector: C,
|
491 + | h2_ref: super::connection::H2ConnectionRef,
|
492 + | hooks: PoolHooks,
|
493 + | authority: Authority,
|
494 + | spawner: Arc<dyn DriverSpawner>,
|
495 + | ) -> Self {
|
496 + | Self {
|
497 + | connector,
|
498 + | h2_ref,
|
499 + | hooks,
|
500 + | authority,
|
501 + | spawner,
|
502 + | }
|
503 + | }
|
504 + | }
|
505 + |
|
506 + | impl<C: Clone> Clone for H2ConnectAndHandshake<C> {
|
507 + | fn clone(&self) -> Self {
|
508 + | Self {
|
509 + | connector: self.connector.clone(),
|
510 + | h2_ref: self.h2_ref.clone(),
|
511 + | hooks: self.hooks.clone(),
|
512 + | authority: self.authority.clone(),
|
513 + | spawner: self.spawner.clone(),
|
514 + | }
|
515 + | }
|
516 + | }
|
517 + |
|
518 + | impl<C, IO> Service<()> for H2ConnectAndHandshake<C>
|
519 + | where
|
520 + | C: Service<(), Response = EstablishedConnection<IO>>,
|
521 + | C::Error: Into<BoxError> + 'static,
|
522 + | C::Future: Send + 'static,
|
523 + | IO: hyper::rt::Read + hyper::rt::Write + Connection + Unpin + Send + 'static,
|
524 + | {
|
525 + | type Response = ManagedConnection<H2SendRequest>;
|
526 + | type Error = BoxError;
|
527 + | type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
528 + |
|
529 + | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
530 + | self.connector.poll_ready(cx).map_err(Into::into)
|
531 + | }
|
532 + |
|
533 + | fn call(&mut self, _req: ()) -> Self::Future {
|
534 + | let fut = self.connector.call(());
|
535 + | let h2_ref = self.h2_ref.clone();
|
536 + | let hooks = self.hooks.clone();
|
537 + | let authority = self.authority.clone();
|
538 + | let spawner = self.spawner.clone();
|
539 + | Box::pin(async move {
|
540 + | let connect_start = Instant::now();
|
541 + | let EstablishedConnection {
|
542 + | io,
|
543 + | permit,
|
544 + | establishing,
|
545 + | } = match fut.await.map_err(Into::into) {
|
546 + | Ok(v) => v,
|
547 + | Err(e) => {
|
548 + | hooks.on_connection_failed(&ConnectionFailedEvent::new(
|
549 + | authority,
|
550 + | None,
|
551 + | Into::into(e.to_string()),
|
552 + | ));
|
553 + | return Err(e);
|
554 + | }
|
555 + | };
|
556 + | let connect_duration = connect_start.elapsed();
|
557 + | let info = capture_info(&io, authority.clone());
|
558 + | let conn_id = hooks.next_conn_id();
|
559 + |
|
560 + | let (tx, conn) = match hyper::client::conn::http2::Builder::new(TokioExecutor::new())
|
561 + | .handshake(io)
|
562 + | .await
|
563 + | {
|
564 + | Ok(v) => v,
|
565 + | Err(e) => {
|
566 + | let boxed: BoxError = Box::new(e);
|
567 + | hooks.on_connection_failed(&ConnectionFailedEvent::new(
|
568 + | authority,
|
569 + | info.remote_addr,
|
570 + | Into::into(boxed.to_string()),
|
571 + | ));
|
572 + | return Err(boxed);
|
573 + | }
|
574 + | };
|
575 + |
|
576 + | tracing::debug!(
|
577 + | conn_id = %conn_id,
|
578 + | protocol = "h2",
|
579 + | remote = ?info.remote_addr,
|
580 + | local = ?info.local_addr,
|
581 + | "pool: connection established"
|
582 + | );
|
583 + |
|
584 + | hooks.on_created(&ConnectionCreatedEvent::new(
|
585 + | conn_id,
|
586 + | authority,
|
587 + | info.remote_addr,
|
588 + | NegotiatedProtocol::Http2,
|
589 + | ConnectionTiming::new(connect_duration),
|
590 + | ));
|
591 + |
|
592 + | let established = establishing.promote(super::stats::PROTO_H2);
|
593 + |
|
594 + | spawner.spawn(Box::pin({
|
595 + | let remote_addr = info.remote_addr;
|
596 + | let local_addr = info.local_addr;
|
597 + | async move {
|
598 + | if let Err(e) = conn.await {
|
599 + | tracing::debug!(
|
600 + | conn_id = %conn_id,
|
601 + | protocol = "h2",
|
602 + | ?remote_addr,
|
603 + | ?local_addr,
|
604 + | error = %e,
|
605 + | "pool: connection driver error"
|
606 + | );
|
607 + | }
|
608 + | }
|
609 + | }));
|
610 + |
|
611 + | let managed =
|
612 + | ManagedConnection::new(H2SendRequest::new(tx), info, conn_id, permit, established);
|
613 + | // Publish this connection's state (metadata + active_streams +
|
614 + | // idle_at refs) for the checkout side. Singleton replaces its
|
615 + | // stored connection wholesale on each new handshake, so
|
616 + | // last-writer-wins on the ref is correct.
|
617 + | h2_ref.publish(super::connection::H2ConnectionState {
|
618 + | metadata: managed.metadata(),
|
619 + | active_streams: managed.active_streams_ref(),
|
620 + | idle_at: managed.idle_at_ref(),
|
621 + | });
|
622 + | Ok(managed)
|
623 + | })
|
624 + | }
|
625 + | }
|
626 + |
|
627 + | #[cfg(test)]
|
628 + | mod tests {
|
629 + | use super::*;
|
630 + | use crate::client::timeout::test::NeverConnects;
|
631 + | use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep};
|
632 + | use std::time::Duration;
|
633 + | use tower::Service as _;
|
634 + |
|
635 + | /// Verify `ConnectionLimit` applies `connect_timeout` from `ConnectCtx`
|
636 + | /// to the inner TCP connector. `NeverConnects` returns a connector
|
637 + | /// future that never resolves; a short connect_timeout should fire
|
638 + | /// and produce an `HTTP connect timeout occurred after …` error.
|
639 + | #[tokio::test(start_paused = true)]
|
640 + | async fn connect_timeout_fires_on_slow_connector() {
|
641 + | let mut svc = ConnectionLimit::new(
|
642 + | NeverConnects::default(),
|
643 + | None,
|
644 + | None,
|
645 + | Arc::new(super::super::stats::ConnectionCounters::default()),
|
646 + | None,
|
647 + | );
|
648 + | let sleep = SharedAsyncSleep::new(TokioSleep::new());
|
649 + | let ctx = ConnectCtx::new(
|
650 + | "http://example.com".parse().unwrap(),
|
651 + | Some(super::super::connection::TimeoutContext::new(
|
652 + | Duration::from_millis(500),
|
653 + | sleep,
|
654 + | )),
|
655 + | );
|
656 + | let err = match svc.call(ctx).await {
|
657 + | Ok(_) => panic!("connect timeout should fire against a never-resolving connector"),
|
658 + | Err(err) => err,
|
659 + | };
|
660 + | let msg = format!("{err}");
|
661 + | assert!(
|
662 + | msg.contains("HTTP connect"),
|
663 + | "expected `HTTP connect` in error, got: {msg}"
|
664 + | );
|
665 + | assert!(
|
666 + | msg.contains("500ms"),
|
667 + | "expected `500ms` in error, got: {msg}"
|
668 + | );
|
669 + | }
|
670 + |
|
671 + | /// Without a connect_timeout set, `ConnectionLimit` passes through the
|
672 + | /// connector call with no wrapping. Verify the absence of a timeout
|
673 + | /// means the slow connector stays pending (we just check we can start
|
674 + | /// the call; with `start_paused`, nothing advances so the future is
|
675 + | /// still pending after a short yield).
|
676 + | #[tokio::test(start_paused = true)]
|
677 + | async fn no_timeout_does_not_bound_connector() {
|
678 + | let mut svc = ConnectionLimit::new(
|
679 + | NeverConnects::default(),
|
680 + | None,
|
681 + | None,
|
682 + | Arc::new(super::super::stats::ConnectionCounters::default()),
|
683 + | None,
|
684 + | );
|
685 + | let ctx = ConnectCtx::new("http://example.com".parse().unwrap(), None);
|
686 + | let fut = svc.call(ctx);
|
687 + | // A brief tokio yield shouldn't resolve the never-connects future.
|
688 + | tokio::pin!(fut);
|
689 + | tokio::select! {
|
690 + | _ = &mut fut => panic!("future should not resolve without a timeout"),
|
691 + | _ = tokio::task::yield_now() => {}
|
692 + | }
|
693 + | }
|
694 + | }
|