Skip to main content

connpool/policy/
config.rs

1//! `PoolConfig`: the immutable, validated pool configuration, and `IdleStrategy`.
2
3use crate::core::errors::PoolError;
4use crate::resilience::breaker::BreakerConfig;
5use crate::resilience::retry::RetryPolicy;
6
7/// Which idle connection is reused first.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum IdleStrategy {
10    /// Reuse the hottest connection; cold ones age out and get evicted.
11    Lifo,
12    /// Reuse the oldest idle connection; spreads load / rotates evenly.
13    Fifo,
14}
15
16impl IdleStrategy {
17    /// The snake_case wire string for this strategy.
18    pub fn as_str(&self) -> &'static str {
19        match self {
20            IdleStrategy::Lifo => "lifo",
21            IdleStrategy::Fifo => "fifo",
22        }
23    }
24}
25
26impl std::fmt::Display for IdleStrategy {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.write_str(self.as_str())
29    }
30}
31
32/// An immutable, validated pool configuration. Build one with struct-update syntax from
33/// [`PoolConfig::default`], then hand it to a pool — which re-validates it.
34///
35/// ```
36/// use connpool::PoolConfig;
37/// let cfg = PoolConfig { max_size: 20, min_size: 2, ..Default::default() };
38/// assert!(cfg.validate().is_ok());
39/// ```
40#[derive(Debug, Clone)]
41pub struct PoolConfig {
42    // --- sizing ---
43    /// Minimum connections kept warm by `open()` and the maintenance pass.
44    pub min_size: usize,
45    /// Hard cap on total connections (idle + in use + being created).
46    pub max_size: usize,
47
48    // --- timeouts (seconds; None = no limit) ---
49    /// Default wait in `acquire()`.
50    pub acquire_timeout: Option<f64>,
51    /// Enforced by the async pool only; blocking factories must enforce their own connect
52    /// timeout.
53    pub create_timeout: Option<f64>,
54    /// Backpressure: cap callers blocked in `acquire()` (None = unlimited; 0 = never wait
55    /// when full).
56    pub max_waiters: Option<usize>,
57
58    // --- rotation / recycling ---
59    /// Retire connections after this age.
60    pub max_lifetime: Option<f64>,
61    /// Shave up to this fraction off each lifetime so a whole pool never expires at once.
62    pub lifetime_jitter: f64,
63    /// Evict idle connections (down to `min_size`).
64    pub max_idle_time: Option<f64>,
65    /// Retire after N checkouts.
66    pub max_uses: Option<u64>,
67    /// LIFO or FIFO idle reuse.
68    pub idle_strategy: IdleStrategy,
69
70    // --- health ---
71    /// Validate on checkout if idle >= this (0 = always, None = never).
72    pub validate_after_idle: Option<f64>,
73    /// Background sweep period (None = off).
74    pub maintenance_interval: Option<f64>,
75    /// Warn when a borrowed connection is held longer than this (None = off); needs `open()`.
76    pub leak_detection_timeout: Option<f64>,
77    /// Validate idle connections during the maintenance sweep and evict dead ones (needs
78    /// `open()`; adds I/O).
79    pub validate_idle: bool,
80
81    // --- resilience ---
82    /// Retry policy for `factory.create()`.
83    pub retry: RetryPolicy,
84    /// Circuit breaker config (None = off).
85    pub breaker: Option<BreakerConfig>,
86}
87
88impl Default for PoolConfig {
89    fn default() -> Self {
90        Self {
91            min_size: 0,
92            max_size: 10,
93            acquire_timeout: Some(30.0),
94            create_timeout: Some(10.0),
95            max_waiters: None,
96            max_lifetime: Some(1800.0),
97            lifetime_jitter: 0.1,
98            max_idle_time: Some(600.0),
99            max_uses: None,
100            idle_strategy: IdleStrategy::Lifo,
101            validate_after_idle: Some(30.0),
102            maintenance_interval: Some(30.0),
103            leak_detection_timeout: None,
104            validate_idle: false,
105            retry: RetryPolicy::default(),
106            breaker: Some(BreakerConfig::default()),
107        }
108    }
109}
110
111impl PoolConfig {
112    /// Checks the config's invariants, returning [`PoolError::Config`] on the first violation.
113    pub fn validate(&self) -> Result<(), PoolError> {
114        let need = |ok: bool, msg: &str| -> Result<(), PoolError> {
115            if ok {
116                Ok(())
117            } else {
118                Err(PoolError::Config(msg.to_string()))
119            }
120        };
121
122        need(self.max_size >= 1, "max_size must be >= 1")?;
123        need(
124            self.min_size <= self.max_size,
125            "min_size must be within [0, max_size]",
126        )?;
127        need(
128            self.acquire_timeout.map(|v| v >= 0.0).unwrap_or(true),
129            "acquire_timeout must be >= 0",
130        )?;
131        for (value, name) in [
132            (self.create_timeout, "create_timeout"),
133            (self.max_lifetime, "max_lifetime"),
134            (self.max_idle_time, "max_idle_time"),
135            (self.maintenance_interval, "maintenance_interval"),
136            (self.leak_detection_timeout, "leak_detection_timeout"),
137        ] {
138            need(
139                value.map(|v| v > 0.0).unwrap_or(true),
140                &format!("{name} must be > 0 or None"),
141            )?;
142        }
143        need(
144            self.validate_after_idle.map(|v| v >= 0.0).unwrap_or(true),
145            "validate_after_idle must be >= 0 or None",
146        )?;
147        need(
148            (0.0..=1.0).contains(&self.lifetime_jitter),
149            "lifetime_jitter must be within [0, 1]",
150        )?;
151        need(
152            self.max_uses.map(|v| v >= 1).unwrap_or(true),
153            "max_uses must be >= 1 or None",
154        )?;
155        // max_waiters is usize so it is always >= 0; kept for parity of intent.
156
157        self.retry.validate()?;
158        if let Some(b) = &self.breaker {
159            b.validate()?;
160        }
161        Ok(())
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn idle_strategy_wire_values() {
171        assert_eq!(IdleStrategy::Lifo.as_str(), "lifo");
172        assert_eq!(IdleStrategy::Fifo.as_str(), "fifo");
173        assert_eq!(IdleStrategy::Fifo.to_string(), "fifo");
174        assert_eq!(format!("{:?}", IdleStrategy::Lifo), "Lifo");
175    }
176
177    #[test]
178    fn defaults_match_spec() {
179        let c = PoolConfig::default();
180        assert_eq!(c.min_size, 0);
181        assert_eq!(c.max_size, 10);
182        assert_eq!(c.acquire_timeout, Some(30.0));
183        assert_eq!(c.create_timeout, Some(10.0));
184        assert_eq!(c.max_waiters, None);
185        assert_eq!(c.max_lifetime, Some(1800.0));
186        assert_eq!(c.lifetime_jitter, 0.1);
187        assert_eq!(c.max_idle_time, Some(600.0));
188        assert_eq!(c.max_uses, None);
189        assert_eq!(c.idle_strategy, IdleStrategy::Lifo);
190        assert_eq!(c.validate_after_idle, Some(30.0));
191        assert_eq!(c.maintenance_interval, Some(30.0));
192        assert_eq!(c.leak_detection_timeout, None);
193        assert!(!c.validate_idle);
194        assert!(c.breaker.is_some());
195        assert!(c.validate().is_ok());
196        assert!(format!("{c:?}").contains("PoolConfig"));
197        assert!(c.clone().validate().is_ok());
198    }
199
200    #[test]
201    fn validation_rejects_bad_values() {
202        let bad = |c: PoolConfig| c.validate().unwrap_err().to_string();
203        assert_eq!(
204            bad(PoolConfig {
205                max_size: 0,
206                ..Default::default()
207            }),
208            "max_size must be >= 1"
209        );
210        assert_eq!(
211            bad(PoolConfig {
212                min_size: 5,
213                max_size: 4,
214                ..Default::default()
215            }),
216            "min_size must be within [0, max_size]"
217        );
218        assert_eq!(
219            bad(PoolConfig {
220                acquire_timeout: Some(-1.0),
221                ..Default::default()
222            }),
223            "acquire_timeout must be >= 0"
224        );
225        assert_eq!(
226            bad(PoolConfig {
227                create_timeout: Some(0.0),
228                ..Default::default()
229            }),
230            "create_timeout must be > 0 or None"
231        );
232        assert_eq!(
233            bad(PoolConfig {
234                max_lifetime: Some(-2.0),
235                ..Default::default()
236            }),
237            "max_lifetime must be > 0 or None"
238        );
239        assert_eq!(
240            bad(PoolConfig {
241                validate_after_idle: Some(-1.0),
242                ..Default::default()
243            }),
244            "validate_after_idle must be >= 0 or None"
245        );
246        assert_eq!(
247            bad(PoolConfig {
248                lifetime_jitter: 1.5,
249                ..Default::default()
250            }),
251            "lifetime_jitter must be within [0, 1]"
252        );
253        assert_eq!(
254            bad(PoolConfig {
255                max_uses: Some(0),
256                ..Default::default()
257            }),
258            "max_uses must be >= 1 or None"
259        );
260        // propagates retry + breaker validation
261        assert!(
262            PoolConfig {
263                retry: RetryPolicy {
264                    max_attempts: 0,
265                    ..Default::default()
266                },
267                ..Default::default()
268            }
269            .validate()
270            .is_err()
271        );
272        assert!(
273            PoolConfig {
274                breaker: Some(BreakerConfig {
275                    failure_threshold: 0,
276                    ..Default::default()
277                }),
278                ..Default::default()
279            }
280            .validate()
281            .is_err()
282        );
283        // None-valued optionals are accepted
284        assert!(
285            PoolConfig {
286                create_timeout: None,
287                max_lifetime: None,
288                max_idle_time: None,
289                maintenance_interval: None,
290                leak_detection_timeout: None,
291                validate_after_idle: None,
292                acquire_timeout: None,
293                breaker: None,
294                ..Default::default()
295            }
296            .validate()
297            .is_ok()
298        );
299    }
300}