1use std::collections::BTreeMap;
4use std::error::Error;
5use std::sync::Mutex;
6
7use crate::core::connection::{CloseReason, PooledConnection};
8use crate::observability::events::PoolListener;
9use crate::resilience::breaker::BreakerState;
10
11const ACQUIRE_WAIT_WINDOW: usize = 1024;
15
16#[derive(Debug, Clone, PartialEq)]
18pub enum StatValue {
19 Str(String),
21 Int(i64),
23 Float(f64),
25 Map(BTreeMap<String, i64>),
27 Null,
29}
30
31fn percentile(sorted: &[f64], q: f64) -> f64 {
33 if sorted.is_empty() {
34 return 0.0;
35 }
36 let k = (sorted.len() - 1) as f64 * q;
37 let lo = k as usize;
38 let hi = (lo + 1).min(sorted.len() - 1);
39 sorted[lo] + (sorted[hi] - sorted[lo]) * (k - lo as f64)
40}
41
42fn average(values: &[f64]) -> f64 {
43 if values.is_empty() {
44 return 0.0;
45 }
46 values.iter().sum::<f64>() / values.len() as f64
47}
48
49#[derive(Debug, Clone, PartialEq)]
52pub struct PoolStats {
53 pub name: String,
55 pub max_size: usize,
57 pub size: usize,
59 pub idle: usize,
61 pub in_use: usize,
63 pub waiting: usize,
65
66 pub created: u64,
68 pub create_failures: u64,
70 pub acquired: u64,
72 pub released: u64,
74 pub timeouts: u64,
76 pub closed: u64,
78 pub leaks: u64,
80 pub closed_by_reason: BTreeMap<String, i64>,
82
83 pub avg_acquire_wait: f64,
85 pub p50_acquire_wait: f64,
87 pub p95_acquire_wait: f64,
89 pub p99_acquire_wait: f64,
91
92 pub avg_create_latency: f64,
94 pub p50_create_latency: f64,
96 pub p95_create_latency: f64,
98 pub p99_create_latency: f64,
100
101 pub breaker_state: Option<String>,
103}
104
105impl PoolStats {
106 pub fn to_dictionary(&self) -> BTreeMap<&'static str, StatValue> {
108 let mut m = BTreeMap::new();
109 m.insert("name", StatValue::Str(self.name.clone()));
110 m.insert("max_size", StatValue::Int(self.max_size as i64));
111 m.insert("size", StatValue::Int(self.size as i64));
112 m.insert("idle", StatValue::Int(self.idle as i64));
113 m.insert("in_use", StatValue::Int(self.in_use as i64));
114 m.insert("waiting", StatValue::Int(self.waiting as i64));
115 m.insert("created", StatValue::Int(self.created as i64));
116 m.insert("create_failures", StatValue::Int(self.create_failures as i64));
117 m.insert("acquired", StatValue::Int(self.acquired as i64));
118 m.insert("released", StatValue::Int(self.released as i64));
119 m.insert("timeouts", StatValue::Int(self.timeouts as i64));
120 m.insert("closed", StatValue::Int(self.closed as i64));
121 m.insert("leaks", StatValue::Int(self.leaks as i64));
122 m.insert("closed_by_reason", StatValue::Map(self.closed_by_reason.clone()));
123 m.insert("avg_acquire_wait", StatValue::Float(self.avg_acquire_wait));
124 m.insert("p50_acquire_wait", StatValue::Float(self.p50_acquire_wait));
125 m.insert("p95_acquire_wait", StatValue::Float(self.p95_acquire_wait));
126 m.insert("p99_acquire_wait", StatValue::Float(self.p99_acquire_wait));
127 m.insert("avg_create_latency", StatValue::Float(self.avg_create_latency));
128 m.insert("p50_create_latency", StatValue::Float(self.p50_create_latency));
129 m.insert("p95_create_latency", StatValue::Float(self.p95_create_latency));
130 m.insert("p99_create_latency", StatValue::Float(self.p99_create_latency));
131 m.insert(
132 "breaker_state",
133 match &self.breaker_state {
134 Some(s) => StatValue::Str(s.clone()),
135 None => StatValue::Null,
136 },
137 );
138 m
139 }
140}
141
142pub(crate) struct StatsGauges {
144 pub name: String,
145 pub max_size: usize,
146 pub size: usize,
147 pub idle: usize,
148 pub in_use: usize,
149 pub waiting: usize,
150 pub breaker_state: Option<String>,
151}
152
153struct Counters {
154 created: u64,
155 create_failures: u64,
156 acquired: u64,
157 released: u64,
158 timeouts: u64,
159 closed: u64,
160 leaks: u64,
161 by_reason: BTreeMap<String, i64>,
162 waits: Vec<f64>,
163 creates: Vec<f64>,
164}
165
166pub(crate) struct StatsCollector<T> {
169 inner: Mutex<Counters>,
170 _marker: std::marker::PhantomData<fn(T)>,
171}
172
173fn push_window(window: &mut Vec<f64>, value: f64) {
174 window.push(value);
175 if window.len() > ACQUIRE_WAIT_WINDOW {
176 let overflow = window.len() - ACQUIRE_WAIT_WINDOW;
177 window.drain(0..overflow);
178 }
179}
180
181impl<T> StatsCollector<T> {
182 pub(crate) fn new() -> Self {
183 Self {
184 inner: Mutex::new(Counters {
185 created: 0,
186 create_failures: 0,
187 acquired: 0,
188 released: 0,
189 timeouts: 0,
190 closed: 0,
191 leaks: 0,
192 by_reason: BTreeMap::new(),
193 waits: Vec::new(),
194 creates: Vec::new(),
195 }),
196 _marker: std::marker::PhantomData,
197 }
198 }
199
200 pub(crate) fn snapshot(&self, g: StatsGauges) -> PoolStats {
201 let c = self.inner.lock().unwrap();
202 let mut waits = c.waits.clone();
203 waits.sort_by(|a, b| a.partial_cmp(b).unwrap());
204 let mut creates = c.creates.clone();
205 creates.sort_by(|a, b| a.partial_cmp(b).unwrap());
206 PoolStats {
207 name: g.name,
208 max_size: g.max_size,
209 size: g.size,
210 idle: g.idle,
211 in_use: g.in_use,
212 waiting: g.waiting,
213 created: c.created,
214 create_failures: c.create_failures,
215 acquired: c.acquired,
216 released: c.released,
217 timeouts: c.timeouts,
218 closed: c.closed,
219 leaks: c.leaks,
220 closed_by_reason: c.by_reason.clone(),
221 avg_acquire_wait: average(&waits),
222 p50_acquire_wait: percentile(&waits, 0.50),
223 p95_acquire_wait: percentile(&waits, 0.95),
224 p99_acquire_wait: percentile(&waits, 0.99),
225 avg_create_latency: average(&creates),
226 p50_create_latency: percentile(&creates, 0.50),
227 p95_create_latency: percentile(&creates, 0.95),
228 p99_create_latency: percentile(&creates, 0.99),
229 breaker_state: g.breaker_state,
230 }
231 }
232}
233
234impl<T> PoolListener<T> for StatsCollector<T> {
235 fn on_connection_created(&self, _conn: &PooledConnection<T>, duration: f64) {
236 let mut c = self.inner.lock().unwrap();
237 c.created += 1;
238 push_window(&mut c.creates, duration);
239 }
240
241 fn on_connection_closed(&self, _conn: &PooledConnection<T>, reason: CloseReason) {
242 let mut c = self.inner.lock().unwrap();
243 c.closed += 1;
244 *c.by_reason.entry(reason.as_str().to_string()).or_insert(0) += 1;
245 }
246
247 fn on_create_failed(&self, _error: &(dyn Error + 'static)) {
248 self.inner.lock().unwrap().create_failures += 1;
249 }
250
251 fn on_acquired(&self, _conn: &PooledConnection<T>, wait_time: f64) {
252 let mut c = self.inner.lock().unwrap();
253 c.acquired += 1;
254 push_window(&mut c.waits, wait_time);
255 }
256
257 fn on_released(&self, _conn: &PooledConnection<T>, _hold_time: f64) {
258 self.inner.lock().unwrap().released += 1;
259 }
260
261 fn on_acquire_timeout(&self, _waited: f64) {
262 self.inner.lock().unwrap().timeouts += 1;
263 }
264
265 fn on_connection_leak(&self, _conn: &PooledConnection<T>, _held_for: f64) {
266 self.inner.lock().unwrap().leaks += 1;
267 }
268
269 fn on_breaker_state_change(&self, _old: BreakerState, _new: BreakerState) {}
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 fn gauges() -> StatsGauges {
277 StatsGauges {
278 name: "p".into(),
279 max_size: 10,
280 size: 2,
281 idle: 1,
282 in_use: 1,
283 waiting: 0,
284 breaker_state: None,
285 }
286 }
287
288 #[test]
289 fn percentile_and_average_edge_cases() {
290 assert_eq!(percentile(&[], 0.5), 0.0);
291 assert_eq!(average(&[]), 0.0);
292 let v = vec![1.0, 2.0, 3.0, 4.0];
293 assert_eq!(percentile(&v, 0.0), 1.0);
294 assert_eq!(percentile(&v, 1.0), 4.0);
295 assert_eq!(percentile(&v, 0.5), 2.5); assert_eq!(average(&v), 2.5);
297 }
298
299 #[test]
300 fn collector_counts_and_snapshots() {
301 let c = StatsCollector::<u32>::new();
302 let pc = PooledConnection::new(1u32, 0.0, None);
303 c.on_connection_created(&pc, 0.2);
304 c.on_connection_created(&pc, 0.4);
305 c.on_acquired(&pc, 0.1);
306 c.on_released(&pc, 1.0);
307 c.on_acquire_timeout(2.0);
308 c.on_connection_closed(&pc, CloseReason::Idle);
309 c.on_connection_closed(&pc, CloseReason::Idle);
310 c.on_connection_leak(&pc, 5.0);
311 let e: Box<dyn Error + Send + Sync> = "x".into();
312 c.on_create_failed(&*e);
313 c.on_breaker_state_change(BreakerState::Closed, BreakerState::Open);
314
315 let s = c.snapshot(gauges());
316 assert_eq!(s.created, 2);
317 assert_eq!(s.acquired, 1);
318 assert_eq!(s.released, 1);
319 assert_eq!(s.timeouts, 1);
320 assert_eq!(s.closed, 2);
321 assert_eq!(s.leaks, 1);
322 assert_eq!(s.create_failures, 1);
323 assert_eq!(s.closed_by_reason.get("idle"), Some(&2));
324 assert!((s.avg_create_latency - 0.3).abs() < 1e-9);
325 assert_eq!(s.name, "p");
326 assert_eq!(s.max_size, 10);
327 }
328
329 #[test]
330 fn to_dictionary_has_snakecase_keys() {
331 let c = StatsCollector::<u32>::new();
332 let mut g = gauges();
333 g.breaker_state = Some("closed".into());
334 let d = c.snapshot(g).to_dictionary();
335 for key in [
336 "name",
337 "max_size",
338 "size",
339 "idle",
340 "in_use",
341 "waiting",
342 "created",
343 "create_failures",
344 "acquired",
345 "released",
346 "timeouts",
347 "closed",
348 "leaks",
349 "closed_by_reason",
350 "avg_acquire_wait",
351 "p50_acquire_wait",
352 "p95_acquire_wait",
353 "p99_acquire_wait",
354 "avg_create_latency",
355 "p50_create_latency",
356 "p95_create_latency",
357 "p99_create_latency",
358 "breaker_state",
359 ] {
360 assert!(d.contains_key(key), "missing key {key}");
361 }
362 assert_eq!(d["breaker_state"], StatValue::Str("closed".into()));
363 assert_eq!(d["name"], StatValue::Str("p".into()));
364 assert_eq!(d["max_size"], StatValue::Int(10));
365 let d2 = c.snapshot(gauges()).to_dictionary();
367 assert_eq!(d2["breaker_state"], StatValue::Null);
368 }
369
370 #[test]
371 fn window_is_bounded() {
372 let c = StatsCollector::<u32>::new();
373 let pc = PooledConnection::new(1u32, 0.0, None);
374 for i in 0..(ACQUIRE_WAIT_WINDOW + 50) {
375 c.on_acquired(&pc, i as f64);
376 }
377 let inner = c.inner.lock().unwrap();
378 assert_eq!(inner.waits.len(), ACQUIRE_WAIT_WINDOW);
379 assert_eq!(inner.waits[0], 50.0);
381 }
382}