aws_smithy_http_client/client/pool.rs
1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Connection pooling with explicit runtime and network placement.
7//!
8//! A [`ConnectionPool`] owns connection policy and a fixed set of partitions.
9//! A [`Client`] binds Smithy HTTP operations to one partition. Pools without
10//! explicit [`Partition`] values contain one anonymous partition; otherwise a
11//! client selects a declared partition by [`PartitionId`].
12//!
13//! A partition owns connection establishment, protocol drivers, and idle
14//! maintenance. [`ConnectionReuseScope`] controls whether another partition may
15//! dispatch through those connections. Reuse transfers protocol dispatch
16//! authority; it never moves the socket, driver, or capacity accounting.
17//! Partitions in the same eligibility group are exactly those whose configured
18//! reuse scope permits them to share a connection.
19//!
20//! Connection establishment and installed connection lifetime are separate
21//! ownership phases:
22//!
23//! ```text
24//! establishment task
25//! |-- DNS, socket, proxy, TLS, and ALPN
26//! `-- negotiated transport
27//! `-- PendingOpen
28//! |-- Hyper protocol setup
29//! `-- Open
30//! `-- pool installation
31//! `-- discoverable -> draining -> closed
32//! ```
33//!
34//! The establishment task and its permit represent work that may fail before a
35//! physical connection exists. `ConnectionState` begins only after the
36//! connector returns connected I/O and selects HTTP/1 or HTTP/2. For TLS, that
37//! boundary follows TLS and ALPN. Hyper protocol setup moves the state from
38//! `PendingOpen` to `Open`; cell installation then makes the connection
39//! discoverable. Establishment and connection events can therefore be observed
40//! independently without representing failed attempts as installed
41//! connections.
42//!
43//! # State ownership
44//!
45//! ```text
46//! ConnectionPool
47//! `-- PoolInner
48//! |-- immutable policy and transport factory
49//! `-- PartitionRegistry
50//! |-- PartitionState per partition
51//! | |-- runtime placement and idle maintenance
52//! | `-- OriginCell per canonical origin
53//! | |-- acquisition queue and supply revisions
54//! | |-- H1 connection ownership
55//! | `-- H2 flights, generations, routes, and gates
56//! `-- OriginAdmission per bounded origin
57//! |-- capacity budget
58//! |-- demand schedule
59//! |-- H1 supply index and retained matches
60//! `-- H2 supply index and route/reclaim state
61//! ```
62//!
63//! One `OriginCell` lock owns local acquisition order and protocol state.
64//! For a bounded origin, `OriginAdmission` separately owns the origin-wide
65//! connection limit and cross-cell matching. An H2 request-claim lock records
66//! independent upload and response completion. `ConnectionState` owns logical
67//! connection lifetime, and partition maintenance owns its timer state.
68//!
69//! No two pool locks are held together. Demand snapshots and supply revisions
70//! move cell state into admission. Assignments and detached guards carry one
71//! selected payload or route back toward a cell. H2 claim completion detaches
72//! its dispatch guard before entering connection or cell state.
73//! Maintenance detaches cells and wakers before expiration or wake callbacks.
74//!
75//! # HTTP/1 request lifecycle
76//!
77//! Hyper represents an HTTP/1 connection with one exclusive
78//! `SendRequest<SdkBody>` handle. This module calls that handle the sender. The
79//! sender authorizes request dispatch but does not own the socket or protocol
80//! driver.
81//!
82//! ```text
83//! Client(partition, request)
84//! `-- OriginCell(partition, origin)
85//! |-- local idle sender --------------------------> H1Selection
86//! `-- acquisition queue
87//! |-- returned local or eligible peer sender -> H1Selection
88//! |-- capacity permit -> establish HTTP/1 ---> H1Selection
89//! `-- reclaimed peer capacity -> establish ---> H1Selection
90//!
91//! H1Selection -- Hyper accepts request --> H1Exchange
92//! H1Exchange
93//! |-- complete response + ready --> offer to owning OriginCell
94//! `-- failure, cancellation, or upgrade ----------> retire pool record
95//! ```
96//!
97//! A local hit touches only the cell lock. On a miss, one queued acquisition
98//! remains authoritative while a returned sender and establishment race to
99//! satisfy it. Bounded origins may borrow an eligible peer sender or reclaim a
100//! peer connection and transfer its permit; active connections are not
101//! reclaimed.
102//!
103//! Dispatch commits against logical close before Hyper receives the request.
104//! Once accepted, the response lifecycle retains the sender until Hyper proves
105//! a complete reusable message boundary. Failure retires the connection, and
106//! an upgrade closes the pool record before exposing upgraded root I/O.
107//!
108//! # HTTP/2 request lifecycle
109//!
110//! One HTTP/2 connection carries many concurrent request streams. The pool
111//! calls one installed incarnation of that connection a generation. A
112//! replacement connection receives a new generation identity so delayed
113//! close, route, and completion work cannot affect it.
114//!
115//! The connection-owning cell retains the generation's authoritative Hyper
116//! request handle and capacity. A requesting cell may retain only a route that
117//! names the owning cell and one exact accepting generation. Each use
118//! revalidates that route before cloning a transient request handle.
119//!
120//! ```text
121//! Client(partition, request)
122//! `-- OriginCell(partition, origin)
123//! |-- local accepting generation ----------------> H2Activation
124//! `-- acquisition queue
125//! |-- local flight result --------------------> H2Activation
126//! |-- eligible peer generation route --------> H2Activation
127//! `-- capacity permit -> connect + ALPN
128//! |-- HTTP/2 -> join or drive one flight -> H2Activation
129//! `-- HTTP/1 -> H1Selection or incompatible-version error
130//!
131//! H2Activation -- Hyper accepts request --> H2RequestClaim
132//! H2RequestClaim
133//! |-- request body ends or drops -----> upload side complete
134//! `-- response body ends or drops ----> response side complete
135//! both sides complete --------------------> release generation request count
136//! ```
137//!
138//! `H2Activation` reserves pool accounting for a prospective stream on one
139//! exact generation. It is not yet an HTTP/2 stream. Dropping it before Hyper
140//! accepts the request returns its generation-gate turn and request count.
141//! Acceptance creates two independent completion sides because upload and response
142//! can finish in either order. Logical close stops new activations and releases
143//! bounded capacity; accepted streams retain the draining generation until
144//! both sides end. Hyper remains responsible for stream identifiers,
145//! stream credit, and flow control.
146//!
147//! A peer route moves only generation identity. The socket, protocol driver,
148//! request handle, and capacity remain with the connection-owning partition.
149//!
150//! `ConnectionState` separates logical close, accepted-request accounting, and
151//! physical connection ownership. Logical close rejects new dispatch and
152//! normally releases bounded capacity while accepted work drains. An HTTP/1
153//! upgrade retains capacity until upgraded root I/O leaves the client.
154//! `DispatchGuard` follows an accepted request, while
155//! `PhysicalConnectionGuard` follows root I/O until the client releases its
156//! transport handle. The operating system may continue TCP teardown afterward.
157//! All connection-owned work runs through the partition
158//! [`DriverSpawner`].
159//!
160//! # Observation
161//!
162//! A pool-wide [`ConnectionEventListener`] observes establishment failure,
163//! successful installation, logical close, and release of the client's root
164//! transport handle. Callbacks run synchronously after pool locks are released.
165//! Observation is disabled unless a listener is configured.
166//!
167//! [`ConnectionPool::origin_stats`] reports the bounded capacity shared by all
168//! partitions for one canonical origin. [`ConnectionPool::partition_stats`]
169//! reports request acquisition and connection state for one exact
170//! partition-origin cell. These snapshots are diagnostic; the pool does not use
171//! them for admission, reuse, reclaim, routing, or dispatch.
172
173#![cfg_attr(
174 smithy_http_client_loom,
175 allow(
176 dead_code,
177 reason = "Loom builds replace runtime and transport paths with focused coordination models"
178 )
179)]
180
181mod admission;
182mod builder;
183mod cell;
184mod client;
185mod connection;
186mod dispatch;
187mod establish;
188mod events;
189mod maintenance;
190mod origin;
191mod partition;
192mod registry;
193mod stats;
194
195pub use builder::{BuildError, Builder};
196pub use client::{Client, ClientBuildError};
197pub use connection::{CloseReason, ConnectionId, ConnectionInfo, ConnectionProtocol};
198pub use events::{
199 ConnectionEstablishmentFailed, ConnectionEstablishmentId, ConnectionEstablishmentInfo,
200 ConnectionEstablishmentStage, ConnectionEstablishmentStats, ConnectionEvent,
201 ConnectionEventListener, ConnectionLogicalClose, ConnectionOpened, ConnectionPhysicalClose,
202 LogicalCloseCause, SharedConnectionEventListener,
203};
204pub use origin::{InvalidOrigin, OriginKey};
205pub use partition::{ConnectionReuseScope, DriverSpawner, Partition, PartitionId};
206pub use stats::{
207 ConnectionCapacityStats, Http1ConnectionStats, Http2ConnectionStats, OriginConnectionStats,
208 PartitionConnectionStats,
209};
210
211pub use crate::client::connect::ConnectPath;
212use crate::sync::Arc;
213use aws_smithy_runtime_api::client::result::ConnectorError;
214use aws_smithy_types::body::SdkBody;
215use establish::TransportFactory;
216use http_1x::{Request, Response};
217use registry::{PartitionRegistry, PartitionState};
218use std::fmt;
219use std::num::NonZeroUsize;
220use std::sync::atomic::AtomicU64;
221use std::sync::Arc as StdArc;
222use std::time::Duration;
223
224/// Shared connection topology and pooling policy.
225///
226/// Construct a pool with [`ConnectionPool::builder`], then create [`Client`]
227/// handles for its anonymous or explicit partitions. The pool itself owns no
228/// request placement; each client supplies that partition choice. Cloning a
229/// pool or client shares all retained connections and admission state.
230///
231/// A pool built with explicit partitions does not also create an anonymous
232/// partition. Dropping the final shared owner logically closes retained
233/// connections and stops partition maintenance.
234#[derive(Clone)]
235pub struct ConnectionPool {
236 /// Shared pool policy, topology, connector, and connection-ID allocator.
237 inner: Arc<PoolInner>,
238}
239
240impl ConnectionPool {
241 /// Returns a builder for a new connection pool.
242 pub fn builder() -> Builder<super::TlsUnset> {
243 Builder::default()
244 }
245
246 /// Returns current bounded-capacity accounting for one canonical origin.
247 ///
248 /// The capacity limit applies across every partition. Capacity in use
249 /// includes establishments, open connections, and detached HTTP/1 upgrades
250 /// that still own root transport I/O. Ordinary draining connections are
251 /// excluded after returning their capacity.
252 ///
253 /// Unbounded origins return statistics without a capacity value. The query
254 /// does not create admission state for an unused origin.
255 pub fn origin_stats(&self, origin: &OriginKey) -> OriginConnectionStats {
256 self.inner.registry.origin_stats(origin)
257 }
258
259 /// Returns a diagnostic connection snapshot for one partition and origin.
260 ///
261 /// An unknown partition returns `None`. A configured partition without a
262 /// retained cell for `origin` returns zeroed statistics. Cell-owned values
263 /// are read under the existing cell lock. Counts for lifetimes that can
264 /// outlive cell records use relaxed atomics and converge after concurrent
265 /// transitions settle.
266 pub fn partition_stats(
267 &self,
268 partition: PartitionId,
269 origin: &OriginKey,
270 ) -> Option<PartitionConnectionStats> {
271 self.inner.registry.partition_stats(partition, origin)
272 }
273
274 /// Routes one request from its selected partition through pool dispatch.
275 pub(in crate::client::pool) async fn send_request(
276 &self,
277 partition: Arc<PartitionState>,
278 request: Request<SdkBody>,
279 options: dispatch::RequestOptions,
280 ) -> Result<Response<SdkBody>, ConnectorError> {
281 dispatch::send(self, partition, request, options).await
282 }
283}
284
285impl fmt::Debug for ConnectionPool {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 f.debug_struct("ConnectionPool")
288 .field("partitions", &self.inner.registry)
289 .field("idle_timeout", &self.inner.config.idle_timeout)
290 .field(
291 "max_connections_per_host",
292 &self.inner.config.max_connections_per_host,
293 )
294 .field("reuse_scope", &self.inner.config.reuse_scope)
295 .finish_non_exhaustive()
296 }
297}
298
299/// Immutable pool policy retained with the partition registry.
300#[derive(Clone, Debug)]
301struct PoolConfig {
302 /// Duration after which a reusable idle connection is retired.
303 idle_timeout: Option<Duration>,
304 /// Optional logical-connection bound shared by every partition for one origin.
305 max_connections_per_host: Option<NonZeroUsize>,
306 /// Partitions allowed to dispatch through one another's connections.
307 reuse_scope: ConnectionReuseScope,
308}
309
310/// Shared implementation state behind [`ConnectionPool`].
311struct PoolInner {
312 /// Immutable settings shared by pool operations.
313 config: PoolConfig,
314 /// Fixed partitions and lazily created per-origin state.
315 registry: PartitionRegistry,
316 /// Type-erased construction of one partition-bound transport.
317 transport: StdArc<dyn TransportFactory>,
318 /// Pool-wide connection lifecycle observation.
319 connection_events: events::ConnectionEvents,
320 /// Monotonic identity source shared by every physical connection.
321 next_connection_id: AtomicU64,
322}
323
324impl Drop for PoolInner {
325 fn drop(&mut self) {
326 self.registry.close_all(CloseReason::PoolDropped);
327 }
328}
329
330impl fmt::Debug for PoolInner {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 f.debug_struct("PoolInner")
333 .field("config", &self.config)
334 .field("registry", &self.registry)
335 .finish_non_exhaustive()
336 }
337}