mockforge_foundation/
rate_counters.rs1use std::sync::atomic::{AtomicU64, Ordering};
32
33pub static SUCCESSFUL_RESPONSES_TOTAL: AtomicU64 = AtomicU64::new(0);
35
36pub static OK_RESPONSES_TOTAL: AtomicU64 = AtomicU64::new(0);
38
39pub static HTTP_ACCEPTS_TOTAL: AtomicU64 = AtomicU64::new(0);
41
42pub static HTTP_CONNECTIONS_OPEN: AtomicU64 = AtomicU64::new(0);
45
46pub static HTTP_CONNECTIONS_CLOSED_TOTAL: AtomicU64 = AtomicU64::new(0);
48
49#[inline]
51pub fn record_response(status_code: u16) {
52 if (200..=399).contains(&status_code) {
53 SUCCESSFUL_RESPONSES_TOTAL.fetch_add(1, Ordering::Relaxed);
54 }
55 if status_code == 200 {
56 OK_RESPONSES_TOTAL.fetch_add(1, Ordering::Relaxed);
57 }
58}
59
60#[inline]
64pub fn record_accept() {
65 HTTP_ACCEPTS_TOTAL.fetch_add(1, Ordering::Relaxed);
66 HTTP_CONNECTIONS_OPEN.fetch_add(1, Ordering::Relaxed);
67}
68
69#[inline]
72pub fn record_close() {
73 let prev = HTTP_CONNECTIONS_OPEN.load(Ordering::Relaxed);
76 if prev > 0 {
77 HTTP_CONNECTIONS_OPEN.fetch_sub(1, Ordering::Relaxed);
78 }
79 HTTP_CONNECTIONS_CLOSED_TOTAL.fetch_add(1, Ordering::Relaxed);
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub struct CounterSnapshot {
85 pub successful: u64,
86 pub ok: u64,
87 pub accepts: u64,
88 pub connections_open: u64,
90 pub connections_closed: u64,
92}
93
94pub fn snapshot() -> CounterSnapshot {
97 CounterSnapshot {
98 successful: SUCCESSFUL_RESPONSES_TOTAL.load(Ordering::Relaxed),
99 ok: OK_RESPONSES_TOTAL.load(Ordering::Relaxed),
100 accepts: HTTP_ACCEPTS_TOTAL.load(Ordering::Relaxed),
101 connections_open: HTTP_CONNECTIONS_OPEN.load(Ordering::Relaxed),
102 connections_closed: HTTP_CONNECTIONS_CLOSED_TOTAL.load(Ordering::Relaxed),
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use std::sync::Mutex;
110
111 static TEST_LOCK: Mutex<()> = Mutex::new(());
113
114 fn reset_counters() {
115 SUCCESSFUL_RESPONSES_TOTAL.store(0, Ordering::Relaxed);
116 OK_RESPONSES_TOTAL.store(0, Ordering::Relaxed);
117 HTTP_ACCEPTS_TOTAL.store(0, Ordering::Relaxed);
118 HTTP_CONNECTIONS_OPEN.store(0, Ordering::Relaxed);
119 HTTP_CONNECTIONS_CLOSED_TOTAL.store(0, Ordering::Relaxed);
120 }
121
122 #[test]
123 fn record_response_classifies_2xx_as_successful() {
124 let _g = TEST_LOCK.lock().unwrap();
125 reset_counters();
126 record_response(200);
127 record_response(204);
128 record_response(301);
129 let s = snapshot();
130 assert_eq!(s.successful, 3, "200, 204, 301 are all successful");
131 assert_eq!(s.ok, 1, "only one 200");
132 }
133
134 #[test]
135 fn record_response_excludes_4xx_5xx_from_successful() {
136 let _g = TEST_LOCK.lock().unwrap();
137 reset_counters();
138 record_response(404);
139 record_response(429);
140 record_response(500);
141 record_response(503);
142 let s = snapshot();
143 assert_eq!(s.successful, 0);
144 assert_eq!(s.ok, 0);
145 }
146
147 #[test]
148 fn record_accept_increments() {
149 let _g = TEST_LOCK.lock().unwrap();
150 reset_counters();
151 record_accept();
152 record_accept();
153 record_accept();
154 let s = snapshot();
155 assert_eq!(s.accepts, 3);
156 }
157
158 #[test]
159 fn snapshot_returns_current_values() {
160 let _g = TEST_LOCK.lock().unwrap();
161 reset_counters();
162 record_response(200);
163 record_response(200);
164 record_accept();
165 let s = snapshot();
166 assert_eq!(s.successful, 2);
167 assert_eq!(s.ok, 2);
168 assert_eq!(s.accepts, 1);
169 }
170
171 #[test]
172 fn record_accept_bumps_open_gauge() {
173 let _g = TEST_LOCK.lock().unwrap();
174 reset_counters();
175 record_accept();
176 record_accept();
177 let s = snapshot();
178 assert_eq!(s.accepts, 2, "lifetime accepts");
179 assert_eq!(s.connections_open, 2, "currently open");
180 assert_eq!(s.connections_closed, 0);
181 }
182
183 #[test]
184 fn record_close_decrements_open_and_bumps_closed() {
185 let _g = TEST_LOCK.lock().unwrap();
186 reset_counters();
187 record_accept();
188 record_accept();
189 record_close();
190 let s = snapshot();
191 assert_eq!(s.accepts, 2);
192 assert_eq!(s.connections_open, 1, "one still open");
193 assert_eq!(s.connections_closed, 1);
194 }
195
196 #[test]
197 fn record_close_without_matching_accept_does_not_underflow() {
198 let _g = TEST_LOCK.lock().unwrap();
199 reset_counters();
200 record_close();
203 record_close();
204 let s = snapshot();
205 assert_eq!(s.connections_open, 0, "saturating decrement");
206 assert_eq!(s.connections_closed, 2, "closed still tallied");
207 }
208
209 #[test]
210 fn ok_counter_only_for_status_200() {
211 let _g = TEST_LOCK.lock().unwrap();
212 reset_counters();
213 record_response(200);
214 record_response(201);
215 record_response(204);
216 let s = snapshot();
217 assert_eq!(s.ok, 1, "only 200 increments ok counter");
218 assert_eq!(s.successful, 3, "all three are successful");
219 }
220}