Skip to main content

connpool/core/
connection.rs

1//! `PooledConnection`: a raw connection plus the metadata the pool needs to manage it, and
2//! `CloseReason`: why a connection left the pool.
3
4use std::fmt;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7/// Why a connection left the pool. The string values ([`CloseReason::as_str`]) are **wire
8/// values** kept identical across every poolyglot language — do not localize them.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum CloseReason {
11    /// `max_lifetime` reached (rotation).
12    Expired,
13    /// `max_uses` reached (rotation).
14    MaxUses,
15    /// Sat idle longer than `max_idle_time`.
16    Idle,
17    /// Failed validation.
18    Unhealthy,
19    /// The caller's work raised and the connection is suspect.
20    Error,
21    /// `reset()` failed on release.
22    ResetFailed,
23    /// The caller asked for it to be discarded.
24    Discarded,
25    /// The pool was closed.
26    PoolClosed,
27}
28
29impl CloseReason {
30    /// The snake_case wire string for this reason.
31    pub fn as_str(&self) -> &'static str {
32        match self {
33            CloseReason::Expired => "expired",
34            CloseReason::MaxUses => "max_uses",
35            CloseReason::Idle => "idle",
36            CloseReason::Unhealthy => "unhealthy",
37            CloseReason::Error => "error",
38            CloseReason::ResetFailed => "reset_failed",
39            CloseReason::Discarded => "discarded",
40            CloseReason::PoolClosed => "pool_closed",
41        }
42    }
43}
44
45impl fmt::Display for CloseReason {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.write_str(self.as_str())
48    }
49}
50
51static CONN_IDS: AtomicU64 = AtomicU64::new(0);
52
53/// A raw connection plus pool bookkeeping. Instances are tracked by the pool; listeners
54/// receive `&PooledConnection<T>`. The raw connection is reachable via [`PooledConnection::raw`].
55#[derive(Debug, Clone)]
56pub struct PooledConnection<T> {
57    pub(crate) raw: T,
58    pub(crate) created_at: f64,
59    pub(crate) expires_at: Option<f64>,
60    pub(crate) last_returned_at: Option<f64>,
61    pub(crate) checked_out_at: Option<f64>,
62    pub(crate) use_count: u64,
63    pub(crate) leak_reported: bool,
64    pub(crate) id: u64,
65}
66
67impl<T> PooledConnection<T> {
68    /// Builds a wrapper; `last_returned_at` defaults to `created_at`.
69    pub(crate) fn new(raw: T, created_at: f64, expires_at: Option<f64>) -> Self {
70        Self {
71            raw,
72            created_at,
73            expires_at,
74            last_returned_at: Some(created_at),
75            checked_out_at: None,
76            use_count: 0,
77            leak_reported: false,
78            id: CONN_IDS.fetch_add(1, Ordering::Relaxed) + 1,
79        }
80    }
81
82    /// The wrapped connection.
83    pub fn raw(&self) -> &T {
84        &self.raw
85    }
86
87    /// This connection's unique id (monotonic across the process).
88    pub fn id(&self) -> u64 {
89        self.id
90    }
91
92    /// How many times this connection has been checked out.
93    pub fn use_count(&self) -> u64 {
94        self.use_count
95    }
96
97    /// When the connection was created (clock seconds).
98    pub fn created_at(&self) -> f64 {
99        self.created_at
100    }
101
102    /// Age in seconds at `now`.
103    pub fn age(&self, now: f64) -> f64 {
104        now - self.created_at
105    }
106
107    /// Seconds spent idle since last returned (or created), at `now`.
108    pub fn idle_time(&self, now: f64) -> f64 {
109        now - self.last_returned_at.unwrap_or(self.created_at)
110    }
111
112    /// Seconds held since checkout at `now`, or `0.0` if not checked out.
113    pub fn hold_time(&self, now: f64) -> f64 {
114        match self.checked_out_at {
115            Some(t) => now - t,
116            None => 0.0,
117        }
118    }
119
120    pub(crate) fn mark_checked_out(&mut self, now: f64) {
121        self.checked_out_at = Some(now);
122        self.use_count += 1;
123        self.leak_reported = false;
124    }
125
126    pub(crate) fn mark_returned(&mut self, now: f64) {
127        self.checked_out_at = None;
128        self.last_returned_at = Some(now);
129    }
130}
131
132impl<T> fmt::Display for PooledConnection<T> {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "<PooledConnection #{} uses={}>", self.id, self.use_count)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn close_reason_wire_values() {
144        assert_eq!(CloseReason::Expired.as_str(), "expired");
145        assert_eq!(CloseReason::MaxUses.as_str(), "max_uses");
146        assert_eq!(CloseReason::Idle.as_str(), "idle");
147        assert_eq!(CloseReason::Unhealthy.as_str(), "unhealthy");
148        assert_eq!(CloseReason::Error.as_str(), "error");
149        assert_eq!(CloseReason::ResetFailed.as_str(), "reset_failed");
150        assert_eq!(CloseReason::Discarded.as_str(), "discarded");
151        assert_eq!(CloseReason::PoolClosed.as_str(), "pool_closed");
152        assert_eq!(CloseReason::Idle.to_string(), "idle");
153        assert_eq!(format!("{:?}", CloseReason::Idle), "Idle");
154    }
155
156    #[test]
157    fn pooled_connection_lifecycle() {
158        let mut c = PooledConnection::new("conn", 100.0, Some(150.0));
159        assert_eq!(*c.raw(), "conn");
160        assert!(c.id() >= 1);
161        assert_eq!(c.use_count(), 0);
162        assert_eq!(c.created_at(), 100.0);
163        assert_eq!(c.age(110.0), 10.0);
164        assert_eq!(c.idle_time(105.0), 5.0); // last_returned defaults to created_at
165        assert_eq!(c.hold_time(105.0), 0.0); // not checked out
166
167        c.mark_checked_out(120.0);
168        assert_eq!(c.use_count(), 1);
169        assert_eq!(c.hold_time(130.0), 10.0);
170
171        c.mark_returned(140.0);
172        assert_eq!(c.hold_time(150.0), 0.0);
173        assert_eq!(c.idle_time(145.0), 5.0);
174
175        assert!(c.to_string().starts_with("<PooledConnection #"));
176        assert!(format!("{c:?}").contains("PooledConnection"));
177    }
178
179    #[test]
180    fn ids_are_distinct_and_monotonic() {
181        let a = PooledConnection::new((), 0.0, None);
182        let b = PooledConnection::new((), 0.0, None);
183        assert!(b.id() > a.id());
184        // idle_time falls back to created_at when last_returned is cleared.
185        let mut c = PooledConnection::new((), 10.0, None);
186        c.last_returned_at = None;
187        assert_eq!(c.idle_time(25.0), 15.0);
188    }
189}