hashiverse_server_lib/server/stats/
request_counts.rs1use hashiverse_lib::protocol::payload::payload::{PayloadRequestKind, PAYLOAD_REQUEST_KIND_COUNT};
2use hashiverse_lib::tools::decaying_counter::DecayingCounter;
3use hashiverse_lib::tools::time::{TimeMillis, MILLIS_IN_DAY, MILLIS_IN_HOUR, MILLIS_IN_MONTH};
4use parking_lot::Mutex;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7pub struct RequestRateWindows {
14 per_hour: DecayingCounter,
15 per_day: DecayingCounter,
16 per_month: DecayingCounter,
17}
18
19impl RequestRateWindows {
20 pub fn new() -> Self {
21 Self {
22 per_hour: DecayingCounter::new(MILLIS_IN_HOUR),
23 per_day: DecayingCounter::new(MILLIS_IN_DAY),
24 per_month: DecayingCounter::new(MILLIS_IN_MONTH),
25 }
26 }
27
28 pub fn record(&mut self, now: TimeMillis) {
30 self.per_hour.record(now, 1);
31 self.per_day.record(now, 1);
32 self.per_month.record(now, 1);
33 }
34}
35
36impl Default for RequestRateWindows {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42pub fn request_counts_subtree(totals: &[AtomicU64; PAYLOAD_REQUEST_KIND_COUNT], windows: &[Mutex<RequestRateWindows>; PAYLOAD_REQUEST_KIND_COUNT], now: TimeMillis) -> serde_json::Value {
50 let mut map = serde_json::Map::with_capacity(PAYLOAD_REQUEST_KIND_COUNT);
51 for index in 0..PAYLOAD_REQUEST_KIND_COUNT {
52 let kind = match PayloadRequestKind::from_u16(index as u16) {
53 Ok(kind) => kind,
54 Err(_) => continue,
55 };
56
57 let total = totals[index].load(Ordering::Relaxed);
58 let (per_hour, per_day, per_month) = {
59 let window = windows[index].lock();
60 (window.per_hour.estimate(now), window.per_day.estimate(now), window.per_month.estimate(now))
61 };
62
63 let entry = serde_json::json!({
64 "total": total,
65 "per_hour": per_hour.round() as u64,
66 "per_day": per_day.round() as u64,
67 "per_month": per_month.round() as u64,
68 });
69 map.insert(kind.to_string(), entry);
70 }
71 serde_json::Value::Object(map)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use hashiverse_lib::tools::time::MILLIS_IN_MINUTE;
78
79 fn fresh_state() -> ([AtomicU64; PAYLOAD_REQUEST_KIND_COUNT], [Mutex<RequestRateWindows>; PAYLOAD_REQUEST_KIND_COUNT]) {
80 (std::array::from_fn(|_| AtomicU64::new(0)), std::array::from_fn(|_| Mutex::new(RequestRateWindows::new())))
81 }
82
83 #[test]
84 fn subtree_has_nested_shape_per_kind() {
85 let (totals, windows) = fresh_state();
86 let doc = request_counts_subtree(&totals, &windows, TimeMillis::zero());
87
88 let ping = doc.get(&PayloadRequestKind::PingV1.to_string()).expect("PingV1 entry present");
89 for key in ["total", "per_hour", "per_day", "per_month"] {
90 assert!(ping.get(key).is_some(), "PingV1 entry should have {} key: {}", key, ping);
91 }
92 }
93
94 #[test]
95 fn records_reflect_in_total_and_windows() {
96 let (totals, windows) = fresh_state();
97 let ping_index = PayloadRequestKind::PingV1 as usize;
98
99 let mut now = TimeMillis::zero();
101 for _ in 0..60 {
102 now = TimeMillis(now.0 + MILLIS_IN_MINUTE.0);
103 totals[ping_index].fetch_add(1, Ordering::Relaxed);
104 windows[ping_index].lock().record(now);
105 }
106
107 let doc = request_counts_subtree(&totals, &windows, now);
108 let ping = doc.get(&PayloadRequestKind::PingV1.to_string()).unwrap();
109
110 assert_eq!(ping.get("total").unwrap().as_u64().unwrap(), 60);
111 let per_hour = ping.get("per_hour").unwrap().as_u64().unwrap();
114 assert!(per_hour > 0, "per_hour should be non-zero after 60 calls, got {}", per_hour);
115
116 let error = doc.get(&PayloadRequestKind::ErrorV1.to_string()).unwrap();
118 assert_eq!(error.get("total").unwrap().as_u64().unwrap(), 0);
119 assert_eq!(error.get("per_hour").unwrap().as_u64().unwrap(), 0);
120 }
121}