Skip to main content

aws_smithy_http_client/client/pool/
builder.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Construction and validation for [`ConnectionPool`].
7
8use super::establish::{self, TransportFactory};
9use super::events::{ConnectionEventListener, ConnectionEvents, SharedConnectionEventListener};
10use super::maintenance::MaintenanceConfig;
11use super::registry::{AdmissionPolicy, PartitionRegistry, PartitionRegistryError};
12use super::{ConnectionPool, ConnectionReuseScope, Partition, PoolConfig, PoolInner};
13use crate::client::connect::HttpProxyConnector;
14use crate::client::dns::HyperUtilResolver;
15use crate::client::{TlsProviderSelected, TlsUnset};
16use crate::proxy::ProxyConfig;
17use crate::sync::Arc;
18use crate::tls::{self, TlsContext};
19use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep};
20use aws_smithy_async::time::{SharedTimeSource, TimeSource};
21#[cfg(any(
22    all(test, feature = "rt-tokio"),
23    all(feature = "test-util", aws_sdk_unstable)
24))]
25use aws_smithy_runtime_api::box_error::BoxError;
26use aws_smithy_runtime_api::client::dns::{ResolveDns, ResolveDnsError, SharedDnsResolver};
27use aws_smithy_runtime_api::shared::IntoShared;
28#[cfg(any(
29    all(test, feature = "rt-tokio"),
30    all(feature = "test-util", aws_sdk_unstable)
31))]
32use http_1x::Uri;
33use hyper_util::client::legacy::connect::dns::GaiResolver;
34use hyper_util::client::legacy::connect::dns::Name;
35use hyper_util::client::legacy::connect::HttpConnector;
36use std::error::Error;
37use std::fmt;
38use std::future::Future;
39use std::net::SocketAddr;
40use std::num::NonZeroUsize;
41use std::pin::Pin;
42use std::sync::atomic::AtomicU64;
43use std::task::{Context, Poll};
44use std::time::Duration;
45use tower::Service;
46
47/// Default duration for retaining an idle reusable connection.
48const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
49
50#[derive(Clone, Debug, Default, Eq, PartialEq)]
51enum TriStateOption<T> {
52    /// No option was set by the user, so construction applies its default.
53    #[default]
54    NotSet,
55    /// The option was explicitly unset, so construction does not apply a default.
56    ExplicitlyUnset,
57    /// The caller supplied this value.
58    Set(T),
59}
60
61impl<T> TriStateOption<T> {
62    /// Preserves the public nested-option setter contract internally.
63    fn from_nested(value: Option<Option<T>>) -> Self {
64        match value {
65            None => Self::NotSet,
66            Some(None) => Self::ExplicitlyUnset,
67            Some(Some(value)) => Self::Set(value),
68        }
69    }
70
71    /// Resolves the setting, applying `default` only when it was never set.
72    fn resolve(self, default: Option<T>) -> Option<T> {
73        match self {
74            Self::NotSet => default,
75            Self::ExplicitlyUnset => None,
76            Self::Set(value) => Some(value),
77        }
78    }
79}
80
81/// Builds a partition-aware connection pool.
82#[derive(Clone)]
83pub struct Builder<Tls = TlsUnset> {
84    /// Idle retention setting, including explicit disablement.
85    idle_timeout: TriStateOption<Duration>,
86    /// Clock used to assign and evaluate idle deadlines.
87    time_source: Option<SharedTimeSource>,
88    /// Runtime timer used by idle maintenance.
89    sleep_impl: Option<SharedAsyncSleep>,
90    /// Whether newly created TCP sockets disable Nagle's algorithm.
91    tcp_nodelay: bool,
92    /// TCP keepalive setting, including explicit disablement.
93    tcp_keepalive: TriStateOption<Duration>,
94    /// Optional live-connection limit per scheme, host, and port.
95    max_connections_per_host: Option<usize>,
96    /// Partitions allowed to reuse each other's connections.
97    reuse_scope: ConnectionReuseScope,
98    /// Complete explicit partition set, or `None` for anonymous topology.
99    partitions: Option<Vec<Partition>>,
100    /// Pool-wide proxy selection, authentication, and bypass policy.
101    proxy_config: ProxyConfig,
102    /// Caller-supplied DNS resolver, or `None` for the client default.
103    dns_resolver: Option<SharedDnsResolver>,
104    /// Optional listener for every connection lifecycle event from this pool.
105    event_listener: Option<SharedConnectionEventListener>,
106    /// TLS typestate carried into protocol-specific terminal builders.
107    tls: Tls,
108}
109
110/// Connection-pool builder with a TLS provider selected.
111///
112/// Created by [`Builder::tls_provider`]. HTTPS construction requires an enabled
113/// TLS provider feature.
114pub type HttpsBuilder = Builder<TlsProviderSelected>;
115
116impl Default for Builder<TlsUnset> {
117    fn default() -> Self {
118        Self {
119            idle_timeout: TriStateOption::NotSet,
120            time_source: None,
121            sleep_impl: None,
122            tcp_nodelay: true,
123            tcp_keepalive: TriStateOption::NotSet,
124            max_connections_per_host: None,
125            reuse_scope: ConnectionReuseScope::default(),
126            partitions: None,
127            proxy_config: ProxyConfig::disabled(),
128            dns_resolver: None,
129            event_listener: None,
130            tls: TlsUnset {},
131        }
132    }
133}
134
135impl<Tls: fmt::Debug> fmt::Debug for Builder<Tls> {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("Builder")
138            .field("idle_timeout", &self.idle_timeout)
139            .field("time_source", &self.time_source)
140            .field("sleep_impl", &self.sleep_impl)
141            .field("tcp_nodelay", &self.tcp_nodelay)
142            .field("tcp_keepalive", &self.tcp_keepalive)
143            .field("max_connections_per_host", &self.max_connections_per_host)
144            .field("reuse_scope", &self.reuse_scope)
145            .field("partitions", &self.partitions.as_ref().map(Vec::len))
146            .field(
147                "proxy_config",
148                &if self.proxy_config.is_disabled() {
149                    "disabled"
150                } else if self.proxy_config.is_from_env() {
151                    "environment"
152                } else {
153                    "configured"
154                },
155            )
156            .field(
157                "dns_resolver",
158                &if self.dns_resolver.is_some() {
159                    "custom"
160                } else {
161                    "default"
162                },
163            )
164            .field(
165                "event_listener",
166                &if self.event_listener.is_some() {
167                    "configured"
168                } else {
169                    "disabled"
170                },
171            )
172            .field("tls", &self.tls)
173            .finish()
174    }
175}
176
177impl<Tls> Builder<Tls> {
178    /// Sets how long a reusable idle connection remains in the pool.
179    ///
180    /// Passing `None` disables idle expiration. When unset, the pool uses a
181    /// 90-second timeout.
182    pub fn idle_timeout(mut self, timeout: impl Into<Option<Duration>>) -> Self {
183        self.idle_timeout = match timeout.into() {
184            Some(timeout) => TriStateOption::Set(timeout),
185            None => TriStateOption::ExplicitlyUnset,
186        };
187        self
188    }
189
190    /// Mutably sets the idle timeout.
191    ///
192    /// The outer `None` restores the default; `Some(None)` disables expiry.
193    pub fn set_idle_timeout(&mut self, timeout: Option<Option<Duration>>) -> &mut Self {
194        self.idle_timeout = TriStateOption::from_nested(timeout);
195        self
196    }
197
198    /// Sets the clock used by pool maintenance and connection timing.
199    pub fn time_source(mut self, source: impl TimeSource + 'static) -> Self {
200        self.time_source = Some(SharedTimeSource::new(source));
201        self
202    }
203
204    /// Mutably sets the clock used by pool maintenance and connection timing.
205    pub fn set_time_source(&mut self, source: Option<SharedTimeSource>) -> &mut Self {
206        self.time_source = source;
207        self
208    }
209
210    /// Sets the sleep implementation used by pool maintenance.
211    pub fn sleep_impl(mut self, sleep: impl AsyncSleep + 'static) -> Self {
212        self.sleep_impl = Some(SharedAsyncSleep::new(sleep));
213        self
214    }
215
216    /// Mutably sets the sleep implementation used by pool maintenance.
217    pub fn set_sleep_impl(&mut self, sleep: Option<SharedAsyncSleep>) -> &mut Self {
218        self.sleep_impl = sleep;
219        self
220    }
221
222    /// Configures `TCP_NODELAY` for newly created sockets.
223    pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
224        self.tcp_nodelay = enabled;
225        self
226    }
227
228    /// Mutably configures `TCP_NODELAY`.
229    pub fn set_tcp_nodelay(&mut self, enabled: bool) -> &mut Self {
230        self.tcp_nodelay = enabled;
231        self
232    }
233
234    /// Configures TCP keepalive for newly created sockets.
235    pub fn tcp_keepalive(mut self, time: impl Into<Option<Duration>>) -> Self {
236        self.tcp_keepalive = match time.into() {
237            Some(time) => TriStateOption::Set(time),
238            None => TriStateOption::ExplicitlyUnset,
239        };
240        self
241    }
242
243    /// Mutably configures TCP keepalive.
244    pub fn set_tcp_keepalive(&mut self, time: Option<Option<Duration>>) -> &mut Self {
245        self.tcp_keepalive = TriStateOption::from_nested(time);
246        self
247    }
248
249    /// Bounds connection capacity for one origin across every pool partition.
250    ///
251    /// An origin is a scheme, host, and port, so `https://example.com` and
252    /// `http://example.com` are bounded separately, as is each non-default
253    /// port. Establishing, idle, and active connections occupy capacity.
254    /// Logical close normally returns capacity while accepted work drains.
255    /// An HTTP/1 upgrade retains capacity until the caller releases upgraded
256    /// I/O. This is not an idle-connection limit.
257    pub fn max_connections_per_host(mut self, limit: usize) -> Self {
258        self.max_connections_per_host = Some(limit);
259        self
260    }
261
262    /// Mutably sets the connection-capacity bound described by
263    /// [`Builder::max_connections_per_host`].
264    pub fn set_max_connections_per_host(&mut self, limit: Option<usize>) -> &mut Self {
265        self.max_connections_per_host = limit;
266        self
267    }
268
269    /// Selects which partitions may reuse each other's connections.
270    pub fn connection_reuse_scope(mut self, scope: ConnectionReuseScope) -> Self {
271        self.reuse_scope = scope;
272        self
273    }
274
275    /// Mutably selects the connection reuse scope.
276    pub fn set_connection_reuse_scope(&mut self, scope: ConnectionReuseScope) -> &mut Self {
277        self.reuse_scope = scope;
278        self
279    }
280
281    /// Returns whether an explicit partition set is configured.
282    ///
283    /// An explicitly supplied empty set also returns `true`. Clearing the set
284    /// with [`Self::set_partitions`] and `None` restores anonymous partition
285    /// construction.
286    pub fn has_explicit_partitions(&self) -> bool {
287        self.partitions.is_some()
288    }
289
290    /// Declares the complete explicit partition set.
291    pub fn partitions(mut self, partitions: impl IntoIterator<Item = Partition>) -> Self {
292        self.partitions = Some(partitions.into_iter().collect());
293        self
294    }
295
296    /// Mutably declares or clears the explicit partition set.
297    pub fn set_partitions(&mut self, partitions: Option<Vec<Partition>>) -> &mut Self {
298        self.partitions = partitions;
299        self
300    }
301
302    /// Configures proxy selection for every connection established by this pool.
303    ///
304    /// The configuration applies across every partition. Proxying is disabled
305    /// unless this method is called.
306    pub fn proxy_config(mut self, config: ProxyConfig) -> Self {
307        self.proxy_config = config;
308        self
309    }
310
311    /// Mutably configures pool-wide proxy selection.
312    ///
313    /// Passing `None` disables proxying.
314    pub fn set_proxy_config(&mut self, config: Option<ProxyConfig>) -> &mut Self {
315        self.proxy_config = config.unwrap_or_else(ProxyConfig::disabled);
316        self
317    }
318
319    /// Configures the DNS resolver used by connections established by this pool.
320    ///
321    /// The resolver applies across every partition. When unset, the client uses
322    /// its default resolver, which currently delegates to the operating system.
323    pub fn dns_resolver(mut self, resolver: impl ResolveDns + 'static) -> Self {
324        self.dns_resolver = Some(resolver.into_shared());
325        self
326    }
327
328    /// Mutably configures the pool-wide DNS resolver.
329    ///
330    /// Passing `None` restores the client default.
331    pub fn set_dns_resolver(&mut self, resolver: Option<SharedDnsResolver>) -> &mut Self {
332        self.dns_resolver = resolver;
333        self
334    }
335
336    /// Observes connection establishment and installed-lifetime transitions.
337    ///
338    /// The listener applies to every partition. Each callback runs
339    /// synchronously on the task that completed the transition and after pool
340    /// locks are released. A listener must not block on work that requires
341    /// progress from that same task.
342    ///
343    /// Observation is disabled unless a listener is configured.
344    pub fn event_listener(mut self, listener: impl ConnectionEventListener) -> Self {
345        self.event_listener = Some(SharedConnectionEventListener::new(listener));
346        self
347    }
348
349    /// Mutably configures pool-wide connection lifecycle observation.
350    ///
351    /// Passing `None` disables observation. The callback contract is the same
352    /// as [`Builder::event_listener`].
353    pub fn set_event_listener(
354        &mut self,
355        listener: Option<SharedConnectionEventListener>,
356    ) -> &mut Self {
357        self.event_listener = listener;
358        self
359    }
360}
361
362impl Builder<TlsUnset> {
363    /// Selects the TLS provider used by HTTPS pool connections.
364    pub fn tls_provider(self, provider: tls::Provider) -> Builder<TlsProviderSelected> {
365        Builder {
366            idle_timeout: self.idle_timeout,
367            time_source: self.time_source,
368            sleep_impl: self.sleep_impl,
369            tcp_nodelay: self.tcp_nodelay,
370            tcp_keepalive: self.tcp_keepalive,
371            max_connections_per_host: self.max_connections_per_host,
372            reuse_scope: self.reuse_scope,
373            partitions: self.partitions,
374            proxy_config: self.proxy_config,
375            dns_resolver: self.dns_resolver,
376            event_listener: self.event_listener,
377            tls: TlsProviderSelected {
378                provider,
379                context: TlsContext::default(),
380            },
381        }
382    }
383}
384
385impl Builder<TlsProviderSelected> {
386    /// Builds a pool whose connector performs TLS and ALPN negotiation.
387    pub fn build_https(self) -> Result<ConnectionPool, BuildError> {
388        #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
389        {
390            self.build_https_enabled()
391        }
392        #[cfg(not(any(feature = "__rustls", feature = "s2n-tls")))]
393        {
394            // Provider has no variants here; exhaustiveness is compiler-checked.
395            match self.tls.provider {}
396        }
397    }
398}
399
400crate::cfg::cfg_tls! {
401    impl Builder<TlsProviderSelected> {
402        /// Sets provider-specific TLS configuration for HTTPS connections.
403        pub fn tls_context(mut self, context: TlsContext) -> Self {
404            self.tls.context = context;
405            self
406        }
407
408        /// Mutably sets provider-specific TLS configuration.
409        pub fn set_tls_context(&mut self, context: TlsContext) -> &mut Self {
410            self.tls.context = context;
411            self
412        }
413
414        /// Selects the resolver for the HTTPS transport.
415        fn build_https_enabled(self) -> Result<ConnectionPool, BuildError> {
416            let resolver = self.dns_resolver.clone();
417            match resolver {
418                Some(resolver) => {
419                    self.build_https_with_resolver(HyperUtilResolver { resolver })
420                }
421                None => self.build_https_with_resolver(DefaultResolver(GaiResolver::new())),
422            }
423        }
424
425        /// Builds an HTTPS transport around one concrete Hyper resolver.
426        fn build_https_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
427        where
428            R: tower::Service<Name> + Clone + Send + Sync + 'static,
429            R::Response: Iterator<Item = SocketAddr>,
430            R::Future: Send,
431            R::Error: Into<Box<dyn Error + Send + Sync>>,
432        {
433            validate_default_connector_interfaces(self.partitions.as_deref())?;
434            let provider = self.tls.provider.clone();
435            let context = self.tls.context.clone();
436            let proxy_config = self.proxy_config.clone();
437            match provider {
438                #[cfg(feature = "__rustls")]
439                tls::Provider::Rustls(crypto_mode) => {
440                    let tcp_nodelay = self.tcp_nodelay;
441                    let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
442                    let transport = establish::from_cached_interface_connector(
443                        move |interface, alpn_protocols| {
444                            let mut connector =
445                                HttpConnector::new_with_resolver(resolver.clone());
446                            connector.set_nodelay(tcp_nodelay);
447                            connector.set_keepalive(tcp_keepalive);
448                            set_default_connector_interface(&mut connector, interface);
449                            tls::rustls_provider::build_connector::wrap_connector_with_alpn(
450                                connector,
451                                crypto_mode.clone(),
452                                &context,
453                                proxy_config.clone(),
454                                alpn_protocols,
455                            )
456                        },
457                        true,
458                    );
459                    self.build_with_transport(transport)
460                }
461                #[cfg(feature = "s2n-tls")]
462                tls::Provider::S2nTls => {
463                    let tcp_nodelay = self.tcp_nodelay;
464                    let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
465                    let transport = establish::from_cached_interface_connector(
466                        move |interface, _alpn_protocols| {
467                            let mut connector =
468                                HttpConnector::new_with_resolver(resolver.clone());
469                            connector.set_nodelay(tcp_nodelay);
470                            connector.set_keepalive(tcp_keepalive);
471                            set_default_connector_interface(&mut connector, interface);
472                            tls::s2n_tls_provider::build_connector::wrap_connector(
473                                connector,
474                                &context,
475                                proxy_config.clone(),
476                            )
477                        },
478                        false,
479                    );
480                    self.build_with_transport(transport)
481                }
482            }
483        }
484    }
485}
486
487/// Reports a hyper-util resolver's failures as `ResolveDnsError`, so they are classified like
488/// a custom resolver's, as I/O.
489///
490/// The pool wraps its default resolver, `GaiResolver`, in this. getaddrinfo's error does not say
491/// which failures are permanent, so every DNS failure is retried.
492#[derive(Clone)]
493struct DefaultResolver<R>(R);
494
495impl<R> Service<Name> for DefaultResolver<R>
496where
497    R: Service<Name>,
498    R::Error: Into<Box<dyn Error + Send + Sync>>,
499    R::Future: Send + 'static,
500{
501    type Response = R::Response;
502    type Error = ResolveDnsError;
503    type Future = Pin<Box<dyn Future<Output = Result<R::Response, ResolveDnsError>> + Send>>;
504
505    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
506        self.0.poll_ready(cx).map_err(ResolveDnsError::new)
507    }
508
509    fn call(&mut self, name: Name) -> Self::Future {
510        let resolving = self.0.call(name);
511        Box::pin(async move { resolving.await.map_err(ResolveDnsError::new) })
512    }
513}
514
515impl Builder<TlsUnset> {
516    /// Builds a pool for cleartext HTTP connections.
517    ///
518    /// This method remains public for explicit cleartext endpoints and tests,
519    /// but is hidden from generated documentation because HTTPS with automatic
520    /// ALPN negotiation is the intended production entry point.
521    #[doc(hidden)]
522    pub fn build_http(self) -> Result<ConnectionPool, BuildError> {
523        let resolver = self.dns_resolver.clone();
524        match resolver {
525            Some(resolver) => self.build_http_with_resolver(HyperUtilResolver { resolver }),
526            None => self.build_http_with_resolver(DefaultResolver(GaiResolver::new())),
527        }
528    }
529
530    /// Builds a cleartext transport around one concrete Hyper resolver.
531    fn build_http_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
532    where
533        R: tower::Service<Name> + Clone + Send + Sync + 'static,
534        R::Response: Iterator<Item = SocketAddr>,
535        R::Future: Send,
536        R::Error: Into<Box<dyn Error + Send + Sync>>,
537    {
538        validate_default_connector_interfaces(self.partitions.as_deref())?;
539        let tcp_nodelay = self.tcp_nodelay;
540        let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
541        let proxy_config = self.proxy_config.clone();
542        let transport = if proxy_config.is_disabled() {
543            establish::from_interface_connector(move |interface| {
544                let mut connector = HttpConnector::new_with_resolver(resolver.clone());
545                connector.set_nodelay(tcp_nodelay);
546                connector.set_keepalive(tcp_keepalive);
547                set_default_connector_interface(&mut connector, interface);
548                connector
549            })
550        } else {
551            establish::from_interface_connector(move |interface| {
552                let mut connector = HttpConnector::new_with_resolver(resolver.clone());
553                connector.set_nodelay(tcp_nodelay);
554                connector.set_keepalive(tcp_keepalive);
555                set_default_connector_interface(&mut connector, interface);
556                HttpProxyConnector::new(connector, proxy_config.clone())
557            })
558        };
559        self.build_with_transport(transport)
560    }
561
562    /// Builds a pool around a test transport connector.
563    #[cfg(all(feature = "test-util", aws_sdk_unstable))]
564    #[doc(hidden)]
565    pub fn build_http_with_tcp_connector<C, IO>(
566        self,
567        connector: C,
568    ) -> Result<ConnectionPool, BuildError>
569    where
570        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
571        C::Error: Into<BoxError>,
572        C::Future: Send + 'static,
573        IO: hyper::rt::Read
574            + hyper::rt::Write
575            + hyper_util::client::legacy::connect::Connection
576            + Send
577            + Sync
578            + Unpin
579            + 'static,
580    {
581        self.build_with_transport(establish::from_connector(connector))
582    }
583
584    #[cfg(all(test, feature = "rt-tokio"))]
585    pub(super) fn build_with_connector<C, IO>(
586        self,
587        connector: C,
588    ) -> Result<ConnectionPool, BuildError>
589    where
590        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
591        C::Error: Into<BoxError>,
592        C::Future: Send + 'static,
593        IO: hyper::rt::Read
594            + hyper::rt::Write
595            + hyper_util::client::legacy::connect::Connection
596            + Send
597            + Sync
598            + Unpin
599            + 'static,
600    {
601        self.build_with_transport(establish::from_connector(connector))
602    }
603}
604
605impl<Tls> Builder<Tls> {
606    /// Builds a pool with a transport supplied by a pool-internal test.
607    #[cfg(test)]
608    pub(super) fn build_with_transport_for_test(
609        self,
610        transport: std::sync::Arc<dyn TransportFactory>,
611    ) -> Result<ConnectionPool, BuildError> {
612        self.build_with_transport(transport)
613    }
614
615    /// Validates pool policy and installs the type-erased transport factory.
616    fn build_with_transport(
617        self,
618        transport: std::sync::Arc<dyn TransportFactory>,
619    ) -> Result<ConnectionPool, BuildError> {
620        let max_connections_per_host = match self.max_connections_per_host {
621            Some(0) => {
622                return Err(BuildError::new(
623                    "max_connections_per_host must be greater than zero",
624                ))
625            }
626            Some(limit) => NonZeroUsize::new(limit),
627            None => None,
628        };
629        let idle_timeout = self.idle_timeout.resolve(Some(DEFAULT_IDLE_TIMEOUT));
630        let time_source = self.time_source.unwrap_or_default();
631        let sleep_impl = self.sleep_impl.or_else(default_async_sleep);
632        if idle_timeout.is_some() && sleep_impl.is_none() {
633            return Err(BuildError::new(
634                "idle_timeout requires an async sleep implementation",
635            ));
636        }
637        let maintenance = MaintenanceConfig {
638            idle_timeout,
639            time_source: time_source.clone(),
640            sleep: sleep_impl.clone(),
641        };
642        let admission_policy = max_connections_per_host
643            .map(|limit| AdmissionPolicy::new(limit, transport.can_guarantee_http1()));
644        let registry = PartitionRegistry::new(
645            self.partitions,
646            self.reuse_scope,
647            admission_policy,
648            maintenance,
649        )
650        .map_err(BuildError::from)?;
651        let config = PoolConfig {
652            idle_timeout,
653            max_connections_per_host,
654            reuse_scope: self.reuse_scope,
655        };
656        Ok(ConnectionPool {
657            inner: Arc::new(PoolInner {
658                config,
659                registry,
660                transport,
661                connection_events: ConnectionEvents::new(self.event_listener, time_source),
662                next_connection_id: AtomicU64::new(0),
663            }),
664        })
665    }
666}
667
668/// Error returned when a pool configuration is internally inconsistent.
669#[derive(Debug)]
670pub struct BuildError {
671    /// Stable configuration diagnostic.
672    message: String,
673}
674
675impl BuildError {
676    fn new(message: impl Into<String>) -> Self {
677        Self {
678            message: message.into(),
679        }
680    }
681}
682
683impl fmt::Display for BuildError {
684    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
685        self.message.fmt(f)
686    }
687}
688
689impl Error for BuildError {}
690
691impl From<PartitionRegistryError> for BuildError {
692    fn from(error: PartitionRegistryError) -> Self {
693        Self::new(error.to_string())
694    }
695}
696
697/// Rejects interface values the default connector cannot convert to its
698/// platform socket representation.
699#[cfg(any(
700    target_os = "android",
701    target_os = "fuchsia",
702    target_os = "illumos",
703    target_os = "ios",
704    target_os = "linux",
705    target_os = "macos",
706    target_os = "solaris",
707    target_os = "tvos",
708    target_os = "visionos",
709    target_os = "watchos",
710))]
711fn validate_default_connector_interfaces(
712    partitions: Option<&[Partition]>,
713) -> Result<(), BuildError> {
714    if let Some(partition) = partitions.into_iter().flatten().find(|partition| {
715        partition
716            .interface_name()
717            .is_some_and(|interface| interface.as_bytes().contains(&0))
718    }) {
719        return Err(BuildError::new(format!(
720            "partition {:?} has an invalid network-interface name",
721            partition.id()
722        )));
723    }
724    Ok(())
725}
726
727/// Performs no interface validation on targets without connector support.
728#[cfg(not(any(
729    target_os = "android",
730    target_os = "fuchsia",
731    target_os = "illumos",
732    target_os = "ios",
733    target_os = "linux",
734    target_os = "macos",
735    target_os = "solaris",
736    target_os = "tvos",
737    target_os = "visionos",
738    target_os = "watchos",
739)))]
740fn validate_default_connector_interfaces(
741    _partitions: Option<&[Partition]>,
742) -> Result<(), BuildError> {
743    Ok(())
744}
745
746/// Applies this partition's interface to the default connector.
747#[cfg(any(
748    target_os = "android",
749    target_os = "fuchsia",
750    target_os = "illumos",
751    target_os = "ios",
752    target_os = "linux",
753    target_os = "macos",
754    target_os = "solaris",
755    target_os = "tvos",
756    target_os = "visionos",
757    target_os = "watchos",
758))]
759fn set_default_connector_interface<R>(connector: &mut HttpConnector<R>, interface: Option<&str>) {
760    if let Some(interface) = interface {
761        connector.set_interface(interface);
762    }
763}
764
765/// Leaves the default connector unchanged on unsupported targets.
766#[cfg(not(any(
767    target_os = "android",
768    target_os = "fuchsia",
769    target_os = "illumos",
770    target_os = "ios",
771    target_os = "linux",
772    target_os = "macos",
773    target_os = "solaris",
774    target_os = "tvos",
775    target_os = "visionos",
776    target_os = "watchos",
777)))]
778fn set_default_connector_interface<R>(_connector: &mut HttpConnector<R>, _interface: Option<&str>) {
779}
780
781#[cfg(all(test, not(smithy_http_client_loom)))]
782mod tests {
783    use super::*;
784    use crate::client::pool::partition::Spawn;
785    use crate::client::pool::{DriverSpawner, PartitionId};
786    use std::future::Future;
787    use std::net::IpAddr;
788    use std::pin::Pin;
789
790    #[derive(Debug)]
791    struct PendingSleep;
792
793    impl AsyncSleep for PendingSleep {
794        fn sleep(&self, _duration: Duration) -> aws_smithy_async::rt::sleep::Sleep {
795            aws_smithy_async::rt::sleep::Sleep::new(std::future::pending())
796        }
797    }
798
799    #[derive(Debug)]
800    struct TestSpawner;
801
802    impl Spawn for TestSpawner {
803        fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
804            drop(driver);
805        }
806    }
807
808    #[derive(Debug)]
809    struct EmptyDnsResolver;
810
811    impl ResolveDns for EmptyDnsResolver {
812        fn resolve_dns<'a>(
813            &'a self,
814            _name: &'a str,
815        ) -> aws_smithy_runtime_api::client::dns::DnsFuture<'a> {
816            aws_smithy_runtime_api::client::dns::DnsFuture::ready(Ok(Vec::<IpAddr>::new()))
817        }
818    }
819
820    fn partition(index: usize) -> Partition {
821        Partition::new(
822            PartitionId::from_index(index),
823            DriverSpawner::new(TestSpawner),
824        )
825    }
826
827    #[test]
828    fn builder_preserves_unset_disabled_and_configured_settings() {
829        let mut builder = Builder::default();
830        assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
831        assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
832        assert!(builder.tcp_nodelay);
833        assert_eq!(None, builder.max_connections_per_host);
834        assert!(builder.partitions.is_none());
835        assert!(builder.proxy_config.is_disabled());
836        assert!(builder.dns_resolver.is_none());
837        assert!(builder.event_listener.is_none());
838
839        builder.set_idle_timeout(Some(None));
840        builder.set_tcp_keepalive(Some(None));
841        assert_eq!(TriStateOption::ExplicitlyUnset, builder.idle_timeout);
842        assert_eq!(TriStateOption::ExplicitlyUnset, builder.tcp_keepalive);
843
844        builder.set_idle_timeout(Some(Some(Duration::from_secs(5))));
845        builder.set_tcp_keepalive(Some(Some(Duration::from_secs(10))));
846        builder.set_tcp_nodelay(false);
847        builder.set_max_connections_per_host(Some(3));
848        builder.set_partitions(Some(vec![partition(7)]));
849        assert_eq!(
850            TriStateOption::Set(Duration::from_secs(5)),
851            builder.idle_timeout
852        );
853        assert_eq!(
854            TriStateOption::Set(Duration::from_secs(10)),
855            builder.tcp_keepalive
856        );
857        assert!(!builder.tcp_nodelay);
858        assert_eq!(Some(3), builder.max_connections_per_host);
859        assert_eq!(1, builder.partitions.as_ref().unwrap().len());
860
861        builder.set_idle_timeout(None);
862        builder.set_tcp_keepalive(None);
863        builder.set_max_connections_per_host(None);
864        builder.set_partitions(None);
865        assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
866        assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
867        assert_eq!(None, builder.max_connections_per_host);
868        assert!(builder.partitions.is_none());
869    }
870
871    #[test]
872    fn proxy_and_dns_settings_set_reset_and_redact() {
873        let mut builder = Builder::default();
874        builder.set_proxy_config(Some(
875            ProxyConfig::http("http://proxy.test:8080")
876                .unwrap()
877                .with_basic_auth("secret-user", "secret-password"),
878        ));
879        builder.set_dns_resolver(Some(SharedDnsResolver::new(EmptyDnsResolver)));
880
881        assert!(!builder.proxy_config.is_disabled());
882        assert!(builder.dns_resolver.is_some());
883        let debug = format!("{builder:?}");
884        assert!(debug.contains("configured"));
885        assert!(debug.contains("custom"));
886        assert!(!debug.contains("secret-user"));
887        assert!(!debug.contains("secret-password"));
888
889        builder.set_proxy_config(None);
890        builder.set_dns_resolver(None);
891        assert!(builder.proxy_config.is_disabled());
892        assert!(builder.dns_resolver.is_none());
893    }
894
895    #[test]
896    fn event_listener_sets_resets_and_survives_tls_selection() {
897        let observed = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
898        let mut builder = Builder::default().event_listener({
899            let observed = observed.clone();
900            move |_: &super::super::ConnectionEvent<'_>| {
901                observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
902            }
903        });
904        assert!(builder.event_listener.is_some());
905
906        let shared = builder.event_listener.clone();
907        builder.set_event_listener(None);
908        assert!(builder.event_listener.is_none());
909        builder.set_event_listener(shared);
910        assert!(builder.event_listener.is_some());
911
912        #[cfg(feature = "rustls-ring")]
913        let builder = builder.tls_provider(tls::Provider::rustls(
914            tls::rustls_provider::CryptoMode::Ring,
915        ));
916        assert!(builder.event_listener.is_some());
917        assert_eq!(0, observed.load(std::sync::atomic::Ordering::Relaxed));
918    }
919
920    #[cfg(feature = "rustls-ring")]
921    #[test]
922    fn tls_selection_preserves_proxy_and_dns_settings() {
923        let builder = Builder::default()
924            .proxy_config(
925                ProxyConfig::http("http://proxy.test:8080")
926                    .unwrap()
927                    .with_basic_auth("secret-user", "secret-password"),
928            )
929            .dns_resolver(EmptyDnsResolver)
930            .tls_provider(tls::Provider::rustls(
931                tls::rustls_provider::CryptoMode::Ring,
932            ));
933
934        assert!(!builder.proxy_config.is_disabled());
935        assert!(builder.dns_resolver.is_some());
936        let debug = format!("{builder:?}");
937        assert!(debug.contains("configured"));
938        assert!(debug.contains("custom"));
939        assert!(!debug.contains("secret-user"));
940        assert!(!debug.contains("secret-password"));
941    }
942
943    #[test]
944    fn terminal_build_resolves_the_default_idle_timeout() {
945        let pool = Builder::default()
946            .sleep_impl(PendingSleep)
947            .build_http()
948            .unwrap();
949
950        assert_eq!(
951            Some(Duration::from_secs(90)),
952            pool.inner.config.idle_timeout
953        );
954    }
955
956    #[test]
957    fn terminal_build_preserves_explicitly_disabled_idle_timeout() {
958        let pool = Builder::default().idle_timeout(None).build_http().unwrap();
959
960        assert_eq!(None, pool.inner.config.idle_timeout);
961    }
962
963    #[test]
964    #[cfg(any(
965        target_os = "android",
966        target_os = "fuchsia",
967        target_os = "illumos",
968        target_os = "ios",
969        target_os = "linux",
970        target_os = "macos",
971        target_os = "solaris",
972        target_os = "tvos",
973        target_os = "visionos",
974        target_os = "watchos",
975    ))]
976    fn terminal_build_rejects_interface_names_with_nul_bytes() {
977        let error = Builder::default()
978            .partitions([partition(7).interface("eth\0invalid")])
979            .build_http()
980            .unwrap_err();
981        assert_eq!(
982            "partition PartitionId(7) has an invalid network-interface name",
983            error.to_string()
984        );
985    }
986
987    #[test]
988    fn terminal_build_rejects_zero_connection_limit() {
989        let error = Builder::default()
990            .max_connections_per_host(0)
991            .build_http()
992            .unwrap_err();
993        assert_eq!(
994            "max_connections_per_host must be greater than zero",
995            error.to_string()
996        );
997    }
998
999    #[test]
1000    fn terminal_build_rejects_empty_explicit_partition_set() {
1001        let error = Builder::default().partitions([]).build_http().unwrap_err();
1002        assert_eq!(
1003            "explicit partition set must not be empty",
1004            error.to_string()
1005        );
1006    }
1007
1008    #[test]
1009    fn terminal_build_rejects_duplicate_partition_ids() {
1010        let error = Builder::default()
1011            .partitions([partition(7), partition(7)])
1012            .build_http()
1013            .unwrap_err();
1014        assert_eq!(
1015            "duplicate partition identifier: PartitionId(7)",
1016            error.to_string()
1017        );
1018    }
1019
1020    #[test]
1021    fn terminal_build_rejects_reserved_anonymous_partition_id() {
1022        let error = Builder::default()
1023            .partitions([Partition::new(
1024                PartitionId::ANONYMOUS,
1025                DriverSpawner::new(TestSpawner),
1026            )])
1027            .build_http()
1028            .unwrap_err();
1029        assert_eq!(
1030            "the anonymous partition identifier is reserved",
1031            error.to_string()
1032        );
1033    }
1034
1035    /// A resolver that fails every lookup with an `io::Error`.
1036    #[derive(Clone)]
1037    struct FailingResolver;
1038
1039    impl Service<Name> for FailingResolver {
1040        type Response = std::vec::IntoIter<SocketAddr>;
1041        type Error = std::io::Error;
1042        type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
1043
1044        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1045            Poll::Ready(Ok(()))
1046        }
1047
1048        fn call(&mut self, name: Name) -> Self::Future {
1049            std::future::ready(Err(std::io::Error::other(format!(
1050                "no addresses for {name}"
1051            ))))
1052        }
1053    }
1054
1055    #[tokio::test]
1056    async fn default_resolver_failure_through_http_connector_is_io() {
1057        let mut connector = HttpConnector::new_with_resolver(DefaultResolver(FailingResolver));
1058
1059        let error = connector
1060            .call(http_1x::Uri::from_static("http://example.com/"))
1061            .await
1062            .expect_err("DNS resolution fails");
1063
1064        let error = crate::client::downcast_error(error.into());
1065        assert!(error.is_io(), "expected an I/O error, got {error:?}");
1066    }
1067}