1 1 | /*
|
2 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 3 | * SPDX-License-Identifier: Apache-2.0
|
4 4 | */
|
5 5 |
|
6 - | //! Transport construction below HTTP protocol establishment.
|
6 + | //! Transport connection followed by HTTP protocol establishment.
|
7 + | //!
|
8 + | //! [`transport`] creates connected I/O and reports the negotiated protocol.
|
9 + | //! This module then routes that transport through the matching Hyper handshake,
|
10 + | //! creates the protocol-neutral connection identity, and returns or transfers
|
11 + | //! completion ownership for the launching waiter.
|
7 12 |
|
8 13 | mod h1;
|
9 14 | mod h2;
|
15 + | mod transport;
|
10 16 |
|
17 + | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
18 + | pub(super) use transport::from_cached_interface_connector;
|
19 + | #[cfg(any(
|
20 + | all(feature = "test-util", aws_sdk_unstable),
|
21 + | all(test, feature = "rt-tokio")
|
22 + | ))]
|
23 + | pub(super) use transport::from_connector;
|
24 + | pub(super) use transport::{from_interface_connector, TransportFactory, TransportTimeout};
|
25 + |
|
26 + | use self::transport::TransportConnectContext;
|
11 27 | use super::admission::ProtocolRequirement;
|
12 - | use super::cell::{AcquisitionResult, EstablishmentPermit, WaiterId};
|
28 + | use super::cell::{AcquisitionOutcome, EstablishmentPermit, WaiterId};
|
13 29 | use super::dispatch::AcquisitionContext;
|
14 - | use super::registry::PartitionState;
|
15 30 | use super::PoolInner;
|
16 - | use crate::client::connect::{AsyncConn, BoxConn};
|
17 - | use crate::client::timeout::{self, TimeoutKind};
|
18 - | use aws_smithy_async::rt::sleep::SharedAsyncSleep;
|
19 - | use aws_smithy_runtime_api::box_error::BoxError;
|
31 + | use crate::client::downcast_error;
|
20 32 | use aws_smithy_runtime_api::client::connection::ConnectionId;
|
21 33 | use aws_smithy_runtime_api::client::result::ConnectorError;
|
22 - | use http_1x::Uri;
|
23 - | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
24 - | use std::collections::HashMap;
|
25 34 | use std::error::Error;
|
26 35 | use std::fmt;
|
27 - | use std::future::{poll_fn, Future};
|
28 - | use std::pin::Pin;
|
29 36 | use std::sync::atomic::Ordering;
|
30 - | use std::sync::Arc as StdArc;
|
31 - | use std::time::Duration;
|
32 - | use tower::Service;
|
33 - |
|
34 - | /// Timeout policy for one transport connection operation.
|
35 - | ///
|
36 - | /// Connector readiness completes before this timeout starts. Keeping the
|
37 - | /// duration and timer together lets request acquisition pass one complete
|
38 - | /// timeout value into transport construction.
|
39 - | #[derive(Clone, Debug)]
|
40 - | pub(super) struct TransportTimeout {
|
41 - | /// Maximum duration of the connector's connection future.
|
42 - | duration: Duration,
|
43 - | /// Runtime timer used to enforce the deadline.
|
44 - | sleep: SharedAsyncSleep,
|
45 - | }
|
46 - |
|
47 - | impl TransportTimeout {
|
48 - | /// Creates a timeout enforced by the supplied runtime timer.
|
49 - | pub(super) fn new(duration: Duration, sleep: SharedAsyncSleep) -> Self {
|
50 - | Self { duration, sleep }
|
51 - | }
|
52 - | }
|
53 - |
|
54 - | /// Future returned by a type-erased transport factory.
|
55 - | type TransportFuture = Pin<Box<dyn Future<Output = Result<BoxConn, BoxError>> + Send + 'static>>;
|
56 - |
|
57 - | /// Type-erased transport construction below HTTP protocol establishment.
|
58 - | ///
|
59 - | /// A pool retains one factory that applies partition interface placement,
|
60 - | /// connector readiness, and the operation's connection timeout. The concrete
|
61 - | /// connector remains responsible for DNS, proxy, TLS, socket construction,
|
62 - | /// and transport metadata. Protocol establishment consumes the resulting
|
63 - | /// [`BoxConn`].
|
64 - | pub(super) trait TransportFactory: Send + Sync + 'static {
|
65 - | /// Returns whether this factory can guarantee HTTP/1 for a required attempt.
|
66 - | fn guarantees_http1(&self) -> bool;
|
67 - |
|
68 - | /// Creates one partition-bound transport for `uri`.
|
69 - | ///
|
70 - | /// Connector readiness completes before `timeout` starts.
|
71 - | fn connect(
|
72 - | &self,
|
73 - | partition: &PartitionState,
|
74 - | uri: Uri,
|
75 - | timeout: Option<TransportTimeout>,
|
76 - | alpn_protocols: AlpnProtocols,
|
77 - | ) -> TransportFuture;
|
78 - | }
|
79 - |
|
80 - | /// Generic connector factory erased at pool construction.
|
81 - | struct ServiceTransportFactory<F> {
|
82 - | /// Builds a connector with the selected network-interface binding.
|
83 - | connector_for_interface: F,
|
84 - | /// Whether every H1-required connection is guaranteed to negotiate H1.
|
85 - | guarantees_http1: bool,
|
86 - | }
|
87 - |
|
88 - | impl<F, C, IO> TransportFactory for ServiceTransportFactory<F>
|
89 - | where
|
90 - | F: Fn(Option<&str>) -> C + Send + Sync + 'static,
|
91 - | C: Service<Uri, Response = IO> + Send + 'static,
|
92 - | C::Error: Into<BoxError>,
|
93 - | C::Future: Send + 'static,
|
94 - | IO: AsyncConn,
|
95 - | {
|
96 - | fn guarantees_http1(&self) -> bool {
|
97 - | self.guarantees_http1
|
98 - | }
|
99 - |
|
100 - | fn connect(
|
101 - | &self,
|
102 - | partition: &PartitionState,
|
103 - | uri: Uri,
|
104 - | timeout: Option<TransportTimeout>,
|
105 - | _alpn_protocols: AlpnProtocols,
|
106 - | ) -> TransportFuture {
|
107 - | let interface = partition.interface().map(|interface| interface.as_ref());
|
108 - | let mut connector = (self.connector_for_interface)(interface);
|
109 - | Box::pin(async move {
|
110 - | // Readiness follows the existing connector contract and is not timed.
|
111 - | poll_fn(|cx| connector.poll_ready(cx))
|
112 - | .await
|
113 - | .map_err(Into::into)?;
|
114 - | let connect = connector.call(uri);
|
115 - | let io = timeout::maybe_timeout_future(
|
116 - | connect,
|
117 - | timeout.as_ref().map(|timeout| timeout.duration),
|
118 - | timeout.as_ref().map(|timeout| &timeout.sleep),
|
119 - | TimeoutKind::Connect,
|
120 - | )
|
121 - | .await?;
|
122 - | Ok(Box::new(io) as BoxConn)
|
123 - | })
|
124 - | }
|
125 - | }
|
126 - |
|
127 - | #[cfg(any(
|
128 - | all(feature = "test-util", aws_sdk_unstable),
|
129 - | all(test, feature = "rt-tokio")
|
130 - | ))]
|
131 - | /// Erases a clonable connector for pool storage.
|
132 - | pub(super) fn transport_factory<C, IO>(connector: C) -> StdArc<dyn TransportFactory>
|
133 - | where
|
134 - | C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
|
135 - | C::Error: Into<BoxError>,
|
136 - | C::Future: Send + 'static,
|
137 - | IO: AsyncConn,
|
138 - | {
|
139 - | service_transport_factory_for_interface(move |_| connector.clone(), false)
|
140 - | }
|
141 - |
|
142 - | /// Erases a connector constructor that applies an optional interface binding.
|
143 - | ///
|
144 - | /// The default HTTP connector consumes this value. Custom connectors use
|
145 - | /// `transport_factory` and ignore pool interface placement.
|
146 - | pub(super) fn transport_factory_for_interface<F, C, IO>(
|
147 - | connector_for_interface: F,
|
148 - | ) -> StdArc<dyn TransportFactory>
|
149 - | where
|
150 - | F: Fn(Option<&str>) -> C + Send + Sync + 'static,
|
151 - | C: Service<Uri, Response = IO> + Send + 'static,
|
152 - | C::Error: Into<BoxError>,
|
153 - | C::Future: Send + 'static,
|
154 - | IO: AsyncConn,
|
155 - | {
|
156 - | service_transport_factory_for_interface(connector_for_interface, true)
|
157 - | }
|
158 - |
|
159 - | /// Erases a service connector and its HTTP/1 negotiation guarantee.
|
160 - | fn service_transport_factory_for_interface<F, C, IO>(
|
161 - | connector_for_interface: F,
|
162 - | guarantees_http1: bool,
|
163 - | ) -> StdArc<dyn TransportFactory>
|
164 - | where
|
165 - | F: Fn(Option<&str>) -> C + Send + Sync + 'static,
|
166 - | C: Service<Uri, Response = IO> + Send + 'static,
|
167 - | C::Error: Into<BoxError>,
|
168 - | C::Future: Send + 'static,
|
169 - | IO: AsyncConn,
|
170 - | {
|
171 - | StdArc::new(ServiceTransportFactory {
|
172 - | connector_for_interface,
|
173 - | guarantees_http1,
|
174 - | })
|
175 - | }
|
176 - |
|
177 - | /// Erases and caches connectors whose construction carries TLS or other setup.
|
178 - | ///
|
179 - | /// Interface placement is immutable for a partition. The first request for
|
180 - | /// each `(interface, ALPN offer)` constructs its connector while holding the
|
181 - | /// cache lock; later requests clone that retained connector. Provider
|
182 - | /// configuration and certificate loading therefore occur at most once for
|
183 - | /// each placement and offer rather than once per connection.
|
184 - | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
185 - | pub(super) fn cached_transport_factory_for_interface<F, C, IO>(
|
186 - | connector_for_interface: F,
|
187 - | guarantees_http1: bool,
|
188 - | ) -> StdArc<dyn TransportFactory>
|
189 - | where
|
190 - | F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
|
191 - | C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
|
192 - | C::Error: Into<BoxError>,
|
193 - | C::Future: Send + 'static,
|
194 - | IO: AsyncConn,
|
195 - | {
|
196 - | struct Cached<F, C> {
|
197 - | factory: F,
|
198 - | guarantees_http1: bool,
|
199 - | connectors: crate::sync::Mutex<HashMap<(Option<String>, AlpnProtocols), C>>,
|
200 - | }
|
201 - |
|
202 - | impl<F, C, IO> TransportFactory for Cached<F, C>
|
203 - | where
|
204 - | F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
|
205 - | C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
|
206 - | C::Error: Into<BoxError>,
|
207 - | C::Future: Send + 'static,
|
208 - | IO: AsyncConn,
|
209 - | {
|
210 - | fn guarantees_http1(&self) -> bool {
|
211 - | self.guarantees_http1
|
212 - | }
|
213 - |
|
214 - | fn connect(
|
215 - | &self,
|
216 - | partition: &PartitionState,
|
217 - | uri: Uri,
|
218 - | timeout: Option<TransportTimeout>,
|
219 - | alpn_protocols: AlpnProtocols,
|
220 - | ) -> TransportFuture {
|
221 - | let interface = partition.interface().map(|value| value.to_string());
|
222 - | let mut connector = self
|
223 - | .connectors
|
224 - | .lock()
|
225 - | .entry((interface.clone(), alpn_protocols))
|
226 - | .or_insert_with(|| (self.factory)(interface.as_deref(), alpn_protocols))
|
227 - | .clone();
|
228 - | Box::pin(async move {
|
229 - | poll_fn(|cx| connector.poll_ready(cx))
|
230 - | .await
|
231 - | .map_err(Into::into)?;
|
232 - | let connect = connector.call(uri);
|
233 - | let io = timeout::maybe_timeout_future(
|
234 - | connect,
|
235 - | timeout.as_ref().map(|timeout| timeout.duration),
|
236 - | timeout.as_ref().map(|timeout| &timeout.sleep),
|
237 - | TimeoutKind::Connect,
|
238 - | )
|
239 - | .await?;
|
240 - | Ok(Box::new(io) as BoxConn)
|
241 - | })
|
242 - | }
|
243 - | }
|
244 - |
|
245 - | StdArc::new(Cached {
|
246 - | factory: connector_for_interface,
|
247 - | guarantees_http1,
|
248 - | connectors: crate::sync::Mutex::new(HashMap::new()),
|
249 - | })
|
250 - | }
|
251 37 |
|
252 38 | /// Result of one owner-runtime establishment task.
|
253 39 | pub(super) enum EstablishmentOutcome {
|
254 40 | /// The launching waiter receives this terminal result.
|
255 - | Complete(AcquisitionResult),
|
256 - | /// Waiter completion transferred to an H2 flight or generation.
|
257 - | Transferred,
|
41 + | Complete(AcquisitionOutcome),
|
42 + | /// An H2 flight or generation now owns the launching waiter's completion.
|
43 + | WaiterCompletionTransferred,
|
258 44 | }
|
259 45 |
|
260 46 | /// Connects one transport and dispatches protocol establishment after ALPN.
|
261 47 | pub(super) async fn establish(
|
262 48 | context: AcquisitionContext,
|
263 49 | waiter: WaiterId,
|
264 50 | permit: EstablishmentPermit,
|
265 51 | requirement: ProtocolRequirement,
|
266 52 | ) -> EstablishmentOutcome {
|
267 - | let io = match context
|
268 - | .pool
|
269 - | .transport
|
270 - | .connect(
|
271 - | &context.partition,
|
272 - | context.absolute_uri.clone(),
|
273 - | context.connect_timeout.clone(),
|
274 - | alpn_protocols(requirement),
|
275 - | )
|
276 - | .await
|
277 - | {
|
53 + | let connect = TransportConnectContext::new(
|
54 + | &context.partition,
|
55 + | context.absolute_uri.clone(),
|
56 + | context.connect_timeout.clone(),
|
57 + | requirement,
|
58 + | );
|
59 + | let io = match context.pool.transport.connect(connect).await {
|
278 60 | Ok(io) => io,
|
279 61 | Err(error) => {
|
280 62 | tracing::debug!(
|
281 63 | request_partition = ?context.partition.id(),
|
282 64 | connection_partition = ?context.cell.id().partition(),
|
283 65 | origin_scheme = %context.cell.id().origin().scheme(),
|
284 66 | origin_host = context.cell.id().origin().host(),
|
285 67 | origin_port = ?context.cell.id().origin().port(),
|
286 68 | error = ?error,
|
287 69 | "transport establishment failed"
|
288 70 | );
|
289 - | return EstablishmentOutcome::Complete(AcquisitionResult::Failed(
|
290 - | super::super::downcast_error(error),
|
291 - | ));
|
71 + | return EstablishmentOutcome::Complete(AcquisitionOutcome::Failed(downcast_error(
|
72 + | error,
|
73 + | )));
|
292 74 | }
|
293 75 | };
|
294 76 | let connected = io.connected();
|
295 77 | let negotiated_h2 = connected.is_negotiated_h2();
|
296 78 | tracing::debug!(
|
297 79 | request_partition = ?context.partition.id(),
|
298 80 | connection_partition = ?context.cell.id().partition(),
|
299 81 | origin_scheme = %context.cell.id().origin().scheme(),
|
300 82 | origin_host = context.cell.id().origin().host(),
|
301 83 | origin_port = ?context.cell.id().origin().port(),
|
302 84 | negotiated_protocol = if negotiated_h2 { "HTTP/2" } else { "HTTP/1.1" },
|
303 85 | "transport protocol negotiated"
|
304 86 | );
|
305 87 | if negotiated_h2 && !requirement.accepts_h2() {
|
306 88 | drop(io);
|
307 89 | drop(permit);
|
308 - | return EstablishmentOutcome::Complete(AcquisitionResult::Failed(ConnectorError::user(
|
309 - | NegotiatedProtocolMismatch { requirement }.into(),
|
310 - | )));
|
90 + | return EstablishmentOutcome::Complete(AcquisitionOutcome::Failed(
|
91 + | negotiated_protocol_mismatch(requirement),
|
92 + | ));
|
311 93 | }
|
312 94 |
|
313 95 | if negotiated_h2 {
|
314 96 | h2::establish_h2(context, waiter, permit, io, connected).await
|
315 97 | } else {
|
316 98 | EstablishmentOutcome::Complete(
|
317 99 | h1::establish_h1(context, permit, io, connected)
|
318 100 | .await
|
319 - | .map(AcquisitionResult::H1)
|
320 - | .unwrap_or_else(AcquisitionResult::Failed),
|
101 + | .map(AcquisitionOutcome::H1)
|
102 + | .unwrap_or_else(AcquisitionOutcome::Failed),
|
321 103 | )
|
322 104 | }
|
323 105 | }
|
324 - |
|
325 106 | /// An established transport selected a protocol incompatible with the request.
|
326 107 | #[derive(Debug)]
|
327 108 | struct NegotiatedProtocolMismatch {
|
328 109 | requirement: ProtocolRequirement,
|
329 110 | }
|
330 111 |
|
331 112 | impl fmt::Display for NegotiatedProtocolMismatch {
|
332 113 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
333 114 | write!(
|
334 115 | f,
|
335 116 | "transport negotiated HTTP/2, which does not satisfy {:?} request semantics",
|
336 117 | self.requirement
|
337 118 | )
|
338 119 | }
|
339 120 | }
|
340 121 |
|
341 122 | impl Error for NegotiatedProtocolMismatch {}
|
342 123 |
|
124 + | fn negotiated_protocol_mismatch(requirement: ProtocolRequirement) -> ConnectorError {
|
125 + | ConnectorError::other(NegotiatedProtocolMismatch { requirement }.into(), None)
|
126 + | }
|
127 + |
|
343 128 | /// Mints one non-wrapping physical-connection identity.
|
344 129 | fn next_connection_id(pool: &PoolInner) -> Result<ConnectionId, ConnectionIdExhausted> {
|
345 130 | let value = pool
|
346 131 | .next_connection_id
|
347 132 | .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
|
348 133 | current.checked_add(1)
|
349 134 | })
|
350 135 | .map_err(|_| ConnectionIdExhausted)?;
|
351 136 | Ok(ConnectionId::new(value))
|
352 137 | }
|
353 138 |
|
354 139 | /// The pool exhausted its monotonic physical-connection identity space.
|
355 140 | #[derive(Debug)]
|
356 141 | struct ConnectionIdExhausted;
|
357 142 |
|
358 143 | impl fmt::Display for ConnectionIdExhausted {
|
359 144 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
360 145 | f.write_str("connection identifier space exhausted")
|
361 146 | }
|
362 147 | }
|
363 148 |
|
364 149 | impl Error for ConnectionIdExhausted {}
|
365 150 |
|
366 - | /// Static ALPN protocol offer used for one transport connection.
|
367 - | pub(super) type AlpnProtocols = &'static [&'static [u8]];
|
368 - |
|
369 - | /// Default offer for requests that may use HTTP/2.
|
370 - | pub(super) const HTTP_ALPN_PROTOCOLS: AlpnProtocols = &[b"h2", b"http/1.1"];
|
151 + | #[cfg(test)]
|
152 + | mod tests {
|
153 + | use super::*;
|
371 154 |
|
372 - | /// Narrowed offer for requests that require HTTP/1 wire semantics.
|
373 - | pub(super) const HTTP1_ALPN_PROTOCOLS: AlpnProtocols = &[b"http/1.1"];
|
155 + | #[test]
|
156 + | fn negotiated_protocol_mismatch_is_not_a_user_error() {
|
157 + | let error = negotiated_protocol_mismatch(ProtocolRequirement::H1Required);
|
374 158 |
|
375 - | fn alpn_protocols(requirement: ProtocolRequirement) -> AlpnProtocols {
|
376 - | match requirement {
|
377 - | ProtocolRequirement::H1Required => HTTP1_ALPN_PROTOCOLS,
|
378 - | ProtocolRequirement::H1Compatible | ProtocolRequirement::H2Required => HTTP_ALPN_PROTOCOLS,
|
159 + | assert!(error.is_other());
|
160 + | assert!(!error.is_user());
|
379 161 | }
|
380 162 | }
|