Skip to main content

aws_smithy_http_client/client/pool/
stats.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Diagnostic connection state for one origin or partition-origin cell.
7//!
8//! Admission and cell state remain authoritative. These values combine exact
9//! observations under their existing locks with relaxed lifetime counts for
10//! work that can outlive a cell record. They must not be used to make pool
11//! admission or dispatch decisions.
12//!
13//! [`ConnectionPool::origin_stats`](super::ConnectionPool::origin_stats)
14//! reports the shared origin capacity budget.
15//! [`ConnectionPool::partition_stats`](super::ConnectionPool::partition_stats)
16//! reports one exact partition-origin cell without scanning other partitions.
17
18use std::sync::atomic::{AtomicUsize, Ordering};
19
20/// Bounded connection capacity shared by every partition for one origin.
21///
22/// Capacity is retained by establishment, open connections, and detached
23/// HTTP/1 upgrades. Ordinary draining connections return capacity when they
24/// stop accepting pool dispatch.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26#[non_exhaustive]
27pub struct ConnectionCapacityStats {
28    limit: usize,
29    in_use: usize,
30}
31
32impl ConnectionCapacityStats {
33    pub(super) fn new(limit: usize, in_use: usize) -> Self {
34        Self { limit, in_use }
35    }
36
37    /// Returns the configured maximum across all partitions for this origin.
38    pub fn limit(&self) -> usize {
39        self.limit
40    }
41
42    /// Returns capacity currently owned by establishment or connections.
43    pub fn in_use(&self) -> usize {
44        self.in_use
45    }
46}
47
48/// Origin-wide connection capacity.
49///
50/// This snapshot contains no aggregate of partition-local protocol state.
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52#[non_exhaustive]
53pub struct OriginConnectionStats {
54    capacity: Option<ConnectionCapacityStats>,
55}
56
57impl OriginConnectionStats {
58    pub(super) fn new(capacity: Option<ConnectionCapacityStats>) -> Self {
59        Self { capacity }
60    }
61
62    /// Returns bounded-origin capacity, or `None` when the origin is unbounded.
63    pub fn capacity(&self) -> Option<&ConnectionCapacityStats> {
64        self.capacity.as_ref()
65    }
66}
67
68/// HTTP/1 connection state owned by one partition-origin cell.
69///
70/// Idle and active counts are exact under the cell lock. Draining and upgraded
71/// counts use relaxed lifetime accounting because those connections can outlive
72/// their cell records.
73#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
74#[non_exhaustive]
75pub struct Http1ConnectionStats {
76    idle: usize,
77    active: usize,
78    draining: usize,
79    upgraded: usize,
80}
81
82impl Http1ConnectionStats {
83    /// Returns connections available for immediate local selection.
84    pub fn idle(&self) -> usize {
85        self.idle
86    }
87
88    /// Returns connections whose sender is selected or reserved outside idle storage.
89    ///
90    /// This includes senders checked out for local dispatch and senders reserved
91    /// for eligible peer demand.
92    pub fn active(&self) -> usize {
93        self.active
94    }
95
96    /// Returns ordinary logically closed connections that still own root I/O.
97    pub fn draining(&self) -> usize {
98        self.draining
99    }
100
101    /// Returns upgraded connections whose root transport I/O is caller-owned.
102    pub fn upgraded(&self) -> usize {
103        self.upgraded
104    }
105}
106
107/// HTTP/2 connection state owned by one partition-origin cell.
108///
109/// Accepting connections and active requests are exact under the cell lock.
110/// Draining connections use relaxed lifetime accounting because their root I/O
111/// can outlive the installed generation record.
112#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
113#[non_exhaustive]
114pub struct Http2ConnectionStats {
115    accepting: usize,
116    draining: usize,
117    active_requests: usize,
118}
119
120impl Http2ConnectionStats {
121    /// Returns installed connections that may accept new HTTP/2 requests.
122    pub fn accepting(&self) -> usize {
123        self.accepting
124    }
125
126    /// Returns logically closed connections that still own root transport I/O.
127    pub fn draining(&self) -> usize {
128        self.draining
129    }
130
131    /// Returns accepted requests whose upload and response sides have not both finished.
132    pub fn active_requests(&self) -> usize {
133        self.active_requests
134    }
135}
136
137/// Diagnostic connection state for one configured partition and origin.
138///
139/// This snapshot is not transactional across its exact cell-owned values and
140/// relaxed lifetime counts. The relaxed counts are nonnegative and converge
141/// after concurrent transitions settle.
142#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
143#[non_exhaustive]
144pub struct PartitionConnectionStats {
145    pending_acquisitions: usize,
146    establishing_connections: usize,
147    h1: Http1ConnectionStats,
148    h2: Http2ConnectionStats,
149    physically_live_connections: usize,
150}
151
152impl PartitionConnectionStats {
153    /// Returns requests without a terminal cell-local acquisition outcome.
154    pub fn pending_acquisitions(&self) -> usize {
155        self.pending_acquisitions
156    }
157
158    /// Returns connection establishments that have not reached a terminal outcome.
159    ///
160    /// The count spans connector, protocol selection, Hyper handshake, and pool
161    /// installation work.
162    pub fn establishing_connections(&self) -> usize {
163        self.establishing_connections
164    }
165
166    /// Returns HTTP/1 connection state.
167    pub fn h1(&self) -> &Http1ConnectionStats {
168        &self.h1
169    }
170
171    /// Returns HTTP/2 connection state.
172    pub fn h2(&self) -> &Http2ConnectionStats {
173        &self.h2
174    }
175
176    /// Returns root transport handles still owned by the client.
177    ///
178    /// The operating system may continue transport teardown after this count
179    /// decreases.
180    pub fn physically_live_connections(&self) -> usize {
181        self.physically_live_connections
182    }
183}
184
185/// Relaxed lifetime counts retained by one partition-origin cell.
186///
187/// These counts cover lifetimes that can outlive the corresponding cell
188/// record. Exact waiter and accepting-connection state remains under the cell
189/// lock and is supplied when a snapshot is composed.
190#[derive(Debug, Default)]
191pub(super) struct CellConnectionStats {
192    establishing: AtomicUsize,
193    h1_draining: AtomicUsize,
194    h1_upgraded: AtomicUsize,
195    h2_draining: AtomicUsize,
196    physically_live: AtomicUsize,
197}
198
199impl CellConnectionStats {
200    pub(super) fn establishment_started(&self) {
201        increment(
202            &self.establishing,
203            "connection establishment count exhausted",
204        );
205    }
206
207    pub(super) fn establishment_finished(&self) {
208        decrement(
209            &self.establishing,
210            "connection establishment completed without a matching start",
211        );
212    }
213
214    pub(super) fn physical_connection_started(&self) {
215        increment(&self.physically_live, "physical connection count exhausted");
216    }
217
218    pub(super) fn physical_connection_finished(&self) {
219        decrement(
220            &self.physically_live,
221            "physical connection completed without a matching start",
222        );
223    }
224
225    pub(super) fn h1_drain_started(&self) {
226        increment(&self.h1_draining, "HTTP/1 draining count exhausted");
227    }
228
229    pub(super) fn h1_drain_finished(&self) {
230        decrement(
231            &self.h1_draining,
232            "HTTP/1 drain completed without a matching start",
233        );
234    }
235
236    pub(super) fn h1_drain_upgraded(&self) {
237        self.h1_drain_finished();
238        increment(&self.h1_upgraded, "HTTP/1 upgraded count exhausted");
239    }
240
241    pub(super) fn h1_upgrade_started(&self) {
242        increment(&self.h1_upgraded, "HTTP/1 upgraded count exhausted");
243    }
244
245    pub(super) fn h1_upgrade_finished(&self) {
246        decrement(
247            &self.h1_upgraded,
248            "HTTP/1 upgrade completed without a matching start",
249        );
250    }
251
252    pub(super) fn h2_drain_started(&self) {
253        increment(&self.h2_draining, "HTTP/2 draining count exhausted");
254    }
255
256    pub(super) fn h2_drain_finished(&self) {
257        decrement(
258            &self.h2_draining,
259            "HTTP/2 drain completed without a matching start",
260        );
261    }
262
263    pub(super) fn snapshot(
264        &self,
265        pending_acquisitions: usize,
266        h1_idle: usize,
267        h1_active: usize,
268        h2_accepting: usize,
269        h2_active_requests: usize,
270    ) -> PartitionConnectionStats {
271        PartitionConnectionStats {
272            pending_acquisitions,
273            establishing_connections: self.establishing.load(Ordering::Relaxed),
274            h1: Http1ConnectionStats {
275                idle: h1_idle,
276                active: h1_active,
277                draining: self.h1_draining.load(Ordering::Relaxed),
278                upgraded: self.h1_upgraded.load(Ordering::Relaxed),
279            },
280            h2: Http2ConnectionStats {
281                accepting: h2_accepting,
282                draining: self.h2_draining.load(Ordering::Relaxed),
283                active_requests: h2_active_requests,
284            },
285            physically_live_connections: self.physically_live.load(Ordering::Relaxed),
286        }
287    }
288}
289
290fn increment(counter: &AtomicUsize, message: &'static str) {
291    counter
292        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
293            value.checked_add(1)
294        })
295        .expect(message);
296}
297
298fn decrement(counter: &AtomicUsize, message: &'static str) {
299    counter
300        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
301            value.checked_sub(1)
302        })
303        .expect(message);
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn snapshot_combines_exact_and_lifetime_state() {
312        let stats = CellConnectionStats::default();
313        stats.establishment_started();
314        stats.physical_connection_started();
315        stats.h1_drain_started();
316        stats.h1_drain_upgraded();
317        stats.h2_drain_started();
318
319        let snapshot = stats.snapshot(2, 3, 4, 5, 6);
320
321        assert_eq!(2, snapshot.pending_acquisitions());
322        assert_eq!(1, snapshot.establishing_connections());
323        assert_eq!(3, snapshot.h1().idle());
324        assert_eq!(4, snapshot.h1().active());
325        assert_eq!(0, snapshot.h1().draining());
326        assert_eq!(1, snapshot.h1().upgraded());
327        assert_eq!(5, snapshot.h2().accepting());
328        assert_eq!(1, snapshot.h2().draining());
329        assert_eq!(6, snapshot.h2().active_requests());
330        assert_eq!(1, snapshot.physically_live_connections());
331    }
332}