1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! Per-(partition, authority) connection counters and a pool-level inverted index.
|
7 + | //!
|
8 + | //! Counters are maintained with `Relaxed` atomics. A snapshot observes each counter
|
9 + | //! independently; transient mutual inconsistency is expected (e.g. `active` may
|
10 + | //! briefly exceed `established`). The index never blocks the connection hot path.
|
11 + |
|
12 + | use std::collections::HashMap;
|
13 + | use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
|
14 + | use std::sync::{Arc, Weak};
|
15 + |
|
16 + | use super::connection::Authority;
|
17 + | use super::partition::PartitionId;
|
18 + |
|
19 + | // Protocol-tag encoding for the cell. A cell is per-(partition, authority);
|
20 + | // protocol is per-connection, so a cell CAN observe both over its life
|
21 + | // (multi-endpoint authority, server reconfig, proxy). The tag latches to
|
22 + | // MIXED once it sees two different protocols and never leaves it, so
|
23 + | // `capacity_hint` only answers `Some` for a uniformly-H1 cell and never
|
24 + | // overstates reusable capacity.
|
25 + | pub(crate) const PROTO_UNSET: u8 = 0;
|
26 + | pub(crate) const PROTO_H1: u8 = 1;
|
27 + | pub(crate) const PROTO_H2: u8 = 2;
|
28 + | const PROTO_MIXED: u8 = 3;
|
29 + |
|
30 + | /// Per-(partition, authority) connection counts maintained with `Relaxed` atomics.
|
31 + | ///
|
32 + | /// Each counter is loaded independently; concurrent reads may observe transiently
|
33 + | /// inconsistent combinations (e.g. `active` > `established`). Intended for heuristic
|
34 + | /// reads that tolerate stale or momentarily inconsistent values.
|
35 + | #[derive(Debug, Default)]
|
36 + | pub(crate) struct ConnectionCounters {
|
37 + | /// Connections that have completed handshake and exist (idle + active).
|
38 + | pub(crate) established: AtomicUsize,
|
39 + | /// Handshakes in flight.
|
40 + | pub(crate) establishing: AtomicUsize,
|
41 + | /// Connections/streams currently checked out.
|
42 + | pub(crate) active: AtomicUsize,
|
43 + | /// Protocol tag for this cell (monotonic toward MIXED).
|
44 + | protocol: AtomicU8,
|
45 + | }
|
46 + |
|
47 + | /// Tracks a connection committed to handshaking (TCP + TLS + protocol).
|
48 + | ///
|
49 + | /// Construction increments `establishing`. [`promote`](Self::promote) transitions to
|
50 + | /// an [`EstablishedGuard`] (established++ then establishing--) on success; any other
|
51 + | /// drop path (failure, cancel, panic) decrements `establishing`. Exactly-once by
|
52 + | /// construction: `promote` consumes `self`.
|
53 + | pub(crate) struct EstablishingGuard {
|
54 + | counters: Arc<ConnectionCounters>,
|
55 + | promoted: bool,
|
56 + | }
|
57 + |
|
58 + | impl EstablishingGuard {
|
59 + | pub(crate) fn new(counters: Arc<ConnectionCounters>) -> Self {
|
60 + | counters.establishing.fetch_add(1, Ordering::Relaxed);
|
61 + | Self {
|
62 + | counters,
|
63 + | promoted: false,
|
64 + | }
|
65 + | }
|
66 + |
|
67 + | /// Handshake succeeded: transition establishing → established.
|
68 + | ///
|
69 + | /// `established` is incremented BEFORE `establishing` is decremented so a
|
70 + | /// concurrent reader may observe a transient overcount. The overcount is in the
|
71 + | /// direction of over-reporting readiness, never under-reporting.
|
72 + | pub(crate) fn promote(mut self, proto: u8) -> EstablishedGuard {
|
73 + | self.counters.observe_protocol(proto);
|
74 + | self.counters.established.fetch_add(1, Ordering::Relaxed);
|
75 + | self.counters.establishing.fetch_sub(1, Ordering::Relaxed);
|
76 + | self.promoted = true;
|
77 + | EstablishedGuard {
|
78 + | counters: self.counters.clone(),
|
79 + | }
|
80 + | }
|
81 + | }
|
82 + |
|
83 + | impl Drop for EstablishingGuard {
|
84 + | fn drop(&mut self) {
|
85 + | if !self.promoted {
|
86 + | self.counters.establishing.fetch_sub(1, Ordering::Relaxed);
|
87 + | }
|
88 + | }
|
89 + | }
|
90 + |
|
91 + | /// Owns one connection's contribution to `established`. Held on the
|
92 + | /// Arc-shared inner of a `ManagedConnection`, so for H2 (N clones share
|
93 + | /// one connection) it fires `established--` exactly once — when the last
|
94 + | /// clone drops. Non-`Clone` by design: single ownership is compiler-
|
95 + | /// enforced.
|
96 + | pub(crate) struct EstablishedGuard {
|
97 + | counters: Arc<ConnectionCounters>,
|
98 + | }
|
99 + |
|
100 + | impl Drop for EstablishedGuard {
|
101 + | fn drop(&mut self) {
|
102 + | self.counters.established.fetch_sub(1, Ordering::Relaxed);
|
103 + | }
|
104 + | }
|
105 + |
|
106 + | impl ConnectionCounters {
|
107 + | /// Increment the active (checked-out) count. Paired with exactly one
|
108 + | /// [`decr_active`](Self::decr_active) via an RAII checkout guard.
|
109 + | pub(crate) fn incr_active(&self) {
|
110 + | self.active.fetch_add(1, Ordering::Relaxed);
|
111 + | }
|
112 + |
|
113 + | /// Decrement the active count.
|
114 + | ///
|
115 + | /// Every `incr_active` is paired with exactly one `decr_active` via RAII checkout
|
116 + | /// guards, so `active` is non-negative by construction. A saturating sub here
|
117 + | /// would mask a broken-pairing bug; saturation belongs only on the cross-atomic
|
118 + | /// READ (`idle() = established.saturating_sub(active)`) where two independent
|
119 + | /// relaxed loads may transiently cross.
|
120 + | pub(crate) fn decr_active(&self) {
|
121 + | let prev = self.active.fetch_sub(1, Ordering::Relaxed);
|
122 + | debug_assert!(prev > 0, "active underflow: decr without matching incr");
|
123 + | }
|
124 + |
|
125 + | /// Record the negotiated protocol for a connection in this cell.
|
126 + | ///
|
127 + | /// Latches monotonically toward `MIXED`: `UNSET` → observed protocol; same
|
128 + | /// protocol → no-op; different protocol → `MIXED` (terminal). A cell reaches
|
129 + | /// `MIXED` when one authority negotiates differently across connections
|
130 + | /// (multi-endpoint DNS, server reconfiguration, an intermediary).
|
131 + | ///
|
132 + | /// The sole purpose of this tag is to keep `capacity_hint` from overstating:
|
133 + | /// `capacity_hint` returns `Some` only for a cell known to be uniformly one
|
134 + | /// protocol it can reason about (HTTP/1). Per-connection protocol and
|
135 + | /// stream-limit accounting would supersede this cell-level latch.
|
136 + | pub(crate) fn observe_protocol(&self, proto: u8) {
|
137 + | let mut cur = self.protocol.load(Ordering::Relaxed);
|
138 + | loop {
|
139 + | let next = match cur {
|
140 + | PROTO_UNSET => proto,
|
141 + | c if c == proto => return,
|
142 + | PROTO_MIXED => return,
|
143 + | _ => PROTO_MIXED,
|
144 + | };
|
145 + | match self.protocol.compare_exchange_weak(
|
146 + | cur,
|
147 + | next,
|
148 + | Ordering::Relaxed,
|
149 + | Ordering::Relaxed,
|
150 + | ) {
|
151 + | Ok(_) => return,
|
152 + | Err(actual) => cur = actual,
|
153 + | }
|
154 + | }
|
155 + | }
|
156 + |
|
157 + | /// Current protocol tag for this cell: `PROTO_UNSET` before the first
|
158 + | /// handshake, `PROTO_H1`/`PROTO_H2` for a uniform cell, or the internal
|
159 + | /// mixed marker once a cell has seen both.
|
160 + | pub(crate) fn protocol(&self) -> u8 {
|
161 + | self.protocol.load(Ordering::Relaxed)
|
162 + | }
|
163 + | }
|
164 + |
|
165 + | /// One authority's row in the inverted index: per-partition weak references to counters.
|
166 + | #[derive(Debug, Default)]
|
167 + | pub(crate) struct AuthorityCounters {
|
168 + | pub(crate) by_partition: HashMap<PartitionId, Weak<ConnectionCounters>>,
|
169 + | }
|
170 + |
|
171 + | /// Pool-level inverted index: authority → per-partition counters.
|
172 + | ///
|
173 + | /// Non-owning projection: holds `Weak<ConnectionCounters>` references. Strong owners
|
174 + | /// (pool entries, live checkouts) keep cells alive; once all strong references drop,
|
175 + | /// the `Weak` goes dead and is pruned on the next `snapshot`. The index never extends
|
176 + | /// connection lifetime and never blocks the per-request hot path.
|
177 + | #[derive(Debug, Default)]
|
178 + | pub(crate) struct StatsIndex {
|
179 + | inner: std::sync::Mutex<HashMap<Authority, AuthorityCounters>>,
|
180 + | }
|
181 + |
|
182 + | impl StatsIndex {
|
183 + | /// Register a cell's counters at first-touch of (authority, partition).
|
184 + | /// Stores a `Weak` reference; the caller retains the strong `Arc`.
|
185 + | /// Idempotent: re-registration overwrites the previous entry.
|
186 + | pub(crate) fn register(
|
187 + | &self,
|
188 + | authority: Authority,
|
189 + | partition: PartitionId,
|
190 + | counters: &Arc<ConnectionCounters>,
|
191 + | ) {
|
192 + | let mut idx = self.inner.lock().expect("stats index poisoned");
|
193 + | idx.entry(authority)
|
194 + | .or_default()
|
195 + | .by_partition
|
196 + | .insert(partition, Arc::downgrade(counters));
|
197 + | }
|
198 + |
|
199 + | /// Drop the `(authority, partition)` cell if its counters are no longer
|
200 + | /// referenced, removing the authority entry when its last partition is
|
201 + | /// pruned. A no-op if the cell is still strongly held (e.g. a checkout
|
202 + | /// is in flight when the host's idle connections are evicted), so it is
|
203 + | /// safe to call from the eviction path. Reconstructs nothing the caller
|
204 + | /// does not already hold.
|
205 + | pub(crate) fn prune_if_dead(&self, authority: &Authority, partition: PartitionId) {
|
206 + | let mut idx = self.inner.lock().expect("stats index poisoned");
|
207 + | if let Some(a) = idx.get_mut(authority) {
|
208 + | if a.by_partition
|
209 + | .get(&partition)
|
210 + | .is_some_and(|w| w.strong_count() == 0)
|
211 + | {
|
212 + | a.by_partition.remove(&partition);
|
213 + | }
|
214 + | if a.by_partition.is_empty() {
|
215 + | idx.remove(authority);
|
216 + | }
|
217 + | }
|
218 + | }
|
219 + |
|
220 + | #[cfg(test)]
|
221 + | pub(crate) fn len(&self) -> usize {
|
222 + | self.inner.lock().expect("stats index poisoned").len()
|
223 + | }
|
224 + |
|
225 + | #[cfg(test)]
|
226 + | pub(crate) fn established_for(&self, authority: &Authority, partition: PartitionId) -> usize {
|
227 + | let idx = self.inner.lock().expect("stats index poisoned");
|
228 + | idx.get(authority)
|
229 + | .and_then(|a| a.by_partition.get(&partition))
|
230 + | .and_then(|w| w.upgrade())
|
231 + | .map(|c| c.established.load(Ordering::Relaxed))
|
232 + | .unwrap_or(0)
|
233 + | }
|
234 + |
|
235 + | #[cfg(test)]
|
236 + | pub(crate) fn establishing_for(&self, authority: &Authority, partition: PartitionId) -> usize {
|
237 + | let idx = self.inner.lock().expect("stats index poisoned");
|
238 + | idx.get(authority)
|
239 + | .and_then(|a| a.by_partition.get(&partition))
|
240 + | .and_then(|w| w.upgrade())
|
241 + | .map(|c| c.establishing.load(Ordering::Relaxed))
|
242 + | .unwrap_or(0)
|
243 + | }
|
244 + |
|
245 + | /// Partitions with at least one idle connection to `authority`, as
|
246 + | /// `(partition, idle_count)`. Advisory: a relaxed snapshot that
|
247 + | /// *narrows* reclaim/borrow candidates — the cache pop is the
|
248 + | /// authoritative confirmation. Prunes dead `Weak`s under the lock,
|
249 + | /// same as [`Self::snapshot`].
|
250 + | pub(crate) fn idle_partitions_for(&self, authority: &Authority) -> Vec<(PartitionId, usize)> {
|
251 + | let handles: Vec<(PartitionId, Arc<ConnectionCounters>)> = {
|
252 + | let mut idx = self.inner.lock().expect("stats index poisoned");
|
253 + | match idx.get_mut(authority) {
|
254 + | Some(a) => {
|
255 + | let mut live = Vec::new();
|
256 + | a.by_partition.retain(|partition, weak| {
|
257 + | if let Some(strong) = weak.upgrade() {
|
258 + | live.push((*partition, strong));
|
259 + | true
|
260 + | } else {
|
261 + | false
|
262 + | }
|
263 + | });
|
264 + | if a.by_partition.is_empty() {
|
265 + | idx.remove(authority);
|
266 + | }
|
267 + | live
|
268 + | }
|
269 + | None => Vec::new(),
|
270 + | }
|
271 + | }; // lock released before loading atomics
|
272 + | handles
|
273 + | .into_iter()
|
274 + | .filter_map(|(p, c)| {
|
275 + | let established = c.established.load(Ordering::Relaxed);
|
276 + | let active = c.active.load(Ordering::Relaxed);
|
277 + | let idle = established.saturating_sub(active);
|
278 + | (idle > 0).then_some((p, idle))
|
279 + | })
|
280 + | .collect()
|
281 + | }
|
282 + |
|
283 + | /// All `(authority, partition)` cells with at least one idle
|
284 + | /// connection. Drives `Global`-constraint reclaim, where a freed
|
285 + | /// permit is fungible across authorities. Advisory/narrowing, same
|
286 + | /// contract as [`Self::idle_partitions_for`].
|
287 + | pub(crate) fn idle_cells(&self) -> Vec<(Authority, PartitionId)> {
|
288 + | let handles: Vec<(Authority, PartitionId, Arc<ConnectionCounters>)> = {
|
289 + | let mut idx = self.inner.lock().expect("stats index poisoned");
|
290 + | let mut live = Vec::new();
|
291 + | idx.retain(|authority, a| {
|
292 + | a.by_partition.retain(|partition, weak| {
|
293 + | if let Some(strong) = weak.upgrade() {
|
294 + | live.push((authority.clone(), *partition, strong));
|
295 + | true
|
296 + | } else {
|
297 + | false
|
298 + | }
|
299 + | });
|
300 + | !a.by_partition.is_empty()
|
301 + | });
|
302 + | live
|
303 + | }; // lock released before loading atomics
|
304 + | handles
|
305 + | .into_iter()
|
306 + | .filter_map(|(authority, p, c)| {
|
307 + | let established = c.established.load(Ordering::Relaxed);
|
308 + | let active = c.active.load(Ordering::Relaxed);
|
309 + | (established.saturating_sub(active) > 0).then_some((authority, p))
|
310 + | })
|
311 + | .collect()
|
312 + | }
|
313 + |
|
314 + | /// Snapshot one authority's per-partition counters.
|
315 + | ///
|
316 + | /// Under the lock: upgrades each `Weak`, prunes dead entries (removing the
|
317 + | /// authority entirely if all its partitions are dead). Releases the lock before
|
318 + | /// loading atomics into the returned snapshot.
|
319 + | pub(crate) fn snapshot(&self, authority: &Authority) -> AuthorityStats {
|
320 + | let handles: Vec<(PartitionId, Arc<ConnectionCounters>)> = {
|
321 + | let mut idx = self.inner.lock().expect("stats index poisoned");
|
322 + | match idx.get_mut(authority) {
|
323 + | Some(a) => {
|
324 + | let mut live = Vec::new();
|
325 + | a.by_partition.retain(|partition, weak| {
|
326 + | if let Some(strong) = weak.upgrade() {
|
327 + | live.push((*partition, strong));
|
328 + | true
|
329 + | } else {
|
330 + | false
|
331 + | }
|
332 + | });
|
333 + | if a.by_partition.is_empty() {
|
334 + | idx.remove(authority);
|
335 + | }
|
336 + | live
|
337 + | }
|
338 + | None => Vec::new(),
|
339 + | }
|
340 + | }; // lock released here
|
341 + | let by_partition = handles
|
342 + | .into_iter()
|
343 + | .map(|(p, c)| {
|
344 + | (
|
345 + | p,
|
346 + | PartitionStats {
|
347 + | established: c.established.load(Ordering::Relaxed),
|
348 + | establishing: c.establishing.load(Ordering::Relaxed),
|
349 + | active: c.active.load(Ordering::Relaxed),
|
350 + | protocol: c.protocol(),
|
351 + | },
|
352 + | )
|
353 + | })
|
354 + | .collect();
|
355 + | AuthorityStats { by_partition }
|
356 + | }
|
357 + | }
|
358 + |
|
359 + | /// Point-in-time snapshot of one (partition, authority) cell's connection counts.
|
360 + | ///
|
361 + | /// Plain `usize` values from relaxed loads; cheap and `Copy`. Each field is loaded
|
362 + | /// independently and may be transiently inconsistent with the others.
|
363 + | /// `#[non_exhaustive]` allows fields to be added without a breaking change.
|
364 + | #[non_exhaustive]
|
365 + | #[derive(Debug, Clone, Copy)]
|
366 + | pub struct PartitionStats {
|
367 + | /// Connections that have completed handshake (idle + active).
|
368 + | pub established: usize,
|
369 + | /// Handshakes in flight.
|
370 + | pub establishing: usize,
|
371 + | /// Connections/streams currently checked out.
|
372 + | pub active: usize,
|
373 + | // Private: protocol tag for capacity_hint. Not public — implementation
|
374 + | // detail of the hint that would otherwise freeze an internal encoding
|
375 + | // into the API.
|
376 + | protocol: u8,
|
377 + | }
|
378 + |
|
379 + | impl PartitionStats {
|
380 + | /// Connections not currently checked out: `established.saturating_sub(active)`.
|
381 + | ///
|
382 + | /// Exact for HTTP/1 (one stream per connection). For HTTP/2 a positive value
|
383 + | /// means connections with no active streams; saturated multiplexed connections
|
384 + | /// contribute 0. The saturating subtraction handles transient inconsistency
|
385 + | /// between the two independently-loaded atomics.
|
386 + | pub fn idle(&self) -> usize {
|
387 + | self.established.saturating_sub(self.active)
|
388 + | }
|
389 + |
|
390 + | /// Spare stream capacity, if determinable from current state.
|
391 + | ///
|
392 + | /// - Uniformly HTTP/1 cell: `Some(idle)` (one stream per connection).
|
393 + | /// - HTTP/2, mixed-protocol, or not-yet-handshaken cell: `None` (per-connection
|
394 + | /// stream limits are not indexed).
|
395 + | ///
|
396 + | /// When `Some`, still a relaxed snapshot — treat as a hint.
|
397 + | pub fn capacity_hint(&self) -> Option<usize> {
|
398 + | match self.protocol {
|
399 + | PROTO_H1 => Some(self.idle()),
|
400 + | _ => None,
|
401 + | }
|
402 + | }
|
403 + | }
|
404 + |
|
405 + | /// Point-in-time, per-partition snapshot of connection counts for one authority.
|
406 + | ///
|
407 + | /// Sparse: only partitions that have opened a connection to the authority appear.
|
408 + | /// Returned by [`super::SharedPool::stats`].
|
409 + | pub struct AuthorityStats {
|
410 + | by_partition: Vec<(PartitionId, PartitionStats)>,
|
411 + | }
|
412 + |
|
413 + | impl AuthorityStats {
|
414 + | /// Stats for a specific partition, if it has opened a connection to this authority.
|
415 + | pub fn get(&self, partition: PartitionId) -> Option<PartitionStats> {
|
416 + | self.by_partition
|
417 + | .iter()
|
418 + | .find(|(p, _)| *p == partition)
|
419 + | .map(|(_, s)| *s)
|
420 + | }
|
421 + |
|
422 + | /// Iterate (partition, stats) pairs.
|
423 + | pub fn iter(&self) -> impl Iterator<Item = (PartitionId, PartitionStats)> + '_ {
|
424 + | self.by_partition.iter().copied()
|
425 + | }
|
426 + |
|
427 + | /// Number of partitions that have opened a connection to this authority.
|
428 + | pub fn len(&self) -> usize {
|
429 + | self.by_partition.len()
|
430 + | }
|
431 + |
|
432 + | /// Whether no partition has connected to this authority.
|
433 + | pub fn is_empty(&self) -> bool {
|
434 + | self.by_partition.is_empty()
|
435 + | }
|
436 + | }
|
437 + |
|
438 + | #[cfg(test)]
|
439 + | mod tests {
|
440 + | use super::*;
|
441 + |
|
442 + | #[test]
|
443 + | fn active_incremented_on_checkout_decremented_on_drop() {
|
444 + | let counters = Arc::new(ConnectionCounters::default());
|
445 + | assert_eq!(counters.active.load(Ordering::Relaxed), 0);
|
446 + |
|
447 + | counters.incr_active();
|
448 + | assert_eq!(counters.active.load(Ordering::Relaxed), 1);
|
449 + |
|
450 + | counters.decr_active();
|
451 + | assert_eq!(counters.active.load(Ordering::Relaxed), 0);
|
452 + | }
|
453 + |
|
454 + | #[test]
|
455 + | fn establishing_guard_promote_increments_established_and_clears_establishing() {
|
456 + | let counters = Arc::new(ConnectionCounters::default());
|
457 + |
|
458 + | let guard = EstablishingGuard::new(counters.clone());
|
459 + | assert_eq!(counters.establishing.load(Ordering::Relaxed), 1);
|
460 + | assert_eq!(counters.established.load(Ordering::Relaxed), 0);
|
461 + |
|
462 + | let established = guard.promote(PROTO_H1);
|
463 + | assert_eq!(counters.establishing.load(Ordering::Relaxed), 0);
|
464 + | assert_eq!(counters.established.load(Ordering::Relaxed), 1);
|
465 + |
|
466 + | drop(established);
|
467 + | assert_eq!(counters.established.load(Ordering::Relaxed), 0);
|
468 + | }
|
469 + |
|
470 + | #[test]
|
471 + | fn establishing_guard_drop_without_promote_decrements() {
|
472 + | let counters = Arc::new(ConnectionCounters::default());
|
473 + |
|
474 + | let guard = EstablishingGuard::new(counters.clone());
|
475 + | assert_eq!(counters.establishing.load(Ordering::Relaxed), 1);
|
476 + |
|
477 + | drop(guard);
|
478 + | assert_eq!(counters.establishing.load(Ordering::Relaxed), 0);
|
479 + | assert_eq!(counters.established.load(Ordering::Relaxed), 0);
|
480 + | }
|
481 + |
|
482 + | #[test]
|
483 + | fn stats_index_registers_counters_arc() {
|
484 + | let index = StatsIndex::default();
|
485 + | assert_eq!(index.len(), 0);
|
486 + |
|
487 + | let counters = Arc::new(ConnectionCounters::default());
|
488 + | let authority_a = Authority::new("a.example.com:443");
|
489 + | let partition = PartitionId::from_index(0);
|
490 + | index.register(authority_a, partition, &counters);
|
491 + | assert_eq!(index.len(), 1);
|
492 + |
|
493 + | // Different authority → new entry
|
494 + | let authority_b = Authority::new("b.example.com:443");
|
495 + | let counters2 = Arc::new(ConnectionCounters::default());
|
496 + | index.register(authority_b, partition, &counters2);
|
497 + | assert_eq!(index.len(), 2);
|
498 + |
|
499 + | // Same authority, different partition → same entry (len unchanged)
|
500 + | let authority_a2 = Authority::new("a.example.com:443");
|
501 + | let counters3 = Arc::new(ConnectionCounters::default());
|
502 + | let partition2 = PartitionId::from_index(1);
|
503 + | index.register(authority_a2, partition2, &counters3);
|
504 + | assert_eq!(index.len(), 2);
|
505 + | }
|
506 + |
|
507 + | #[test]
|
508 + | fn stats_index_prunes_dead_cells() {
|
509 + | let index = StatsIndex::default();
|
510 + | let authority = Authority::new("ephemeral.example.com:443");
|
511 + | let partition = PartitionId::from_index(0);
|
512 + |
|
513 + | let counters = Arc::new(ConnectionCounters::default());
|
514 + | index.register(authority.clone(), partition, &counters);
|
515 + | assert_eq!(index.len(), 1);
|
516 + |
|
517 + | // Drop the only strong reference — the Weak in the index is now dead
|
518 + | drop(counters);
|
519 + |
|
520 + | // Snapshot triggers pruning; dead cell is removed
|
521 + | let snap = index.snapshot(&authority);
|
522 + | assert!(snap.is_empty());
|
523 + | assert_eq!(index.len(), 0);
|
524 + | }
|
525 + |
|
526 + | #[test]
|
527 + | fn prune_if_dead_keeps_live_cell_removes_dead_cell() {
|
528 + | let index = StatsIndex::default();
|
529 + | let authority = Authority::new("host.example.com:443");
|
530 + | let partition = PartitionId::from_index(0);
|
531 + | let counters = Arc::new(ConnectionCounters::default());
|
532 + | index.register(authority.clone(), partition, &counters);
|
533 + | assert_eq!(index.len(), 1);
|
534 + |
|
535 + | // Cell is still strongly held (mirrors a checkout in flight when the
|
536 + | // host's idle connections are evicted): prune is a no-op.
|
537 + | index.prune_if_dead(&authority, partition);
|
538 + | assert_eq!(index.len(), 1);
|
539 + |
|
540 + | // Strong ref gone (entry + checkouts dropped): prune removes the cell
|
541 + | // and, as its last partition, the authority entry.
|
542 + | drop(counters);
|
543 + | index.prune_if_dead(&authority, partition);
|
544 + | assert_eq!(index.len(), 0);
|
545 + | }
|
546 + |
|
547 + | #[test]
|
548 + | fn capacity_hint_h1_some_h2_none_mixed_none() {
|
549 + | // H1-only cell: capacity_hint == Some(idle)
|
550 + | let s = PartitionStats {
|
551 + | established: 5,
|
552 + | establishing: 0,
|
553 + | active: 2,
|
554 + | protocol: PROTO_H1,
|
555 + | };
|
556 + | assert_eq!(s.capacity_hint(), Some(3));
|
557 + |
|
558 + | // H2-only cell: None
|
559 + | let s = PartitionStats {
|
560 + | established: 5,
|
561 + | establishing: 0,
|
562 + | active: 2,
|
563 + | protocol: PROTO_H2,
|
564 + | };
|
565 + | assert_eq!(s.capacity_hint(), None);
|
566 + |
|
567 + | // UNSET cell: None
|
568 + | let s = PartitionStats {
|
569 + | established: 0,
|
570 + | establishing: 1,
|
571 + | active: 0,
|
572 + | protocol: PROTO_UNSET,
|
573 + | };
|
574 + | assert_eq!(s.capacity_hint(), None);
|
575 + |
|
576 + | // Mixed cell via observe_protocol transitions: None
|
577 + | let counters = Arc::new(ConnectionCounters::default());
|
578 + | counters.observe_protocol(PROTO_H1);
|
579 + | assert_eq!(counters.protocol(), PROTO_H1);
|
580 + | counters.observe_protocol(PROTO_H2);
|
581 + | assert_eq!(counters.protocol(), PROTO_MIXED);
|
582 + | let s = PartitionStats {
|
583 + | established: 5,
|
584 + | establishing: 0,
|
585 + | active: 2,
|
586 + | protocol: counters.protocol(),
|
587 + | };
|
588 + | assert_eq!(s.capacity_hint(), None);
|
589 + | }
|
590 + |
|
591 + | #[test]
|
592 + | fn observe_protocol_latches_monotonically_to_mixed() {
|
593 + | // First observation sets the protocol from UNSET.
|
594 + | let c = Arc::new(ConnectionCounters::default());
|
595 + | assert_eq!(c.protocol(), PROTO_UNSET);
|
596 + | c.observe_protocol(PROTO_H1);
|
597 + | assert_eq!(c.protocol(), PROTO_H1);
|
598 + |
|
599 + | // Same protocol again is a no-op (stays H1, does not advance to MIXED).
|
600 + | c.observe_protocol(PROTO_H1);
|
601 + | assert_eq!(c.protocol(), PROTO_H1);
|
602 + |
|
603 + | // A different protocol latches to MIXED.
|
604 + | c.observe_protocol(PROTO_H2);
|
605 + | assert_eq!(c.protocol(), PROTO_MIXED);
|
606 + |
|
607 + | // MIXED is terminal: observing either protocol again cannot un-mix it.
|
608 + | c.observe_protocol(PROTO_H1);
|
609 + | assert_eq!(c.protocol(), PROTO_MIXED);
|
610 + | c.observe_protocol(PROTO_H2);
|
611 + | assert_eq!(c.protocol(), PROTO_MIXED);
|
612 + |
|
613 + | // The symmetric first-observation path: H2 first, then H1 → MIXED.
|
614 + | let c = Arc::new(ConnectionCounters::default());
|
615 + | c.observe_protocol(PROTO_H2);
|
616 + | assert_eq!(c.protocol(), PROTO_H2);
|
617 + | c.observe_protocol(PROTO_H1);
|
618 + | assert_eq!(c.protocol(), PROTO_MIXED);
|
619 + | }
|
620 + |
|
621 + | #[test]
|
622 + | fn idle_is_established_minus_active_saturating() {
|
623 + | let s = PartitionStats {
|
624 + | established: 3,
|
625 + | establishing: 0,
|
626 + | active: 1,
|
627 + | protocol: PROTO_H1,
|
628 + | };
|
629 + | assert_eq!(s.idle(), 2);
|
630 + |
|
631 + | // H2 over-subscription: active > established (multiple streams per conn)
|
632 + | let s = PartitionStats {
|
633 + | established: 1,
|
634 + | establishing: 0,
|
635 + | active: 5,
|
636 + | protocol: PROTO_H2,
|
637 + | };
|
638 + | assert_eq!(s.idle(), 0);
|
639 + | }
|
640 + | }
|