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}