1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! Transport connection policy below HTTP protocol establishment.
|
7 + | //!
|
8 + | //! This module owns the boundary from a pool connection attempt to connected
|
9 + | //! I/O with a negotiated protocol. [`TransportConnectContext`] keeps placement,
|
10 + | //! timeout, and ALPN inputs together. A factory may delegate DNS, proxy, TLS,
|
11 + | //! and ALPN to an injected connector, but it must report whether an HTTP/1-only
|
12 + | //! attempt is enforceable before admission may reclaim an idle H2 connection.
|
13 + | //!
|
14 + | //! Cached factories retain configured connector services, not connections or
|
15 + | //! DNS results. Network I/O begins only when [`TransportFactory::connect`] is
|
16 + | //! called.
|
17 + |
|
18 + | use super::super::admission::ProtocolRequirement;
|
19 + | use super::super::registry::PartitionState;
|
20 + | use crate::client::connect::{AsyncConn, BoxConn};
|
21 + | use crate::client::timeout::{self, TimeoutKind};
|
22 + | use aws_smithy_async::rt::sleep::SharedAsyncSleep;
|
23 + | use aws_smithy_runtime_api::box_error::BoxError;
|
24 + | use http_1x::Uri;
|
25 + | use std::future::{poll_fn, Future};
|
26 + | use std::pin::Pin;
|
27 + | use std::sync::Arc as StdArc;
|
28 + | use std::time::Duration;
|
29 + | use tower::Service;
|
30 + |
|
31 + | /// Timeout policy for one transport connection operation.
|
32 + | ///
|
33 + | /// Connector readiness completes before this timeout starts. Keeping the
|
34 + | /// duration and timer together lets request acquisition pass one complete
|
35 + | /// timeout value into transport construction.
|
36 + | #[derive(Clone, Debug)]
|
37 + | pub(in crate::client::pool) struct TransportTimeout {
|
38 + | /// Maximum duration of the connector's connection future.
|
39 + | duration: Duration,
|
40 + | /// Runtime timer used to enforce the deadline.
|
41 + | sleep: SharedAsyncSleep,
|
42 + | }
|
43 + |
|
44 + | impl TransportTimeout {
|
45 + | /// Creates a timeout enforced by the supplied runtime timer.
|
46 + | pub(in crate::client::pool) fn new(duration: Duration, sleep: SharedAsyncSleep) -> Self {
|
47 + | Self { duration, sleep }
|
48 + | }
|
49 + | }
|
50 + |
|
51 + | /// Static ALPN protocol offer used for one transport connection.
|
52 + | pub(in crate::client::pool) type AlpnProtocols = &'static [&'static [u8]];
|
53 + |
|
54 + | /// Default offer for requests that may use HTTP/2.
|
55 + | const HTTP_ALPN_PROTOCOLS: AlpnProtocols = &[b"h2", b"http/1.1"];
|
56 + |
|
57 + | /// Narrowed offer for requests that require HTTP/1 wire semantics.
|
58 + | const HTTP1_ALPN_PROTOCOLS: AlpnProtocols = &[b"http/1.1"];
|
59 + |
|
60 + | /// Inputs needed to create one transport connection.
|
61 + | ///
|
62 + | /// This context is the transport-attempt boundary for DNS, socket, proxy, TLS,
|
63 + | /// and ALPN lifecycle observation. It carries no installed connection state
|
64 + | /// because an attempt may fail before a connection exists.
|
65 + | pub(in crate::client::pool) struct TransportConnectContext<'a> {
|
66 + | /// Partition whose placement policy owns the connection attempt.
|
67 + | partition: &'a PartitionState,
|
68 + | /// Absolute URI passed to the connector contract.
|
69 + | uri: Uri,
|
70 + | /// Optional deadline for the connector's connection future.
|
71 + | timeout: Option<TransportTimeout>,
|
72 + | /// Protocols the default TLS connector may advertise for this attempt.
|
73 + | alpn_protocols: AlpnProtocols,
|
74 + | }
|
75 + |
|
76 + | impl<'a> TransportConnectContext<'a> {
|
77 + | /// Creates transport inputs after request protocol classification.
|
78 + | pub(super) fn new(
|
79 + | partition: &'a PartitionState,
|
80 + | uri: Uri,
|
81 + | timeout: Option<TransportTimeout>,
|
82 + | requirement: ProtocolRequirement,
|
83 + | ) -> Self {
|
84 + | Self {
|
85 + | partition,
|
86 + | uri,
|
87 + | timeout,
|
88 + | alpn_protocols: alpn_protocols(requirement),
|
89 + | }
|
90 + | }
|
91 + | }
|
92 + |
|
93 + | /// Future returned by a type-erased transport factory.
|
94 + | type TransportFuture = Pin<Box<dyn Future<Output = Result<BoxConn, BoxError>> + Send + 'static>>;
|
95 + |
|
96 + | /// Type-erased transport construction below HTTP protocol establishment.
|
97 + | ///
|
98 + | /// A pool retains one factory. The concrete connector remains responsible for
|
99 + | /// the transport stages it implements and for returning connector metadata.
|
100 + | /// Protocol establishment consumes the resulting [`BoxConn`].
|
101 + | pub(in crate::client::pool) trait TransportFactory:
|
102 + | Send + Sync + 'static
|
103 + | {
|
104 + | /// Returns whether an H1-required attempt is guaranteed to negotiate H1.
|
105 + | fn can_guarantee_http1(&self) -> bool;
|
106 + |
|
107 + | /// Creates one partition-bound transport.
|
108 + | ///
|
109 + | /// Connector readiness completes before the connection timeout starts.
|
110 + | fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture;
|
111 + | }
|
112 + |
|
113 + | /// Connector factory whose concrete service is selected by interface.
|
114 + | struct ServiceTransportFactory<F> {
|
115 + | /// Builds a connector with the selected network-interface binding.
|
116 + | connector_for_interface: F,
|
117 + | /// Whether every H1-required connection is guaranteed to negotiate H1.
|
118 + | can_guarantee_http1: bool,
|
119 + | }
|
120 + |
|
121 + | impl<F, C, IO> TransportFactory for ServiceTransportFactory<F>
|
122 + | where
|
123 + | F: Fn(Option<&str>) -> C + Send + Sync + 'static,
|
124 + | C: Service<Uri, Response = IO> + Send + 'static,
|
125 + | C::Error: Into<BoxError>,
|
126 + | C::Future: Send + 'static,
|
127 + | IO: AsyncConn,
|
128 + | {
|
129 + | fn can_guarantee_http1(&self) -> bool {
|
130 + | self.can_guarantee_http1
|
131 + | }
|
132 + |
|
133 + | fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture {
|
134 + | let TransportConnectContext {
|
135 + | partition,
|
136 + | uri,
|
137 + | timeout,
|
138 + | alpn_protocols: _alpn_protocols,
|
139 + | } = context;
|
140 + | let interface = partition.interface().map(|interface| interface.as_ref());
|
141 + | let mut connector = (self.connector_for_interface)(interface);
|
142 + | Box::pin(async move {
|
143 + | // Readiness follows the existing connector contract and is not timed.
|
144 + | poll_fn(|cx| connector.poll_ready(cx))
|
145 + | .await
|
146 + | .map_err(Into::into)?;
|
147 + | let connect = connector.call(uri);
|
148 + | let io = timeout::maybe_timeout_future(
|
149 + | connect,
|
150 + | timeout.as_ref().map(|timeout| timeout.duration),
|
151 + | timeout.as_ref().map(|timeout| &timeout.sleep),
|
152 + | TimeoutKind::Connect,
|
153 + | )
|
154 + | .await?;
|
155 + | Ok(Box::new(io) as BoxConn)
|
156 + | })
|
157 + | }
|
158 + | }
|
159 + |
|
160 + | /// Erases an injected connector that owns placement and protocol negotiation.
|
161 + | ///
|
162 + | /// The pool cannot apply its interface or ALPN inputs to this connector. It is
|
163 + | /// therefore conservative about H2 reclaim for H1-required demand.
|
164 + | #[cfg(any(
|
165 + | all(feature = "test-util", aws_sdk_unstable),
|
166 + | all(test, feature = "rt-tokio")
|
167 + | ))]
|
168 + | pub(in crate::client::pool) fn from_connector<C, IO>(connector: C) -> StdArc<dyn TransportFactory>
|
169 + | where
|
170 + | C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
|
171 + | C::Error: Into<BoxError>,
|
172 + | C::Future: Send + 'static,
|
173 + | IO: AsyncConn,
|
174 + | {
|
175 + | service_factory(move |_| connector.clone(), false)
|
176 + | }
|
177 + |
|
178 + | /// Erases a cleartext connector constructor that applies interface placement.
|
179 + | pub(in crate::client::pool) fn from_interface_connector<F, C, IO>(
|
180 + | connector_for_interface: F,
|
181 + | ) -> StdArc<dyn TransportFactory>
|
182 + | where
|
183 + | F: Fn(Option<&str>) -> C + Send + Sync + 'static,
|
184 + | C: Service<Uri, Response = IO> + Send + 'static,
|
185 + | C::Error: Into<BoxError>,
|
186 + | C::Future: Send + 'static,
|
187 + | IO: AsyncConn,
|
188 + | {
|
189 + | service_factory(connector_for_interface, true)
|
190 + | }
|
191 + |
|
192 + | /// Erases a service connector and its HTTP/1 negotiation guarantee.
|
193 + | fn service_factory<F, C, IO>(
|
194 + | connector_for_interface: F,
|
195 + | can_guarantee_http1: bool,
|
196 + | ) -> StdArc<dyn TransportFactory>
|
197 + | where
|
198 + | F: Fn(Option<&str>) -> C + Send + Sync + 'static,
|
199 + | C: Service<Uri, Response = IO> + Send + 'static,
|
200 + | C::Error: Into<BoxError>,
|
201 + | C::Future: Send + 'static,
|
202 + | IO: AsyncConn,
|
203 + | {
|
204 + | StdArc::new(ServiceTransportFactory {
|
205 + | connector_for_interface,
|
206 + | can_guarantee_http1,
|
207 + | })
|
208 + | }
|
209 + |
|
210 + | /// Erases and caches configured TLS connector services.
|
211 + | ///
|
212 + | /// Interface placement is immutable for a partition. The first request for
|
213 + | /// each `(interface, ALPN offer)` constructs its connector while holding the
|
214 + | /// cache lock; later requests clone that retained connector. Provider
|
215 + | /// configuration and certificate loading therefore occur at most once for
|
216 + | /// each placement and offer rather than once per connection.
|
217 + | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
218 + | pub(in crate::client::pool) fn from_cached_interface_connector<F, C, IO>(
|
219 + | connector_for_interface: F,
|
220 + | can_guarantee_http1: bool,
|
221 + | ) -> StdArc<dyn TransportFactory>
|
222 + | where
|
223 + | F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
|
224 + | C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
|
225 + | C::Error: Into<BoxError>,
|
226 + | C::Future: Send + 'static,
|
227 + | IO: AsyncConn,
|
228 + | {
|
229 + | struct CachedTransportFactory<F, C> {
|
230 + | factory: F,
|
231 + | can_guarantee_http1: bool,
|
232 + | connectors:
|
233 + | crate::sync::Mutex<std::collections::HashMap<(Option<String>, AlpnProtocols), C>>,
|
234 + | }
|
235 + |
|
236 + | impl<F, C, IO> TransportFactory for CachedTransportFactory<F, C>
|
237 + | where
|
238 + | F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
|
239 + | C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
|
240 + | C::Error: Into<BoxError>,
|
241 + | C::Future: Send + 'static,
|
242 + | IO: AsyncConn,
|
243 + | {
|
244 + | fn can_guarantee_http1(&self) -> bool {
|
245 + | self.can_guarantee_http1
|
246 + | }
|
247 + |
|
248 + | fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture {
|
249 + | let TransportConnectContext {
|
250 + | partition,
|
251 + | uri,
|
252 + | timeout,
|
253 + | alpn_protocols,
|
254 + | } = context;
|
255 + | let interface = partition.interface().map(|value| value.to_string());
|
256 + | let mut connector = self
|
257 + | .connectors
|
258 + | .lock()
|
259 + | .entry((interface.clone(), alpn_protocols))
|
260 + | .or_insert_with(|| (self.factory)(interface.as_deref(), alpn_protocols))
|
261 + | .clone();
|
262 + | Box::pin(async move {
|
263 + | poll_fn(|cx| connector.poll_ready(cx))
|
264 + | .await
|
265 + | .map_err(Into::into)?;
|
266 + | let connect = connector.call(uri);
|
267 + | let io = timeout::maybe_timeout_future(
|
268 + | connect,
|
269 + | timeout.as_ref().map(|timeout| timeout.duration),
|
270 + | timeout.as_ref().map(|timeout| &timeout.sleep),
|
271 + | TimeoutKind::Connect,
|
272 + | )
|
273 + | .await?;
|
274 + | Ok(Box::new(io) as BoxConn)
|
275 + | })
|
276 + | }
|
277 + | }
|
278 + |
|
279 + | StdArc::new(CachedTransportFactory {
|
280 + | factory: connector_for_interface,
|
281 + | can_guarantee_http1,
|
282 + | connectors: crate::sync::Mutex::new(std::collections::HashMap::new()),
|
283 + | })
|
284 + | }
|
285 + |
|
286 + | /// Selects the ALPN offer that preserves the request's wire semantics.
|
287 + | fn alpn_protocols(requirement: ProtocolRequirement) -> AlpnProtocols {
|
288 + | match requirement {
|
289 + | ProtocolRequirement::H1Required => HTTP1_ALPN_PROTOCOLS,
|
290 + | ProtocolRequirement::H1Compatible | ProtocolRequirement::H2Required => HTTP_ALPN_PROTOCOLS,
|
291 + | }
|
292 + | }
|