Skip to main content

compio_pool/
slot.rs

1//! Per-connection bookkeeping.
2
3use std::time::{Duration, Instant};
4
5use crate::config::Config;
6
7/// Lifecycle metadata the pool tracks for every connection.
8///
9/// Handed to [`Manage::recycle`](crate::Manage::recycle) so a manager can make
10/// policy decisions of its own (for example, pinging only connections that have
11/// been idle for a while).
12#[derive(Debug, Clone)]
13pub struct SlotMeta {
14    /// When the connection was established.
15    pub created_at: Instant,
16    /// When the connection was last returned to the pool.
17    pub last_used: Instant,
18    /// Number of completed checkouts.
19    pub uses: u64,
20    /// Pool generation this connection was created under.
21    pub generation: u64,
22}
23
24impl SlotMeta {
25    pub(crate) fn new(generation: u64) -> Self {
26        let now = Instant::now();
27        Self {
28            created_at: now,
29            last_used: now,
30            uses: 0,
31            generation,
32        }
33    }
34
35    /// How long the connection has existed.
36    pub fn age(&self) -> Duration {
37        self.created_at.elapsed()
38    }
39
40    /// How long since the connection was last handed back.
41    pub fn idle_for(&self) -> Duration {
42        self.last_used.elapsed()
43    }
44}
45
46/// A connection plus its metadata, as stored in a shard's free list.
47#[derive(Debug)]
48pub(crate) struct Slot<C> {
49    pub conn: C,
50    pub meta: SlotMeta,
51}
52
53impl<C> Slot<C> {
54    pub(crate) fn new(conn: C, generation: u64) -> Self {
55        Self {
56            conn,
57            meta: SlotMeta::new(generation),
58        }
59    }
60
61    /// True when the slot must be destroyed rather than reused.
62    ///
63    /// Checked on the way out of the free list, so a connection that aged out
64    /// while idle is never handed to a caller.
65    pub(crate) fn is_expired(&self, cfg: &Config, current_generation: u64) -> bool {
66        if self.meta.generation != current_generation {
67            return true;
68        }
69        if let Some(max) = cfg.max_lifetime
70            && self.meta.age() >= max
71        {
72            return true;
73        }
74        if let Some(idle) = cfg.idle_timeout
75            && self.meta.idle_for() >= idle
76        {
77            return true;
78        }
79        if let Some(max_uses) = cfg.max_uses
80            && self.meta.uses >= max_uses
81        {
82            return true;
83        }
84        false
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    /// Every limit switched off, so a test can enable exactly one.
93    fn no_limits() -> Config {
94        Config::new()
95            .max_lifetime(None)
96            .idle_timeout(None)
97            .max_uses(None)
98    }
99
100    #[test]
101    fn a_fresh_meta_starts_unused_at_its_generation() {
102        let m = SlotMeta::new(7);
103        assert_eq!(m.uses, 0);
104        assert_eq!(m.generation, 7);
105        assert_eq!(
106            m.created_at, m.last_used,
107            "a connection counts as just returned"
108        );
109    }
110
111    #[test]
112    fn age_and_idle_for_advance() {
113        let m = SlotMeta::new(0);
114        // Both read a monotonic clock, so the only safe assertion is ordering.
115        assert!(m.age() <= m.created_at.elapsed());
116        assert!(m.idle_for() <= m.last_used.elapsed());
117    }
118
119    #[test]
120    fn meta_is_clone_and_debug() {
121        let m = SlotMeta::new(3);
122        let c = m.clone();
123        assert_eq!(c.generation, 3);
124        assert!(format!("{m:?}").contains("generation"));
125    }
126
127    #[test]
128    fn a_new_slot_carries_its_connection_and_generation() {
129        let slot = Slot::new("conn", 4);
130        assert_eq!(slot.conn, "conn");
131        assert_eq!(slot.meta.generation, 4);
132        assert!(format!("{slot:?}").contains("conn"));
133    }
134
135    #[test]
136    fn a_slot_with_no_limits_never_expires() {
137        let slot = Slot::new((), 0);
138        assert!(!slot.is_expired(&no_limits(), 0));
139    }
140
141    #[test]
142    fn a_stale_generation_expires_the_slot() {
143        let slot = Slot::new((), 0);
144        assert!(
145            slot.is_expired(&no_limits(), 1),
146            "invalidate() bumps the generation; older connections must go"
147        );
148    }
149
150    /// A generation *ahead* of the pool's cannot happen, but the check is an
151    /// inequality, so pin the behaviour either way.
152    #[test]
153    fn any_generation_mismatch_expires_the_slot() {
154        let slot = Slot::new((), 5);
155        assert!(slot.is_expired(&no_limits(), 4));
156        assert!(!slot.is_expired(&no_limits(), 5));
157    }
158
159    #[test]
160    fn max_lifetime_expires_the_slot() {
161        let slot = Slot::new((), 0);
162        // `age() >= ZERO` always holds, so this is deterministic.
163        assert!(slot.is_expired(&no_limits().max_lifetime(Duration::ZERO), 0));
164    }
165
166    #[test]
167    fn a_lifetime_that_has_not_elapsed_does_not_expire_the_slot() {
168        let slot = Slot::new((), 0);
169        assert!(!slot.is_expired(&no_limits().max_lifetime(Duration::from_secs(3600)), 0));
170    }
171
172    #[test]
173    fn idle_timeout_expires_the_slot() {
174        let slot = Slot::new((), 0);
175        assert!(slot.is_expired(&no_limits().idle_timeout(Duration::ZERO), 0));
176    }
177
178    #[test]
179    fn an_idle_timeout_that_has_not_elapsed_does_not_expire_the_slot() {
180        let slot = Slot::new((), 0);
181        assert!(!slot.is_expired(&no_limits().idle_timeout(Duration::from_secs(3600)), 0));
182    }
183
184    #[test]
185    fn max_uses_expires_the_slot_once_reached() {
186        let mut slot = Slot::new((), 0);
187        let cfg = no_limits().max_uses(2u64);
188
189        assert!(!slot.is_expired(&cfg, 0), "unused");
190        slot.meta.uses = 1;
191        assert!(!slot.is_expired(&cfg, 0), "one checkout of two");
192        slot.meta.uses = 2;
193        assert!(slot.is_expired(&cfg, 0), "the cap is inclusive");
194        slot.meta.uses = 3;
195        assert!(slot.is_expired(&cfg, 0), "and stays tripped past it");
196    }
197
198    /// With every limit configured but none tripped, `is_expired` has to fall
199    /// all the way through to `false`.
200    #[test]
201    fn all_limits_set_and_none_tripped() {
202        let cfg = Config::new()
203            .max_lifetime(Duration::from_secs(3600))
204            .idle_timeout(Duration::from_secs(3600))
205            .max_uses(10u64);
206        assert!(!Slot::new((), 0).is_expired(&cfg, 0));
207    }
208}