html2pdf_api/
stats.rs

1//! Pool statistics for monitoring and health checks.
2//!
3//! This module provides [`PoolStats`], a snapshot of the browser pool's
4//! current state. Use it for monitoring, logging, and health checks.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use html2pdf_api::BrowserPool;
10//!
11//! let pool = BrowserPool::builder()
12//!     .factory(Box::new(ChromeBrowserFactory::with_defaults()))
13//!     .build()?;
14//!
15//! let stats = pool.stats();
16//! println!("Available: {}, Active: {}", stats.available, stats.active);
17//! ```
18
19/// Snapshot of pool statistics at a point in time.
20///
21/// Useful for monitoring, logging, and health checks.
22///
23/// # Fields
24///
25/// | Field | Description |
26/// |-------|-------------|
27/// | `available` | Browsers ready for checkout |
28/// | `active` | All tracked browsers (pooled + checked-out) |
29/// | `total` | Reserved for future use (currently same as `active`) |
30///
31/// # Example
32///
33/// ```rust
34/// use html2pdf_api::PoolStats;
35///
36/// let stats = PoolStats {
37///     available: 3,
38///     active: 5,
39///     total: 5,
40/// };
41///
42/// println!("Pool status: {}/{} available", stats.available, stats.active);
43/// ```
44///
45/// # Usage with BrowserPool
46///
47/// ```rust,ignore
48/// let pool = /* ... */;
49///
50/// // Get current stats
51/// let stats = pool.stats();
52///
53/// // Use for health checks
54/// if stats.available == 0 {
55///     log::warn!("No browsers available in pool!");
56/// }
57///
58/// // Use for monitoring
59/// metrics::gauge!("browser_pool.available", stats.available as f64);
60/// metrics::gauge!("browser_pool.active", stats.active as f64);
61/// ```
62#[derive(Debug, Clone)]
63pub struct PoolStats {
64    /// Number of browsers available in pool (ready for checkout).
65    ///
66    /// These browsers are idle and can be immediately returned by
67    /// [`BrowserPool::get()`](crate::BrowserPool::get).
68    ///
69    /// # Note
70    ///
71    /// This value can change immediately after reading if another thread
72    /// checks out or returns a browser.
73    pub available: usize,
74
75    /// Number of active browsers (all browsers being tracked).
76    ///
77    /// This includes both pooled and checked-out browsers.
78    ///
79    /// # Relationship to `available`
80    ///
81    /// - `active` >= `available` (always)
82    /// - `active` - `available` = browsers currently checked out
83    pub active: usize,
84
85    /// Total browsers (currently same as active, reserved for future use).
86    ///
87    /// # Future Use
88    ///
89    /// This field may be used to track browsers in different states
90    /// (e.g., browsers being created, browsers being destroyed).
91    pub total: usize,
92}
93
94impl PoolStats {
95    /// Get the number of browsers currently checked out.
96    ///
97    /// This is a convenience method that calculates `active - available`.
98    ///
99    /// # Example
100    ///
101    /// ```rust
102    /// use html2pdf_api::PoolStats;
103    ///
104    /// let stats = PoolStats {
105    ///     available: 3,
106    ///     active: 5,
107    ///     total: 5,
108    /// };
109    ///
110    /// assert_eq!(stats.checked_out(), 2);
111    /// ```
112    #[inline]
113    pub fn checked_out(&self) -> usize {
114        self.active.saturating_sub(self.available)
115    }
116
117    /// Check if the pool has available browsers.
118    ///
119    /// # Example
120    ///
121    /// ```rust
122    /// use html2pdf_api::PoolStats;
123    ///
124    /// let stats = PoolStats {
125    ///     available: 3,
126    ///     active: 5,
127    ///     total: 5,
128    /// };
129    ///
130    /// assert!(stats.has_available());
131    /// ```
132    #[inline]
133    pub fn has_available(&self) -> bool {
134        self.available > 0
135    }
136
137    /// Check if the pool is empty (no browsers at all).
138    ///
139    /// # Example
140    ///
141    /// ```rust
142    /// use html2pdf_api::PoolStats;
143    ///
144    /// let stats = PoolStats {
145    ///     available: 0,
146    ///     active: 0,
147    ///     total: 0,
148    /// };
149    ///
150    /// assert!(stats.is_empty());
151    /// ```
152    #[inline]
153    pub fn is_empty(&self) -> bool {
154        self.active == 0
155    }
156}
157
158impl std::fmt::Display for PoolStats {
159    /// Format stats for logging.
160    ///
161    /// # Example
162    ///
163    /// ```rust
164    /// use html2pdf_api::PoolStats;
165    ///
166    /// let stats = PoolStats {
167    ///     available: 3,
168    ///     active: 5,
169    ///     total: 5,
170    /// };
171    ///
172    /// assert_eq!(
173    ///     stats.to_string(),
174    ///     "PoolStats { available: 3, active: 5, total: 5 }"
175    /// );
176    /// ```
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        write!(
179            f,
180            "PoolStats {{ available: {}, active: {}, total: {} }}",
181            self.available, self.active, self.total
182        )
183    }
184}
185
186// ============================================================================
187// Unit Tests
188// ============================================================================
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    /// Verifies PoolStats structure and field access.
195    ///
196    /// PoolStats is a simple data structure returned by `pool.stats()`.
197    /// This test ensures the structure is correctly defined.
198    #[test]
199    fn test_pool_stats_structure() {
200        let stats = PoolStats {
201            available: 5,
202            active: 3,
203            total: 8,
204        };
205
206        assert_eq!(
207            stats.available, 5,
208            "Available browsers should be accessible"
209        );
210        assert_eq!(stats.active, 3, "Active browsers should be accessible");
211        assert_eq!(stats.total, 8, "Total browsers should be accessible");
212    }
213
214    /// Verifies the checked_out() convenience method.
215    #[test]
216    fn test_checked_out() {
217        let stats = PoolStats {
218            available: 2,
219            active: 5,
220            total: 5,
221        };
222
223        assert_eq!(stats.checked_out(), 3);
224    }
225
226    /// Verifies checked_out() handles edge case where available > active.
227    #[test]
228    fn test_checked_out_saturating() {
229        // Edge case: shouldn't happen in practice, but handle gracefully
230        let stats = PoolStats {
231            available: 10,
232            active: 5,
233            total: 5,
234        };
235
236        assert_eq!(stats.checked_out(), 0); // saturating_sub prevents underflow
237    }
238
239    /// Verifies has_available() method.
240    #[test]
241    fn test_has_available() {
242        let stats_with = PoolStats {
243            available: 1,
244            active: 1,
245            total: 1,
246        };
247        assert!(stats_with.has_available());
248
249        let stats_without = PoolStats {
250            available: 0,
251            active: 1,
252            total: 1,
253        };
254        assert!(!stats_without.has_available());
255    }
256
257    /// Verifies is_empty() method.
258    #[test]
259    fn test_is_empty() {
260        let empty = PoolStats {
261            available: 0,
262            active: 0,
263            total: 0,
264        };
265        assert!(empty.is_empty());
266
267        let not_empty = PoolStats {
268            available: 0,
269            active: 1,
270            total: 1,
271        };
272        assert!(!not_empty.is_empty());
273    }
274
275    /// Verifies Display implementation.
276    #[test]
277    fn test_display() {
278        let stats = PoolStats {
279            available: 3,
280            active: 5,
281            total: 5,
282        };
283
284        assert_eq!(
285            stats.to_string(),
286            "PoolStats { available: 3, active: 5, total: 5 }"
287        );
288    }
289
290    /// Verifies that PoolStats implements Clone.
291    #[test]
292    fn test_clone() {
293        let stats = PoolStats {
294            available: 3,
295            active: 5,
296            total: 5,
297        };
298
299        let cloned = stats.clone();
300        assert_eq!(cloned.available, stats.available);
301        assert_eq!(cloned.active, stats.active);
302        assert_eq!(cloned.total, stats.total);
303    }
304
305    /// Verifies that PoolStats implements Debug.
306    #[test]
307    fn test_debug() {
308        let stats = PoolStats {
309            available: 3,
310            active: 5,
311            total: 5,
312        };
313
314        let debug_str = format!("{:?}", stats);
315        assert!(debug_str.contains("PoolStats"));
316        assert!(debug_str.contains("available"));
317    }
318}