Skip to main content

rustev_remote_http/
pool.rs

1//! Bounded, isolation-keyed connection reuse (spec 020, 3.5, I-2).
2//!
3//! Off by default. A host that enables it constructs one
4//! [`ConnectionPool`] under a [`PoolPolicy`] and hands it to the clients
5//! that may share it. Every bound is hard: total connections, connections
6//! per origin, concurrent streams per connection (one: HTTP/1.1 carries a
7//! single request at a time, so nothing is multiplexed), idle connections,
8//! idle lifetime, absolute lifetime and uses per connection. Exhaustion
9//! queues or refuses as the policy declares, never past the attempt budget.
10//!
11//! A connection is reused only under the exact [`PoolKey`] it was opened
12//! under: the digest of every isolation member of [`KeyMaterial`]
13//! (endpoint class and authority, protocol, proxy route, TLS posture,
14//! credential identity digest, privacy options, principal posture, scope
15//! and reference digest, key schema and adapter binding). Unknown or
16//! noncanonical key material makes a key that never reuses. Pool state is
17//! runtime state only: never core state, never replay input. Connections
18//! are never moved between keys; a stale connection is evicted, never
19//! transparently replaced mid-exchange.
20
21use std::collections::BTreeMap;
22use std::fmt;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::{Arc, Mutex};
25use std::time::Duration;
26
27use rustev_contract::canonical::{canonical_bytes, tagged_digest};
28use rustev_contract::ids::ArtifactId;
29use rustev_contract::remote::{
30    AgeBucket, ConnectionObservation, ConnectionReuse, EvictionReason, Multiplex,
31};
32use serde::Serialize;
33use tokio::sync::Notify;
34use tokio::time::Instant;
35
36use crate::budget::Budget;
37
38/// The schema of the isolation key's canonical material.
39pub const POOL_KEY_SCHEMA: &str = "rustev.pool-key/1";
40
41/// What a policy declares when the pool has no room.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "snake_case")]
44pub enum OnExhaustion {
45    /// Refuse at once: `transport_unsent`, nothing sent.
46    Refuse,
47    /// Wait for room at most this long, and never past the attempt budget.
48    Queue { max_wait_ms: u64 },
49}
50
51/// The hard bounds of a pool (spec 020, 3.5). Every bound is positive;
52/// `max_streams` is 1 because the HTTP/1.1 binding cannot multiplex.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
54#[serde(deny_unknown_fields)]
55pub struct PoolPolicy {
56    pub max_connections: u32,
57    pub max_per_origin: u32,
58    pub max_streams: u32,
59    pub max_idle: u32,
60    pub idle_lifetime_ms: u64,
61    pub max_lifetime_ms: u64,
62    pub max_uses: u64,
63    pub on_exhaustion: OnExhaustion,
64}
65
66impl PoolPolicy {
67    /// Refuse an incomplete or unsatisfiable policy (spec 020, 3.1).
68    pub fn validate(&self) -> Result<(), String> {
69        let positive = [
70            ("max_connections", self.max_connections as u64),
71            ("max_per_origin", self.max_per_origin as u64),
72            ("max_streams", self.max_streams as u64),
73            ("max_idle", self.max_idle as u64),
74            ("idle_lifetime_ms", self.idle_lifetime_ms),
75            ("max_lifetime_ms", self.max_lifetime_ms),
76            ("max_uses", self.max_uses),
77        ];
78        if let Some((name, _)) = positive.iter().find(|(_, v)| *v == 0) {
79            return Err(format!("pool bound `{name}` must be positive"));
80        }
81        if self.max_streams != 1 {
82            return Err("the HTTP/1.1 binding carries one stream per connection".into());
83        }
84        if self.max_per_origin > self.max_connections || self.max_idle > self.max_connections {
85            return Err("per-origin and idle bounds cannot exceed the total".into());
86        }
87        if let OnExhaustion::Queue { max_wait_ms: 0 } = self.on_exhaustion {
88            return Err("a queue needs a positive wait".into());
89        }
90        Ok(())
91    }
92}
93
94/// The pool posture of an adapter binding: the policy and the key schema
95/// it isolates by. `disabled`, the default, opens a fresh connection per
96/// exchange, as spec 009 does.
97#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
98#[serde(rename_all = "snake_case")]
99pub enum PoolBinding {
100    #[default]
101    Disabled,
102    Enabled {
103        policy: PoolPolicy,
104        key_schema: String,
105    },
106}
107
108impl PoolBinding {
109    pub fn is_disabled(&self) -> bool {
110        matches!(self, PoolBinding::Disabled)
111    }
112}
113
114/// Every isolation member of a pooled connection (spec 020, 3.5). Only
115/// digests of secrets: the credential identity is a digest, the principal
116/// reference is its evidence digest.
117#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
118pub struct KeyMaterial {
119    pub schema: String,
120    pub endpoint_class: String,
121    /// `scheme://host:port`.
122    pub authority: String,
123    /// The wire protocol and the HTTP version.
124    pub protocol: String,
125    /// `direct`: the binding supports no proxy.
126    pub proxy_route: String,
127    /// `plaintext`, `webpki-roots`, or the host's label for its own TLS
128    /// configuration; `None` (unknown) disables reuse.
129    pub tls: Option<String>,
130    /// `none`, or the digest of the credential's identity.
131    pub credential: String,
132    /// Privacy and provider-routing options.
133    pub privacy: BTreeMap<String, String>,
134    /// `omitted`, or `host_operated` with mapping version and scope.
135    pub principal_posture: String,
136    /// The principal reference's evidence digest, per exchange.
137    pub principal: Option<String>,
138    pub binding: Option<ArtifactId>,
139}
140
141/// The digest of a credential's identity: never the credential.
142pub fn credential_identity(secret: Option<&str>) -> String {
143    match secret {
144        None => "none".into(),
145        Some(s) => tagged_digest("rustev.credential-identity/1", s.as_bytes()),
146    }
147}
148
149/// A connection's isolation key and origin.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct PoolKey {
152    /// `None` when the key material is unknown or noncanonical: such a
153    /// connection is never reused.
154    pub digest: Option<String>,
155    pub origin: String,
156}
157
158impl KeyMaterial {
159    /// The key: the tagged digest of the canonical material, or no reuse
160    /// when any member is unknown or the material has no canonical form.
161    pub fn key(&self) -> PoolKey {
162        let known = self.tls.is_some() && self.binding.is_some() && self.schema == POOL_KEY_SCHEMA;
163        let digest = known
164            .then(|| canonical_bytes(self).ok())
165            .flatten()
166            .map(|b| tagged_digest(POOL_KEY_SCHEMA, &b));
167        PoolKey {
168            digest,
169            origin: self.authority.clone(),
170        }
171    }
172}
173
174/// The pool could not provide a connection within the policy and budget.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct Exhausted;
177
178struct Meta {
179    id: u64,
180    key: Option<String>,
181    origin: String,
182    created: Instant,
183    uses: u64,
184    last_used: Instant,
185}
186
187struct Idle<C> {
188    meta: Meta,
189    conn: C,
190}
191
192struct State<C> {
193    idle: Vec<Idle<C>>,
194    /// Connections open (checked out, reserved or idle) per origin.
195    per_origin: BTreeMap<String, u32>,
196    open: u32,
197    evictions: BTreeMap<String, u64>,
198}
199
200/// Pool counters, for hosts and tests.
201#[derive(Debug, Clone, PartialEq, Eq, Default)]
202pub struct PoolStats {
203    pub open: u32,
204    pub idle: u32,
205    pub opened: u64,
206    pub reused: u64,
207    pub evictions: BTreeMap<String, u64>,
208}
209
210/// A connection the pool can tell is closed.
211pub trait Pooled: Send + 'static {
212    fn is_closed(&self) -> bool;
213}
214
215/// Bounded connections shared by every client given the same pool.
216pub struct ConnectionPool<C> {
217    policy: PoolPolicy,
218    state: Mutex<State<C>>,
219    room: Notify,
220    next_id: AtomicU64,
221    opened: AtomicU64,
222    reused: AtomicU64,
223}
224
225impl<C> fmt::Debug for ConnectionPool<C> {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        f.debug_struct("ConnectionPool")
228            .field("policy", &self.policy)
229            .finish_non_exhaustive()
230    }
231}
232
233fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
234    m.lock().unwrap_or_else(|e| e.into_inner())
235}
236
237fn reason_name(r: EvictionReason) -> String {
238    serde_json::to_value(r)
239        .ok()
240        .and_then(|v| v.as_str().map(str::to_string))
241        .unwrap_or_default()
242}
243
244fn ms(d: Duration) -> u64 {
245    u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
246}
247
248/// A checked-out connection, or a reserved place for a fresh one. Dropped
249/// without [`ConnectionPool::release`], it closes and frees its place.
250pub struct Lease<C: Pooled> {
251    pool: Arc<ConnectionPool<C>>,
252    meta: Option<Meta>,
253    conn: Option<C>,
254    reused: bool,
255}
256
257impl<C: Pooled> Lease<C> {
258    /// The connection, once there is one.
259    pub fn conn(&self) -> Option<&C> {
260        self.conn.as_ref()
261    }
262
263    pub fn is_reused(&self) -> bool {
264        self.reused
265    }
266
267    pub fn conn_mut(&mut self) -> Option<&mut C> {
268        self.conn.as_mut()
269    }
270
271    /// Put the fresh connection in the reserved place.
272    pub fn fill(&mut self, conn: C) {
273        self.conn = Some(conn);
274    }
275
276    /// What evidence states about the connection, before release.
277    pub fn observation(&self, key_digest: &str) -> Option<ConnectionObservation> {
278        let m = self.meta.as_ref()?;
279        Some(ConnectionObservation {
280            reuse: if self.reused {
281                ConnectionReuse::Reused
282            } else {
283                ConnectionReuse::Fresh
284            },
285            instance_id: format!("conn-{}", m.id),
286            age_bucket: AgeBucket::of_ms(ms(Instant::now().saturating_duration_since(m.created))),
287            use_count: m.uses,
288            multiplex: Multiplex::None,
289            key_digest: key_digest.to_string(),
290            eviction: None,
291        })
292    }
293}
294
295impl<C: Pooled> Drop for Lease<C> {
296    fn drop(&mut self) {
297        if let Some(m) = self.meta.take() {
298            self.pool.close(&m, EvictionReason::Failed);
299        }
300    }
301}
302
303impl<C: Pooled> ConnectionPool<C> {
304    /// A pool under `policy`, which must validate.
305    pub fn new(policy: PoolPolicy) -> Result<Arc<Self>, String> {
306        policy.validate()?;
307        Ok(Arc::new(ConnectionPool {
308            policy,
309            state: Mutex::new(State {
310                idle: vec![],
311                per_origin: BTreeMap::new(),
312                open: 0,
313                evictions: BTreeMap::new(),
314            }),
315            room: Notify::new(),
316            next_id: AtomicU64::new(1),
317            opened: AtomicU64::new(0),
318            reused: AtomicU64::new(0),
319        }))
320    }
321
322    pub fn policy(&self) -> &PoolPolicy {
323        &self.policy
324    }
325
326    /// The binding view of this pool.
327    pub fn binding(&self) -> PoolBinding {
328        PoolBinding::Enabled {
329            policy: self.policy,
330            key_schema: POOL_KEY_SCHEMA.into(),
331        }
332    }
333
334    pub fn stats(&self) -> PoolStats {
335        let st = lock(&self.state);
336        PoolStats {
337            open: st.open,
338            idle: st.idle.len() as u32,
339            opened: self.opened.load(Ordering::SeqCst),
340            reused: self.reused.load(Ordering::SeqCst),
341            evictions: st.evictions.clone(),
342        }
343    }
344
345    fn count(st: &mut State<C>, reason: EvictionReason) {
346        *st.evictions.entry(reason_name(reason)).or_default() += 1;
347    }
348
349    fn close(&self, m: &Meta, reason: EvictionReason) {
350        let mut st = lock(&self.state);
351        Self::forget(&mut st, m);
352        Self::count(&mut st, reason);
353        drop(st);
354        self.room.notify_waiters();
355    }
356
357    fn forget(st: &mut State<C>, m: &Meta) {
358        st.open = st.open.saturating_sub(1);
359        if let Some(n) = st.per_origin.get_mut(&m.origin) {
360            *n = n.saturating_sub(1);
361            if *n == 0 {
362                st.per_origin.remove(&m.origin);
363            }
364        }
365    }
366
367    /// Drop every idle connection past its idle or absolute lifetime, or
368    /// closed. Deterministic: in idle order.
369    fn sweep(&self, st: &mut State<C>, now: Instant) {
370        let idle_life = Duration::from_millis(self.policy.idle_lifetime_ms);
371        let life = Duration::from_millis(self.policy.max_lifetime_ms);
372        let mut keep = Vec::with_capacity(st.idle.len());
373        for i in std::mem::take(&mut st.idle) {
374            let reason = if i.conn.is_closed() {
375                Some(EvictionReason::Closed)
376            } else if now.saturating_duration_since(i.meta.created) >= life {
377                Some(EvictionReason::LifetimeExpired)
378            } else if now.saturating_duration_since(i.meta.last_used) >= idle_life {
379                Some(EvictionReason::IdleExpired)
380            } else {
381                None
382            };
383            match reason {
384                Some(r) => {
385                    Self::forget(st, &i.meta);
386                    Self::count(st, r);
387                }
388                None => keep.push(i),
389            }
390        }
391        st.idle = keep;
392    }
393
394    fn try_acquire(self: &Arc<Self>, key: &PoolKey) -> Option<Lease<C>> {
395        let now = Instant::now();
396        let mut st = lock(&self.state);
397        self.sweep(&mut st, now);
398        // Reuse: the exact key only, never a connection of another key.
399        if let Some(digest) = &key.digest
400            && let Some(at) = st
401                .idle
402                .iter()
403                .position(|i| i.meta.key.as_deref() == Some(digest.as_str()))
404        {
405            let Idle { mut meta, conn } = st.idle.remove(at);
406            meta.uses += 1;
407            meta.last_used = now;
408            self.reused.fetch_add(1, Ordering::SeqCst);
409            return Some(Lease {
410                pool: self.clone(),
411                meta: Some(meta),
412                conn: Some(conn),
413                reused: true,
414            });
415        }
416        let origin_open = st.per_origin.get(&key.origin).copied().unwrap_or(0);
417        if origin_open >= self.policy.max_per_origin {
418            // Make room only from this origin's idle connections.
419            let at = st.idle.iter().position(|i| i.meta.origin == key.origin)?;
420            let old = st.idle.remove(at);
421            Self::forget(&mut st, &old.meta);
422            Self::count(&mut st, EvictionReason::Capacity);
423        }
424        if st.open >= self.policy.max_connections {
425            // The least recently used idle connection of any key closes to
426            // make room; it moves nowhere.
427            let at = (0..st.idle.len())
428                .min_by_key(|&i| (st.idle[i].meta.last_used, st.idle[i].meta.id))?;
429            let old = st.idle.remove(at);
430            Self::forget(&mut st, &old.meta);
431            Self::count(&mut st, EvictionReason::Capacity);
432        }
433        st.open += 1;
434        *st.per_origin.entry(key.origin.clone()).or_default() += 1;
435        self.opened.fetch_add(1, Ordering::SeqCst);
436        Some(Lease {
437            pool: self.clone(),
438            meta: Some(Meta {
439                id: self.next_id.fetch_add(1, Ordering::SeqCst),
440                key: key.digest.clone(),
441                origin: key.origin.clone(),
442                created: now,
443                uses: 1,
444                last_used: now,
445            }),
446            conn: None,
447            reused: false,
448        })
449    }
450
451    /// An idle connection under exactly `key`, or a reserved place for a
452    /// fresh one. On exhaustion, refuse or queue as the policy declares,
453    /// never past `budget`.
454    pub async fn acquire(
455        self: &Arc<Self>,
456        key: &PoolKey,
457        budget: &Budget,
458    ) -> Result<Lease<C>, Exhausted> {
459        let wait = match self.policy.on_exhaustion {
460            OnExhaustion::Refuse => None,
461            OnExhaustion::Queue { max_wait_ms } => Some(Budget::from_ms(max_wait_ms).min(*budget)),
462        };
463        loop {
464            let room = self.room.notified();
465            tokio::pin!(room);
466            room.as_mut().enable();
467            if let Some(l) = self.try_acquire(key) {
468                return Ok(l);
469            }
470            let Some(w) = wait else {
471                return Err(Exhausted);
472            };
473            tokio::select! {
474                _ = &mut room => {}
475                _ = w.expiry() => return Err(Exhausted),
476            }
477        }
478    }
479
480    /// Return a connection after its exchange. `reusable` is false when the
481    /// exchange failed or did not complete its response. Returns why the
482    /// connection left the pool, if it did.
483    pub fn release(&self, mut lease: Lease<C>, reusable: bool) -> Option<EvictionReason> {
484        let meta = lease.meta.take()?;
485        let conn = lease.conn.take();
486        let now = Instant::now();
487        let reason = match &conn {
488            None => Some(EvictionReason::Failed),
489            Some(_) if !reusable => Some(EvictionReason::Failed),
490            Some(c) if c.is_closed() => Some(EvictionReason::Closed),
491            Some(_) if meta.key.is_none() => Some(EvictionReason::Closed),
492            Some(_) if meta.uses >= self.policy.max_uses => Some(EvictionReason::MaxUses),
493            Some(_)
494                if now.saturating_duration_since(meta.created)
495                    >= Duration::from_millis(self.policy.max_lifetime_ms) =>
496            {
497                Some(EvictionReason::LifetimeExpired)
498            }
499            Some(_) => None,
500        };
501        let mut st = lock(&self.state);
502        match (reason, conn) {
503            (Some(r), _) => {
504                Self::forget(&mut st, &meta);
505                Self::count(&mut st, r);
506                drop(st);
507                self.room.notify_waiters();
508                Some(r)
509            }
510            (None, Some(conn)) => {
511                if st.idle.len() as u32 >= self.policy.max_idle {
512                    // The oldest idle connection gives way.
513                    let at = (0..st.idle.len())
514                        .min_by_key(|&i| (st.idle[i].meta.last_used, st.idle[i].meta.id))
515                        .expect("max_idle is positive");
516                    let old = st.idle.remove(at);
517                    Self::forget(&mut st, &old.meta);
518                    Self::count(&mut st, EvictionReason::IdleCapacity);
519                }
520                let mut meta = meta;
521                meta.last_used = now;
522                st.idle.push(Idle { meta, conn });
523                drop(st);
524                self.room.notify_waiters();
525                None
526            }
527            (None, None) => unreachable!("no connection is always a reason"),
528        }
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use std::sync::atomic::AtomicBool;
536
537    /// A SYNTHETIC connection whose closure a test controls.
538    #[derive(Clone, Default)]
539    struct Fake(Arc<AtomicBool>);
540
541    impl Pooled for Fake {
542        fn is_closed(&self) -> bool {
543            self.0.load(Ordering::SeqCst)
544        }
545    }
546
547    fn policy() -> PoolPolicy {
548        PoolPolicy {
549            max_connections: 3,
550            max_per_origin: 2,
551            max_streams: 1,
552            max_idle: 2,
553            idle_lifetime_ms: 100,
554            max_lifetime_ms: 1_000,
555            max_uses: 3,
556            on_exhaustion: OnExhaustion::Refuse,
557        }
558    }
559
560    fn key(d: &str, origin: &str) -> PoolKey {
561        PoolKey {
562            digest: Some(d.into()),
563            origin: origin.into(),
564        }
565    }
566
567    async fn take(p: &Arc<ConnectionPool<Fake>>, k: &PoolKey) -> Lease<Fake> {
568        let mut l = p.acquire(k, &Budget::from_ms(10_000)).await.unwrap();
569        if l.conn().is_none() {
570            l.fill(Fake::default());
571        }
572        l
573    }
574
575    #[test]
576    fn every_bound_must_be_positive_and_consistent() {
577        assert!(policy().validate().is_ok());
578        let cases: Vec<fn(&mut PoolPolicy)> = vec![
579            |p| p.max_connections = 0,
580            |p| p.max_per_origin = 0,
581            |p| p.max_streams = 0,
582            |p| p.max_streams = 2,
583            |p| p.max_idle = 0,
584            |p| p.idle_lifetime_ms = 0,
585            |p| p.max_lifetime_ms = 0,
586            |p| p.max_uses = 0,
587            |p| p.max_per_origin = 4,
588            |p| p.max_idle = 4,
589            |p| p.on_exhaustion = OnExhaustion::Queue { max_wait_ms: 0 },
590        ];
591        for c in cases {
592            let mut p = policy();
593            c(&mut p);
594            assert!(p.validate().is_err(), "{p:?}");
595            assert!(ConnectionPool::<Fake>::new(p).is_err());
596        }
597    }
598
599    #[tokio::test(flavor = "current_thread", start_paused = true)]
600    async fn a_connection_is_reused_only_under_its_exact_key() {
601        let p = ConnectionPool::<Fake>::new(policy()).unwrap();
602        let a = key("A", "o");
603        let l = take(&p, &a).await;
604        assert!(!l.is_reused());
605        assert_eq!(p.release(l, true), None);
606        // Another key at the same origin never takes it.
607        let b = take(&p, &key("B", "o")).await;
608        assert!(!b.is_reused());
609        let again = take(&p, &a).await;
610        assert!(again.is_reused());
611        assert_eq!(again.observation("A").unwrap().use_count, 2);
612        // A key without a digest (unknown material) never reuses.
613        let unknown = PoolKey {
614            digest: None,
615            origin: "u".into(),
616        };
617        let u = take(&p, &unknown).await;
618        assert_eq!(p.release(u, true), Some(EvictionReason::Closed));
619        let stats = p.stats();
620        assert_eq!((stats.opened, stats.reused), (3, 1));
621    }
622
623    #[tokio::test(flavor = "current_thread", start_paused = true)]
624    async fn lifetimes_uses_and_closure_evict() {
625        let p = ConnectionPool::<Fake>::new(policy()).unwrap();
626        let a = key("A", "o");
627        // Idle lifetime.
628        let l = take(&p, &a).await;
629        p.release(l, true);
630        tokio::time::advance(Duration::from_millis(100)).await;
631        assert!(!take(&p, &a).await.is_reused());
632        assert_eq!(p.stats().evictions["idle_expired"], 1);
633        // Uses: the third use is the last.
634        let p = ConnectionPool::<Fake>::new(policy()).unwrap();
635        let mut last = None;
636        for n in 1..=3 {
637            let l = take(&p, &a).await;
638            assert_eq!(l.is_reused(), n > 1);
639            last = p.release(l, true);
640        }
641        assert_eq!(last, Some(EvictionReason::MaxUses));
642        // Absolute lifetime, even while in use.
643        let p = ConnectionPool::<Fake>::new(policy()).unwrap();
644        let l = take(&p, &a).await;
645        tokio::time::advance(Duration::from_millis(1_000)).await;
646        assert_eq!(p.release(l, true), Some(EvictionReason::LifetimeExpired));
647        // Absolute lifetime while idle.
648        let l = take(&p, &a).await;
649        p.release(l, true);
650        for _ in 0..11 {
651            tokio::time::advance(Duration::from_millis(95)).await;
652            let l = take(&p, &a).await;
653            if !l.is_reused() {
654                break;
655            }
656            p.release(l, true);
657        }
658        assert!(p.stats().evictions.contains_key("lifetime_expired"));
659        // A closed idle connection is evicted at checkout, not reused.
660        let p = ConnectionPool::<Fake>::new(policy()).unwrap();
661        let l = take(&p, &a).await;
662        let flag = l.conn().unwrap().0.clone();
663        p.release(l, true);
664        flag.store(true, Ordering::SeqCst);
665        assert!(!take(&p, &a).await.is_reused());
666        assert_eq!(p.stats().evictions["closed"], 1);
667        // A failed exchange never returns its connection.
668        let l = take(&p, &key("F", "o")).await;
669        assert_eq!(p.release(l, false), Some(EvictionReason::Failed));
670    }
671
672    #[tokio::test(flavor = "current_thread", start_paused = true)]
673    async fn idle_total_and_origin_bounds_hold_and_exhaustion_refuses_or_queues() {
674        let p = ConnectionPool::<Fake>::new(policy()).unwrap();
675        // Idle bound: a third idle connection pushes the oldest out.
676        let (x, y, z) = (
677            take(&p, &key("X", "o1")).await,
678            take(&p, &key("Y", "o1")).await,
679            take(&p, &key("Z", "o2")).await,
680        );
681        p.release(x, true);
682        tokio::time::advance(Duration::from_millis(1)).await;
683        p.release(y, true);
684        p.release(z, true);
685        assert_eq!(p.stats().idle, 2);
686        assert_eq!(p.stats().evictions["idle_capacity"], 1);
687        // Per-origin bound: two checked out at o1, a third is refused.
688        let p = ConnectionPool::<Fake>::new(policy()).unwrap();
689        let _a = take(&p, &key("A", "o1")).await;
690        let _b = take(&p, &key("B", "o1")).await;
691        assert!(
692            p.acquire(&key("C", "o1"), &Budget::from_ms(10))
693                .await
694                .is_err()
695        );
696        // Total bound: one more at o2 fits, then nothing.
697        let _c = take(&p, &key("C", "o2")).await;
698        assert!(
699            p.acquire(&key("D", "o3"), &Budget::from_ms(10))
700                .await
701                .is_err()
702        );
703        // An idle connection of another key gives way (closed, not moved).
704        let p = ConnectionPool::<Fake>::new(policy()).unwrap();
705        let a = take(&p, &key("A", "o1")).await;
706        let _b = take(&p, &key("B", "o2")).await;
707        let _c = take(&p, &key("C", "o3")).await;
708        p.release(a, true);
709        let d = take(&p, &key("D", "o4")).await;
710        assert!(!d.is_reused());
711        assert_eq!(p.stats().evictions["capacity"], 1);
712
713        // Queue: wait for room within the declared wait and the budget.
714        let mut q = policy();
715        q.max_connections = 1;
716        q.max_per_origin = 1;
717        q.max_idle = 1;
718        q.on_exhaustion = OnExhaustion::Queue { max_wait_ms: 50 };
719        let p = ConnectionPool::<Fake>::new(q).unwrap();
720        let held = take(&p, &key("A", "o")).await;
721        let t0 = Instant::now();
722        assert!(
723            p.acquire(&key("B", "o"), &Budget::from_ms(10_000))
724                .await
725                .is_err()
726        );
727        assert_eq!(t0.elapsed(), Duration::from_millis(50));
728        let t0 = Instant::now();
729        assert!(
730            p.acquire(&key("B", "o"), &Budget::from_ms(20))
731                .await
732                .is_err()
733        );
734        assert_eq!(
735            t0.elapsed(),
736            Duration::from_millis(20),
737            "never past the budget"
738        );
739        let p2 = p.clone();
740        tokio::spawn(async move {
741            tokio::time::sleep(Duration::from_millis(30)).await;
742            drop(held);
743        });
744        let got = p2.acquire(&key("B", "o"), &Budget::from_ms(10_000)).await;
745        assert!(got.is_ok());
746        let _ = p;
747    }
748
749    #[test]
750    fn every_key_member_changes_the_key_and_unknown_material_disables_reuse() {
751        let base = KeyMaterial {
752            schema: POOL_KEY_SCHEMA.into(),
753            endpoint_class: "c".into(),
754            authority: "http://127.0.0.1:1".into(),
755            protocol: "rustev.remote/1 http/1.1".into(),
756            proxy_route: "direct".into(),
757            tls: Some("plaintext".into()),
758            credential: credential_identity(Some("SYNTHETIC-A")),
759            privacy: BTreeMap::new(),
760            principal_posture: "omitted".into(),
761            principal: None,
762            binding: Some(
763                ArtifactId::parse(
764                    "sha256:00000000000000000000000000000000000000000000000000000000000000aa",
765                )
766                .unwrap(),
767            ),
768        };
769        let k = base.key().digest.unwrap();
770        let variants: Vec<KeyMaterial> =
771            vec![
772            KeyMaterial { endpoint_class: "x".into(), ..base.clone() },
773            KeyMaterial { authority: "http://127.0.0.1:2".into(), ..base.clone() },
774            KeyMaterial { protocol: "rustev.remote/2 http/1.1".into(), ..base.clone() },
775            KeyMaterial { proxy_route: "via".into(), ..base.clone() },
776            KeyMaterial { tls: Some("webpki-roots".into()), ..base.clone() },
777            KeyMaterial { credential: credential_identity(Some("SYNTHETIC-B")), ..base.clone() },
778            KeyMaterial { credential: credential_identity(None), ..base.clone() },
779            KeyMaterial { privacy: BTreeMap::from([("k".into(), "v".into())]), ..base.clone() },
780            KeyMaterial { principal_posture: "host_operated m/1 s".into(), ..base.clone() },
781            KeyMaterial { principal: Some("d".into()), ..base.clone() },
782            KeyMaterial {
783                binding: Some(
784                    ArtifactId::parse(
785                        "sha256:00000000000000000000000000000000000000000000000000000000000000bb",
786                    )
787                    .unwrap(),
788                ),
789                ..base.clone()
790            },
791        ];
792        for v in variants {
793            assert_ne!(v.key().digest.unwrap(), k, "{v:?}");
794        }
795        assert!(!credential_identity(Some("SYNTHETIC-A")).contains("SYNTHETIC"));
796        for unknown in [
797            KeyMaterial {
798                tls: None,
799                ..base.clone()
800            },
801            KeyMaterial {
802                binding: None,
803                ..base.clone()
804            },
805            KeyMaterial {
806                schema: "rustev.pool-key/0".into(),
807                ..base.clone()
808            },
809        ] {
810            assert_eq!(unknown.key().digest, None);
811        }
812    }
813}