Skip to main content

compio_pool/
config.rs

1//! Pool sizing and lifecycle configuration.
2
3use std::time::Duration;
4
5/// Tunables for a [`Pool`](crate::Pool).
6///
7/// # Sizing is per-shard
8///
9/// `compio` is a thread-per-core runtime, so this pool keeps one independent
10/// *shard* per runtime thread (see the crate docs). [`Config::max_size`] and
11/// [`Config::min_idle`] therefore apply **per shard**, not to the process. A
12/// pool with `max_size = 8` running on 4 compio threads can hold up to 32
13/// connections in total.
14///
15/// This is deliberate: a global cap requires a cross-thread semaphore on the
16/// acquire fast path, which is exactly the contention thread-per-core runtimes
17/// exist to avoid.
18#[derive(Debug, Clone)]
19pub struct Config {
20    pub(crate) max_size: usize,
21    pub(crate) min_idle: usize,
22    pub(crate) acquire_timeout: Option<Duration>,
23    pub(crate) max_lifetime: Option<Duration>,
24    pub(crate) idle_timeout: Option<Duration>,
25    pub(crate) max_uses: Option<u64>,
26    pub(crate) reap_interval: Duration,
27}
28
29impl Default for Config {
30    fn default() -> Self {
31        Self {
32            max_size: 16,
33            min_idle: 0,
34            acquire_timeout: Some(Duration::from_secs(30)),
35            max_lifetime: Some(Duration::from_secs(30 * 60)),
36            idle_timeout: Some(Duration::from_secs(10 * 60)),
37            max_uses: None,
38            reap_interval: Duration::from_secs(30),
39        }
40    }
41}
42
43impl Config {
44    /// Starts from the defaults.
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Maximum live connections **per runtime thread**. Must be non-zero.
50    pub fn max_size(mut self, n: usize) -> Self {
51        assert!(n > 0, "max_size must be greater than zero");
52        self.max_size = n;
53        self
54    }
55
56    /// Idle connections each shard keeps thread-locally. Clamped to `max_size`.
57    ///
58    /// This doubles as the split point between local caching and sharing: with
59    /// a [`Reservoir`](crate::Reservoir) installed, returned connections beyond
60    /// `min_idle` are parked for other threads to claim. Leaving it at `0`
61    /// therefore means *every* connection goes back to the shared stack and
62    /// each checkout pays a mutex. Set it to your steady-state per-thread
63    /// concurrency to keep the hot path lock-free and share only the surplus.
64    pub fn min_idle(mut self, n: usize) -> Self {
65        self.min_idle = n;
66        self
67    }
68
69    /// How long [`Pool::acquire`](crate::Pool::acquire) waits before returning
70    /// [`Error::Timeout`](crate::Error::Timeout). `None` waits forever.
71    pub fn acquire_timeout(mut self, t: impl Into<Option<Duration>>) -> Self {
72        self.acquire_timeout = t.into();
73        self
74    }
75
76    /// Hard age cap; a connection older than this is closed instead of reused.
77    pub fn max_lifetime(mut self, t: impl Into<Option<Duration>>) -> Self {
78        self.max_lifetime = t.into();
79        self
80    }
81
82    /// How long a connection may sit unused in the free list before being reaped.
83    pub fn idle_timeout(mut self, t: impl Into<Option<Duration>>) -> Self {
84        self.idle_timeout = t.into();
85        self
86    }
87
88    /// Retire a connection after this many checkouts.
89    pub fn max_uses(mut self, n: impl Into<Option<u64>>) -> Self {
90        self.max_uses = n.into();
91        self
92    }
93
94    /// How often each shard's background reaper runs.
95    ///
96    /// The reaper is what actually enforces [`idle_timeout`](Config::idle_timeout)
97    /// and [`max_lifetime`](Config::max_lifetime) on connections nobody is
98    /// asking for, and what refills [`min_idle`](Config::min_idle).
99    pub fn reap_interval(mut self, t: Duration) -> Self {
100        assert!(!t.is_zero(), "reap_interval must be non-zero");
101        self.reap_interval = t;
102        self
103    }
104
105    pub(crate) fn needs_reaper(&self) -> bool {
106        self.min_idle > 0 || self.idle_timeout.is_some() || self.max_lifetime.is_some()
107    }
108
109    pub(crate) fn effective_min_idle(&self) -> usize {
110        self.min_idle.min(self.max_size)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    /// A config with no timed policy at all, so a test can add exactly one.
119    fn untimed() -> Config {
120        Config::new().max_lifetime(None).idle_timeout(None)
121    }
122
123    #[test]
124    fn defaults_are_documented_values() {
125        let c = Config::default();
126        assert_eq!(c.max_size, 16);
127        assert_eq!(c.min_idle, 0);
128        assert_eq!(c.acquire_timeout, Some(Duration::from_secs(30)));
129        assert_eq!(c.max_lifetime, Some(Duration::from_secs(30 * 60)));
130        assert_eq!(c.idle_timeout, Some(Duration::from_secs(10 * 60)));
131        assert_eq!(c.max_uses, None);
132        assert_eq!(c.reap_interval, Duration::from_secs(30));
133    }
134
135    #[test]
136    fn new_is_default() {
137        let a = Config::new();
138        let b = Config::default();
139        assert_eq!(a.max_size, b.max_size);
140        assert_eq!(a.min_idle, b.min_idle);
141        assert_eq!(a.acquire_timeout, b.acquire_timeout);
142        assert_eq!(a.max_lifetime, b.max_lifetime);
143        assert_eq!(a.idle_timeout, b.idle_timeout);
144        assert_eq!(a.max_uses, b.max_uses);
145        assert_eq!(a.reap_interval, b.reap_interval);
146    }
147
148    #[test]
149    fn every_setter_round_trips() {
150        let c = Config::new()
151            .max_size(7)
152            .min_idle(3)
153            .acquire_timeout(Duration::from_millis(11))
154            .max_lifetime(Duration::from_millis(22))
155            .idle_timeout(Duration::from_millis(33))
156            .max_uses(44u64)
157            .reap_interval(Duration::from_millis(55));
158
159        assert_eq!(c.max_size, 7);
160        assert_eq!(c.min_idle, 3);
161        assert_eq!(c.acquire_timeout, Some(Duration::from_millis(11)));
162        assert_eq!(c.max_lifetime, Some(Duration::from_millis(22)));
163        assert_eq!(c.idle_timeout, Some(Duration::from_millis(33)));
164        assert_eq!(c.max_uses, Some(44));
165        assert_eq!(c.reap_interval, Duration::from_millis(55));
166    }
167
168    /// The `impl Into<Option<_>>` setters must accept both a bare value and
169    /// `None`, which is the whole reason for that signature.
170    #[test]
171    fn optional_setters_accept_none() {
172        let c = Config::new()
173            .acquire_timeout(None)
174            .max_lifetime(None)
175            .idle_timeout(None)
176            .max_uses(None);
177
178        assert_eq!(c.acquire_timeout, None);
179        assert_eq!(c.max_lifetime, None);
180        assert_eq!(c.idle_timeout, None);
181        assert_eq!(c.max_uses, None);
182    }
183
184    #[test]
185    fn optional_setters_accept_an_explicit_some() {
186        let c = Config::new()
187            .acquire_timeout(Some(Duration::from_secs(1)))
188            .max_lifetime(Some(Duration::from_secs(2)))
189            .idle_timeout(Some(Duration::from_secs(3)))
190            .max_uses(Some(4u64));
191
192        assert_eq!(c.acquire_timeout, Some(Duration::from_secs(1)));
193        assert_eq!(c.max_lifetime, Some(Duration::from_secs(2)));
194        assert_eq!(c.idle_timeout, Some(Duration::from_secs(3)));
195        assert_eq!(c.max_uses, Some(4));
196    }
197
198    #[test]
199    #[should_panic(expected = "max_size must be greater than zero")]
200    fn a_zero_max_size_panics() {
201        // A shard that may hold nothing would make `acquire` wait forever.
202        let _ = Config::new().max_size(0);
203    }
204
205    #[test]
206    #[should_panic(expected = "reap_interval must be non-zero")]
207    fn a_zero_reap_interval_panics() {
208        // Otherwise the reaper becomes a busy loop on the runtime.
209        let _ = Config::new().reap_interval(Duration::ZERO);
210    }
211
212    #[test]
213    fn min_idle_is_stored_unclamped_but_read_back_clamped() {
214        let c = Config::new().max_size(2).min_idle(9);
215        assert_eq!(c.min_idle, 9, "the setter records what was asked for");
216        assert_eq!(c.effective_min_idle(), 2, "but never exceeds max_size");
217    }
218
219    #[test]
220    fn effective_min_idle_passes_through_below_max_size() {
221        assert_eq!(
222            Config::new().max_size(9).min_idle(2).effective_min_idle(),
223            2
224        );
225        assert_eq!(
226            Config::new().max_size(9).min_idle(0).effective_min_idle(),
227            0
228        );
229        assert_eq!(
230            Config::new().max_size(4).min_idle(4).effective_min_idle(),
231            4
232        );
233    }
234
235    #[test]
236    fn a_config_with_no_timed_policy_needs_no_reaper() {
237        assert!(!untimed().needs_reaper());
238    }
239
240    #[test]
241    fn each_timed_policy_on_its_own_needs_a_reaper() {
242        assert!(
243            untimed().min_idle(1).needs_reaper(),
244            "min_idle has to be refilled by someone"
245        );
246        assert!(
247            untimed()
248                .idle_timeout(Duration::from_secs(1))
249                .needs_reaper(),
250            "an idle connection nobody asks for ages out only in the reaper"
251        );
252        assert!(
253            untimed()
254                .max_lifetime(Duration::from_secs(1))
255                .needs_reaper(),
256            "same for the hard age cap"
257        );
258    }
259
260    /// `max_uses` is checked on the way out of the free list, so it never needs
261    /// a timer of its own.
262    #[test]
263    fn max_uses_alone_needs_no_reaper() {
264        assert!(!untimed().max_uses(1u64).needs_reaper());
265    }
266
267    #[test]
268    fn is_clone_and_debug() {
269        let c = Config::new().max_size(3).min_idle(1);
270        let d = c.clone();
271        assert_eq!(d.max_size, 3);
272        assert_eq!(d.min_idle, 1);
273        assert!(format!("{c:?}").contains("max_size"));
274    }
275}