Skip to main content

nntp_proxy/types/
pool.rs

1//! Connection pool metric newtypes
2
3use nutype::nutype;
4use std::fmt;
5
6#[allow(clippy::cast_precision_loss)] // Pool percentages are display values derived from exact usize counters.
7const fn pool_count_as_f64(value: usize) -> f64 {
8    // Pool utilization is a percentage for display/thresholding. The exact
9    // connection counts remain available as usize newtypes.
10    value as f64
11}
12
13/// Macro to reduce boilerplate for pool connection count types.
14/// All pool types have the same `zero()` and `get()` implementations.
15macro_rules! pool_count_type {
16    ($(#[$meta:meta])* $name:ident) => {
17        $(#[$meta])*
18        #[nutype(derive(
19            Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Display, From
20        ))]
21        pub struct $name(usize);
22
23        impl $name {
24            #[inline]
25            pub fn zero() -> Self {
26                Self::new(0)
27            }
28
29            #[inline]
30            #[must_use]
31            pub fn get(&self) -> usize {
32                self.into_inner()
33            }
34        }
35    };
36}
37
38pool_count_type!(
39    /// Number of available connections in the pool
40    AvailableConnections
41);
42
43pool_count_type!(
44    /// Maximum size of the connection pool
45    MaxPoolSize
46);
47
48pool_count_type!(
49    /// Total number of connections created in the pool's lifetime
50    CreatedConnections
51);
52
53pool_count_type!(
54    /// Number of connections currently in use
55    InUseConnections
56);
57
58impl InUseConnections {
59    /// Calculate from pool capacity and availability
60    #[inline]
61    #[must_use]
62    pub fn from_pool_stats(max: MaxPoolSize, available: AvailableConnections) -> Self {
63        Self::new(max.get().saturating_sub(available.get()))
64    }
65}
66
67/// Pool utilization as a percentage (0-100)
68///
69/// Calculated as: (`in_use` / `max_size`) * 100
70/// Useful for monitoring pool health and load.
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct PoolUtilization(f64);
73
74impl PoolUtilization {
75    /// Create a new pool utilization percentage
76    ///
77    /// # Panics
78    /// Panics if percentage is not in range [0.0, 100.0]
79    #[inline]
80    #[must_use]
81    pub fn new(percentage: f64) -> Self {
82        assert!(
83            (0.0..=100.0).contains(&percentage),
84            "Utilization must be 0-100%, got {percentage}"
85        );
86        Self(percentage)
87    }
88
89    /// Calculate utilization from pool stats
90    #[inline]
91    #[must_use]
92    pub fn from_pool_stats(max: MaxPoolSize, available: AvailableConnections) -> Self {
93        let max_size = max.get();
94        if max_size == 0 {
95            return Self(0.0);
96        }
97
98        let in_use = max_size.saturating_sub(available.get());
99        let utilization = (pool_count_as_f64(in_use) / pool_count_as_f64(max_size)) * 100.0;
100        Self(utilization)
101    }
102
103    #[inline]
104    #[must_use]
105    pub const fn as_percentage(self) -> f64 {
106        self.0
107    }
108
109    #[inline]
110    #[must_use]
111    pub fn is_full(self) -> bool {
112        self.0 >= 100.0
113    }
114
115    #[inline]
116    #[must_use]
117    pub fn is_empty(self) -> bool {
118        self.0 == 0.0
119    }
120
121    #[inline]
122    #[must_use]
123    pub fn is_high_load(self) -> bool {
124        self.0 >= 80.0
125    }
126}
127
128impl fmt::Display for PoolUtilization {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{:.1}%", self.0)
131    }
132}
133
134#[cfg(test)]
135#[allow(clippy::float_cmp)] // These tests compare exact percentage values from bounded pool math.
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_available_connections() {
141        let available = AvailableConnections::new(5);
142        assert_eq!(available.get(), 5);
143        assert_eq!(format!("{available}"), "5");
144
145        let zero = AvailableConnections::zero();
146        assert_eq!(zero.get(), 0);
147    }
148
149    #[test]
150    fn test_max_pool_size() {
151        let max = MaxPoolSize::new(10);
152        assert_eq!(max.get(), 10);
153        assert_eq!(format!("{max}"), "10");
154    }
155
156    #[test]
157    fn test_created_connections() {
158        let created = CreatedConnections::new(25);
159        assert_eq!(created.get(), 25);
160        assert_eq!(format!("{created}"), "25");
161
162        let zero = CreatedConnections::zero();
163        assert_eq!(zero.get(), 0);
164    }
165
166    #[test]
167    fn test_in_use_connections() {
168        let max = MaxPoolSize::new(10);
169        let available = AvailableConnections::new(3);
170        let in_use = InUseConnections::from_pool_stats(max, available);
171        assert_eq!(in_use.get(), 7);
172    }
173
174    #[test]
175    fn test_in_use_saturating() {
176        let max = MaxPoolSize::new(5);
177        let available = AvailableConnections::new(10);
178        let in_use = InUseConnections::from_pool_stats(max, available);
179        assert_eq!(in_use.get(), 0);
180    }
181
182    #[test]
183    fn test_pool_utilization() {
184        let max = MaxPoolSize::new(10);
185        let available = AvailableConnections::new(3);
186        let utilization = PoolUtilization::from_pool_stats(max, available);
187        assert_eq!(utilization.as_percentage(), 70.0);
188        assert_eq!(format!("{utilization}"), "70.0%");
189    }
190
191    #[test]
192    fn test_pool_utilization_full() {
193        let max = MaxPoolSize::new(10);
194        let available = AvailableConnections::new(0);
195        let utilization = PoolUtilization::from_pool_stats(max, available);
196        assert!(utilization.is_full());
197        assert!(utilization.is_high_load());
198        assert!(!utilization.is_empty());
199    }
200
201    #[test]
202    fn test_pool_utilization_empty() {
203        let max = MaxPoolSize::new(10);
204        let available = AvailableConnections::new(10);
205        let utilization = PoolUtilization::from_pool_stats(max, available);
206        assert!(utilization.is_empty());
207        assert!(!utilization.is_full());
208        assert!(!utilization.is_high_load());
209    }
210
211    #[test]
212    fn test_pool_utilization_high_load() {
213        let max = MaxPoolSize::new(10);
214        let available = AvailableConnections::new(1);
215        let utilization = PoolUtilization::from_pool_stats(max, available);
216        assert!(utilization.is_high_load());
217        assert!(!utilization.is_full());
218    }
219
220    #[test]
221    fn test_pool_utilization_zero_max() {
222        let max = MaxPoolSize::new(0);
223        let available = AvailableConnections::new(0);
224        let utilization = PoolUtilization::from_pool_stats(max, available);
225        assert_eq!(utilization.as_percentage(), 0.0);
226    }
227
228    #[test]
229    #[should_panic(expected = "Utilization must be 0-100%")]
230    fn test_pool_utilization_invalid() {
231        let _ = PoolUtilization::new(150.0);
232    }
233
234    #[test]
235    fn test_ordering() {
236        let small = AvailableConnections::new(1);
237        let large = AvailableConnections::new(10);
238        assert!(small < large);
239        assert_eq!(small, small);
240    }
241
242    #[test]
243    fn test_from_conversions() {
244        let available: AvailableConnections = 5usize.into();
245        assert_eq!(available.get(), 5);
246
247        let max: MaxPoolSize = 10usize.into();
248        assert_eq!(max.get(), 10);
249
250        let created: CreatedConnections = 25usize.into();
251        assert_eq!(created.get(), 25);
252    }
253}