Skip to main content

mockforge_foundation/
rate_counters.rs

1//! Per-second rate counters for HTTP traffic.
2//!
3//! Simple monotonic atomic counters. The HTTP metrics middleware bumps
4//! the response counters; the TCP-accept counter is bumped at the listener
5//! level (see `mockforge-http`'s `CountingTcpListener` and `mockforge-chaos`'s
6//! `ChaosTcpListener`).
7//!
8//! These are sampled at fixed intervals by the admin dashboard collector
9//! to derive per-second rates:
10//!
11//! * **TPS** — successful (2xx/3xx) responses per second
12//! * **RPS** — 200-OK responses per second
13//! * **CPS** — accepted TCP connections per second  *(plain HTTP only;
14//!   the TLS path uses `axum_server`'s own accept loop and is not yet
15//!   instrumented)*
16//!
17//! The "successful API transaction" definition for TPS is `200..=399`,
18//! matching how load-testing tools (k6, JMeter, etc.) classify a successful
19//! request — anything that wasn't a 4xx/5xx error.
20
21use std::sync::atomic::{AtomicU64, Ordering};
22
23/// Total successful HTTP responses (status `200..=399`).
24pub static SUCCESSFUL_RESPONSES_TOTAL: AtomicU64 = AtomicU64::new(0);
25
26/// Total HTTP `200 OK` responses.
27pub static OK_RESPONSES_TOTAL: AtomicU64 = AtomicU64::new(0);
28
29/// Total accepted TCP connections (plain HTTP path only — see module docs).
30pub static HTTP_ACCEPTS_TOTAL: AtomicU64 = AtomicU64::new(0);
31
32/// Bump the response counters according to the response's status code.
33#[inline]
34pub fn record_response(status_code: u16) {
35    if (200..=399).contains(&status_code) {
36        SUCCESSFUL_RESPONSES_TOTAL.fetch_add(1, Ordering::Relaxed);
37    }
38    if status_code == 200 {
39        OK_RESPONSES_TOTAL.fetch_add(1, Ordering::Relaxed);
40    }
41}
42
43/// Bump the TCP-accept counter. Call once per accepted connection.
44#[inline]
45pub fn record_accept() {
46    HTTP_ACCEPTS_TOTAL.fetch_add(1, Ordering::Relaxed);
47}
48
49/// Point-in-time snapshot of the rate counters.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub struct CounterSnapshot {
52    pub successful: u64,
53    pub ok: u64,
54    pub accepts: u64,
55}
56
57/// Read all counters atomically (each load is independent — a snapshot
58/// is not transactional, but for sampling rates this is fine).
59pub fn snapshot() -> CounterSnapshot {
60    CounterSnapshot {
61        successful: SUCCESSFUL_RESPONSES_TOTAL.load(Ordering::Relaxed),
62        ok: OK_RESPONSES_TOTAL.load(Ordering::Relaxed),
63        accepts: HTTP_ACCEPTS_TOTAL.load(Ordering::Relaxed),
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use std::sync::Mutex;
71
72    // The counters are global; serialize tests to avoid cross-test interference.
73    static TEST_LOCK: Mutex<()> = Mutex::new(());
74
75    fn reset_counters() {
76        SUCCESSFUL_RESPONSES_TOTAL.store(0, Ordering::Relaxed);
77        OK_RESPONSES_TOTAL.store(0, Ordering::Relaxed);
78        HTTP_ACCEPTS_TOTAL.store(0, Ordering::Relaxed);
79    }
80
81    #[test]
82    fn record_response_classifies_2xx_as_successful() {
83        let _g = TEST_LOCK.lock().unwrap();
84        reset_counters();
85        record_response(200);
86        record_response(204);
87        record_response(301);
88        let s = snapshot();
89        assert_eq!(s.successful, 3, "200, 204, 301 are all successful");
90        assert_eq!(s.ok, 1, "only one 200");
91    }
92
93    #[test]
94    fn record_response_excludes_4xx_5xx_from_successful() {
95        let _g = TEST_LOCK.lock().unwrap();
96        reset_counters();
97        record_response(404);
98        record_response(429);
99        record_response(500);
100        record_response(503);
101        let s = snapshot();
102        assert_eq!(s.successful, 0);
103        assert_eq!(s.ok, 0);
104    }
105
106    #[test]
107    fn record_accept_increments() {
108        let _g = TEST_LOCK.lock().unwrap();
109        reset_counters();
110        record_accept();
111        record_accept();
112        record_accept();
113        let s = snapshot();
114        assert_eq!(s.accepts, 3);
115    }
116
117    #[test]
118    fn snapshot_returns_current_values() {
119        let _g = TEST_LOCK.lock().unwrap();
120        reset_counters();
121        record_response(200);
122        record_response(200);
123        record_accept();
124        let s = snapshot();
125        assert_eq!(s.successful, 2);
126        assert_eq!(s.ok, 2);
127        assert_eq!(s.accepts, 1);
128    }
129
130    #[test]
131    fn ok_counter_only_for_status_200() {
132        let _g = TEST_LOCK.lock().unwrap();
133        reset_counters();
134        record_response(200);
135        record_response(201);
136        record_response(204);
137        let s = snapshot();
138        assert_eq!(s.ok, 1, "only 200 increments ok counter");
139        assert_eq!(s.successful, 3, "all three are successful");
140    }
141}