200 211 | C::Error: Into<BoxError>,
|
201 212 | C::Future: Send + 'static,
|
202 213 | IO: AsyncConn,
|
203 214 | {
|
204 215 | StdArc::new(ServiceTransportFactory {
|
205 216 | connector_for_interface,
|
206 217 | can_guarantee_http1,
|
207 218 | })
|
208 219 | }
|
209 220 |
|
221 + | /// Identifies one configured connector service in the TLS transport cache.
|
222 + | ///
|
223 + | /// Partition identity is deliberately absent. Partitions with equal interface
|
224 + | /// bindings use the same connector configuration and therefore share entries.
|
225 + | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
226 + | #[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
227 + | struct ConnectorCacheKey {
|
228 + | /// Network-interface binding applied before connect.
|
229 + | interface: Option<StdArc<str>>,
|
230 + | /// Protocol offer installed in the TLS connector.
|
231 + | alpn_protocols: AlpnProtocols,
|
232 + | }
|
233 + |
|
234 + | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
235 + | impl ConnectorCacheKey {
|
236 + | /// Derives the connector configuration used by one partition and offer.
|
237 + | fn for_partition(partition: &PartitionState, alpn_protocols: AlpnProtocols) -> Self {
|
238 + | Self {
|
239 + | interface: partition.interface().cloned(),
|
240 + | alpn_protocols,
|
241 + | }
|
242 + | }
|
243 + |
|
244 + | /// Returns the borrowed interface name expected by connector builders.
|
245 + | fn interface(&self) -> Option<&str> {
|
246 + | self.interface.as_deref()
|
247 + | }
|
248 + | }
|
249 + |
|
250 + | /// Retains configured TLS connector services by placement and ALPN offer.
|
251 + | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
252 + | struct CachedTransportFactory<F, C> {
|
253 + | /// Constructs a connector service for one exact cache key.
|
254 + | factory: F,
|
255 + | /// Whether every H1-required connection is guaranteed to negotiate H1.
|
256 + | can_guarantee_http1: bool,
|
257 + | /// Connector services constructed during client validation or first use.
|
258 + | connectors: crate::sync::Mutex<std::collections::HashMap<ConnectorCacheKey, C>>,
|
259 + | }
|
260 + |
|
261 + | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
262 + | impl<F, C> CachedTransportFactory<F, C>
|
263 + | where
|
264 + | F: Fn(Option<&str>, AlpnProtocols) -> C,
|
265 + | C: Clone,
|
266 + | {
|
267 + | /// Returns the retained connector for one partition and protocol offer.
|
268 + | fn connector(&self, partition: &PartitionState, alpn_protocols: AlpnProtocols) -> C {
|
269 + | let key = ConnectorCacheKey::for_partition(partition, alpn_protocols);
|
270 + | match self.connectors.lock().entry(key) {
|
271 + | std::collections::hash_map::Entry::Occupied(entry) => entry.get().clone(),
|
272 + | std::collections::hash_map::Entry::Vacant(entry) => {
|
273 + | let connector = (self.factory)(entry.key().interface(), entry.key().alpn_protocols);
|
274 + | entry.insert(connector).clone()
|
275 + | }
|
276 + | }
|
277 + | }
|
278 + |
|
279 + | /// Materializes every connector variant this partition may select.
|
280 + | fn initialize_connectors_for_partition(&self, partition: &PartitionState) {
|
281 + | drop(self.connector(partition, HTTP_ALPN_PROTOCOLS));
|
282 + | drop(self.connector(partition, HTTP1_ALPN_PROTOCOLS));
|
283 + | }
|
284 + | }
|
285 + |
|
286 + | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
287 + | impl<F, C, IO> TransportFactory for CachedTransportFactory<F, C>
|
288 + | where
|
289 + | F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
|
290 + | C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
|
291 + | C::Error: Into<BoxError>,
|
292 + | C::Future: Send + 'static,
|
293 + | IO: AsyncConn,
|
294 + | {
|
295 + | fn initialize_for_partition(&self, partition: &PartitionState) -> Result<(), BoxError> {
|
296 + | self.initialize_connectors_for_partition(partition);
|
297 + | Ok(())
|
298 + | }
|
299 + |
|
300 + | fn can_guarantee_http1(&self) -> bool {
|
301 + | self.can_guarantee_http1
|
302 + | }
|
303 + |
|
304 + | fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture {
|
305 + | let TransportConnectContext {
|
306 + | partition,
|
307 + | uri,
|
308 + | timeout,
|
309 + | alpn_protocols,
|
310 + | } = context;
|
311 + | let mut connector = self.connector(partition, alpn_protocols);
|
312 + | Box::pin(async move {
|
313 + | poll_fn(|cx| connector.poll_ready(cx))
|
314 + | .await
|
315 + | .map_err(Into::into)?;
|
316 + | let connect = connector.call(uri);
|
317 + | let io = timeout::maybe_timeout_future(
|
318 + | connect,
|
319 + | timeout.as_ref().map(|timeout| timeout.duration),
|
320 + | timeout.as_ref().map(|timeout| &timeout.sleep),
|
321 + | TimeoutKind::Connect,
|
322 + | )
|
323 + | .await?;
|
324 + | Ok(Box::new(io) as BoxConn)
|
325 + | })
|
326 + | }
|
327 + | }
|
328 + |
|
210 329 | /// Erases and caches configured TLS connector services.
|
211 330 | ///
|
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
|
331 + | /// Interface placement is immutable for a partition. Client validation
|
332 + | /// constructs both ALPN variants for the selected partition while holding the
|
333 + | /// cache lock; later requests clone the retained connector. Provider
|
215 334 | /// configuration and certificate loading therefore occur at most once for
|
216 335 | /// each placement and offer rather than once per connection.
|
217 336 | #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
|
218 337 | pub(in crate::client::pool) fn from_cached_interface_connector<F, C, IO>(
|
219 338 | connector_for_interface: F,
|
220 339 | can_guarantee_http1: bool,
|
221 340 | ) -> StdArc<dyn TransportFactory>
|
222 341 | where
|
223 342 | F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
|
224 343 | C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
|
225 344 | C::Error: Into<BoxError>,
|
226 345 | C::Future: Send + 'static,
|
227 346 | IO: AsyncConn,
|
228 347 | {
|
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 348 | StdArc::new(CachedTransportFactory {
|
280 349 | factory: connector_for_interface,
|
281 350 | can_guarantee_http1,
|
282 351 | connectors: crate::sync::Mutex::new(std::collections::HashMap::new()),
|
283 352 | })
|
284 353 | }
|
285 354 |
|
286 355 | /// Selects the ALPN offer that preserves the request's wire semantics.
|
287 356 | fn alpn_protocols(requirement: ProtocolRequirement) -> AlpnProtocols {
|
288 357 | match requirement {
|
289 358 | ProtocolRequirement::H1Required => HTTP1_ALPN_PROTOCOLS,
|
290 359 | ProtocolRequirement::H1Compatible | ProtocolRequirement::H2Required => HTTP_ALPN_PROTOCOLS,
|
291 360 | }
|
292 361 | }
|
362 + |
|
363 + | #[cfg(all(
|
364 + | test,
|
365 + | any(feature = "__rustls", feature = "s2n-tls"),
|
366 + | any(
|
367 + | target_os = "android",
|
368 + | target_os = "fuchsia",
|
369 + | target_os = "illumos",
|
370 + | target_os = "ios",
|
371 + | target_os = "linux",
|
372 + | target_os = "macos",
|
373 + | target_os = "solaris",
|
374 + | target_os = "tvos",
|
375 + | target_os = "visionos",
|
376 + | target_os = "watchos",
|
377 + | )
|
378 + | ))]
|
379 + | mod tests {
|
380 + | use super::*;
|
381 + | use crate::client::pool::maintenance::MaintenanceConfig;
|
382 + | use crate::client::pool::partition::{
|
383 + | ConnectionReuseScope, DriverSpawner, Partition, PartitionId,
|
384 + | };
|
385 + | use crate::client::pool::registry::PartitionRegistry;
|
386 + | use aws_smithy_runtime_api::client::http::HttpClient;
|
387 + | use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
|
388 + | use aws_smithy_types::config_bag::ConfigBag;
|
389 + |
|
390 + | use std::sync::{Arc, Mutex};
|
391 + |
|
392 + | #[derive(Debug)]
|
393 + | struct TestSpawner;
|
394 + |
|
395 + | impl DriverSpawner for TestSpawner {
|
396 + | fn spawn(&self, _: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {}
|
397 + | }
|
398 + |
|
399 + | struct InitializationRecorder {
|
400 + | partitions: Arc<Mutex<Vec<PartitionId>>>,
|
401 + | }
|
402 + |
|
403 + | impl TransportFactory for InitializationRecorder {
|
404 + | fn initialize_for_partition(&self, partition: &PartitionState) -> Result<(), BoxError> {
|
405 + | self.partitions
|
406 + | .lock()
|
407 + | .expect("initialization log is not poisoned")
|
408 + | .push(partition.id());
|
409 + | Ok(())
|
410 + | }
|
411 + |
|
412 + | fn can_guarantee_http1(&self) -> bool {
|
413 + | true
|
414 + | }
|
415 + |
|
416 + | fn connect(&self, _: TransportConnectContext<'_>) -> TransportFuture {
|
417 + | Box::pin(async { panic!("client validation must not start a connection") })
|
418 + | }
|
419 + | }
|
420 + |
|
421 + | type Construction = (Option<String>, AlpnProtocols);
|
422 + |
|
423 + | struct CachedFactoryFixture<F> {
|
424 + | factory: CachedTransportFactory<F, usize>,
|
425 + | constructions: Arc<Mutex<Vec<Construction>>>,
|
426 + | }
|
427 + |
|
428 + | fn recording_factory() -> CachedFactoryFixture<impl Fn(Option<&str>, AlpnProtocols) -> usize> {
|
429 + | let constructions = Arc::new(Mutex::new(Vec::new()));
|
430 + | let observed = constructions.clone();
|
431 + | let factory = move |interface: Option<&str>, alpn_protocols: AlpnProtocols| {
|
432 + | let mut constructions = observed.lock().expect("construction log is not poisoned");
|
433 + | constructions.push((interface.map(str::to_owned), alpn_protocols));
|
434 + | constructions.len()
|
435 + | };
|
436 + | CachedFactoryFixture {
|
437 + | factory: CachedTransportFactory {
|
438 + | factory,
|
439 + | can_guarantee_http1: true,
|
440 + | connectors: crate::sync::Mutex::new(std::collections::HashMap::new()),
|
441 + | },
|
442 + | constructions,
|
443 + | }
|
444 + | }
|
445 + |
|
446 + | fn registry(partitions: Option<Vec<Partition>>) -> PartitionRegistry {
|
447 + | PartitionRegistry::new(
|
448 + | partitions,
|
449 + | ConnectionReuseScope::Partition,
|
450 + | None,
|
451 + | MaintenanceConfig::default(),
|
452 + | )
|
453 + | .expect("valid partition registry")
|
454 + | }
|
455 + |
|
456 + | #[test]
|
457 + | fn client_validation_initializes_its_selected_partition() {
|
458 + | let selected = PartitionId::from_index(7);
|
459 + | let initialized = Arc::new(Mutex::new(Vec::new()));
|
460 + | let pool = crate::client::pool::builder::Builder::default()
|
461 + | .partitions([Partition::new(selected, TestSpawner)])
|
462 + | .build_with_transport_for_test(Arc::new(InitializationRecorder {
|
463 + | partitions: initialized.clone(),
|
464 + | }))
|
465 + | .expect("valid test pool");
|
466 + | let client = crate::client::pool::Client::from_partition(&pool, selected)
|
467 + | .expect("selected partition exists");
|
468 + |
|
469 + | client
|
470 + | .validate_base_client_config(&RuntimeComponentsBuilder::for_tests(), &ConfigBag::base())
|
471 + | .expect("transport initialization succeeds");
|
472 + |
|
473 + | assert_eq!(
|
474 + | vec![selected],
|
475 + | *initialized
|
476 + | .lock()
|
477 + | .expect("initialization log is not poisoned")
|
478 + | );
|
479 + | }
|
480 + |
|
481 + | #[test]
|
482 + | fn initialization_constructs_each_alpn_variant_once() {
|
483 + | let partition = registry(None)
|
484 + | .partition(PartitionId::ANONYMOUS)
|
485 + | .expect("anonymous partition exists");
|
486 + | let fixture = recording_factory();
|
487 + |
|
488 + | fixture
|
489 + | .factory
|
490 + | .initialize_connectors_for_partition(&partition);
|
491 + | fixture
|
492 + | .factory
|
493 + | .initialize_connectors_for_partition(&partition);
|
494 + |
|
495 + | let constructions = fixture
|
496 + | .constructions
|
497 + | .lock()
|
498 + | .expect("construction log is not poisoned");
|
499 + | assert_eq!(2, constructions.len());
|
500 + | assert_eq!(None, constructions[0].0);
|
501 + | assert_eq!(HTTP_ALPN_PROTOCOLS, constructions[0].1);
|
502 + | assert_eq!(HTTP1_ALPN_PROTOCOLS, constructions[1].1);
|
503 + | assert_eq!(2, fixture.factory.connectors.lock().len());
|
504 + | }
|
505 + |
|
506 + | #[test]
|
507 + | fn equal_interface_bindings_share_connector_entries() {
|
508 + | let first = PartitionId::from_index(1);
|
509 + | let second = PartitionId::from_index(2);
|
510 + | let registry = registry(Some(vec![
|
511 + | Partition::new(first, TestSpawner).interface("interface-a"),
|
512 + | Partition::new(second, TestSpawner).interface("interface-a"),
|
513 + | ]));
|
514 + | let fixture = recording_factory();
|
515 + |
|
516 + | fixture.factory.initialize_connectors_for_partition(
|
517 + | ®istry.partition(first).expect("first partition exists"),
|
518 + | );
|
519 + | fixture.factory.initialize_connectors_for_partition(
|
520 + | ®istry.partition(second).expect("second partition exists"),
|
521 + | );
|
522 + |
|
523 + | let constructions = fixture
|
524 + | .constructions
|
525 + | .lock()
|
526 + | .expect("construction log is not poisoned");
|
527 + | assert_eq!(
|
528 + | [
|
529 + | (Some("interface-a".to_string()), HTTP_ALPN_PROTOCOLS),
|
530 + | (Some("interface-a".to_string()), HTTP1_ALPN_PROTOCOLS),
|
531 + | ],
|
532 + | constructions.as_slice()
|
533 + | );
|
534 + | assert_eq!(2, fixture.factory.connectors.lock().len());
|
535 + | }
|
536 + |
|
537 + | #[test]
|
538 + | fn distinct_interface_bindings_use_distinct_connector_entries() {
|
539 + | let first = PartitionId::from_index(1);
|
540 + | let second = PartitionId::from_index(2);
|
541 + | let registry = registry(Some(vec![
|
542 + | Partition::new(first, TestSpawner).interface("interface-a"),
|
543 + | Partition::new(second, TestSpawner).interface("interface-b"),
|
544 + | ]));
|
545 + | let fixture = recording_factory();
|
546 + |
|
547 + | fixture.factory.initialize_connectors_for_partition(
|
548 + | ®istry.partition(first).expect("first partition exists"),
|
549 + | );
|
550 + | fixture.factory.initialize_connectors_for_partition(
|
551 + | ®istry.partition(second).expect("second partition exists"),
|
552 + | );
|
553 + |
|
554 + | let constructions = fixture
|
555 + | .constructions
|
556 + | .lock()
|
557 + | .expect("construction log is not poisoned");
|
558 + | assert_eq!(
|
559 + | [
|
560 + | (Some("interface-a".to_string()), HTTP_ALPN_PROTOCOLS),
|
561 + | (Some("interface-a".to_string()), HTTP1_ALPN_PROTOCOLS),
|
562 + | (Some("interface-b".to_string()), HTTP_ALPN_PROTOCOLS),
|
563 + | (Some("interface-b".to_string()), HTTP1_ALPN_PROTOCOLS),
|
564 + | ],
|
565 + | constructions.as_slice()
|
566 + | );
|
567 + | assert_eq!(4, fixture.factory.connectors.lock().len());
|
568 + | }
|
569 + | }
|