use chrono::{DateTime, Utc};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Default)]
pub struct StatsSnapshot {
pub total_requests: u64,
pub allowed_requests: u64,
pub rejected_requests: u64,
pub banned_requests: u64,
pub error_count: u64,
pub last_updated: Option<DateTime<Utc>>,
}
pub struct StatsManager {
total_requests: AtomicU64,
allowed_requests: AtomicU64,
rejected_requests: AtomicU64,
banned_requests: AtomicU64,
error_count: AtomicU64,
}
impl StatsManager {
pub fn new() -> Self {
Self {
total_requests: AtomicU64::new(0),
allowed_requests: AtomicU64::new(0),
rejected_requests: AtomicU64::new(0),
banned_requests: AtomicU64::new(0),
error_count: AtomicU64::new(0),
}
}
pub fn with_values(total: u64, allowed: u64, rejected: u64, banned: u64, error: u64) -> Self {
Self {
total_requests: AtomicU64::new(total),
allowed_requests: AtomicU64::new(allowed),
rejected_requests: AtomicU64::new(rejected),
banned_requests: AtomicU64::new(banned),
error_count: AtomicU64::new(error),
}
}
#[inline]
pub fn increment_total(&self) {
self.total_requests.fetch_add(1, Ordering::SeqCst);
}
#[inline]
pub fn increment_allowed(&self) {
self.allowed_requests.fetch_add(1, Ordering::SeqCst);
}
#[inline]
pub fn increment_rejected(&self) {
self.rejected_requests.fetch_add(1, Ordering::SeqCst);
}
#[inline]
pub fn increment_banned(&self) {
self.banned_requests.fetch_add(1, Ordering::SeqCst);
}
#[inline]
pub fn increment_error(&self) {
self.error_count.fetch_add(1, Ordering::SeqCst);
}
pub fn snapshot(&self) -> StatsSnapshot {
StatsSnapshot {
total_requests: self.total_requests.load(Ordering::SeqCst),
allowed_requests: self.allowed_requests.load(Ordering::SeqCst),
rejected_requests: self.rejected_requests.load(Ordering::SeqCst),
banned_requests: self.banned_requests.load(Ordering::SeqCst),
error_count: self.error_count.load(Ordering::SeqCst),
last_updated: Some(Utc::now()),
}
}
pub fn reset(&self) {
self.total_requests.store(0, Ordering::SeqCst);
self.allowed_requests.store(0, Ordering::SeqCst);
self.rejected_requests.store(0, Ordering::SeqCst);
self.banned_requests.store(0, Ordering::SeqCst);
self.error_count.store(0, Ordering::SeqCst);
}
#[inline]
pub fn total(&self) -> u64 {
self.total_requests.load(Ordering::SeqCst)
}
#[inline]
pub fn allowed(&self) -> u64 {
self.allowed_requests.load(Ordering::SeqCst)
}
#[inline]
pub fn rejected(&self) -> u64 {
self.rejected_requests.load(Ordering::SeqCst)
}
#[inline]
pub fn banned(&self) -> u64 {
self.banned_requests.load(Ordering::SeqCst)
}
#[inline]
pub fn errors(&self) -> u64 {
self.error_count.load(Ordering::SeqCst)
}
pub fn allow_rate(&self) -> f64 {
let total = self.total();
if total == 0 {
return 0.0;
}
self.allowed() as f64 / total as f64
}
pub fn reject_rate(&self) -> f64 {
let total = self.total();
if total == 0 {
return 0.0;
}
self.rejected() as f64 / total as f64
}
pub fn ban_rate(&self) -> f64 {
let total = self.total();
if total == 0 {
return 0.0;
}
self.banned() as f64 / total as f64
}
pub fn error_rate(&self) -> f64 {
let total = self.total();
if total == 0 {
return 0.0;
}
self.errors() as f64 / total as f64
}
}
impl Default for StatsManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stats_manager_new() {
let stats = StatsManager::new();
let snapshot = stats.snapshot();
assert_eq!(snapshot.total_requests, 0);
assert_eq!(snapshot.allowed_requests, 0);
assert_eq!(snapshot.rejected_requests, 0);
assert_eq!(snapshot.banned_requests, 0);
assert_eq!(snapshot.error_count, 0);
}
#[test]
fn test_stats_manager_with_values() {
let stats = StatsManager::with_values(100, 80, 10, 5, 5);
let snapshot = stats.snapshot();
assert_eq!(snapshot.total_requests, 100);
assert_eq!(snapshot.allowed_requests, 80);
assert_eq!(snapshot.rejected_requests, 10);
assert_eq!(snapshot.banned_requests, 5);
assert_eq!(snapshot.error_count, 5);
}
#[test]
fn test_stats_manager_increment() {
let stats = StatsManager::new();
stats.increment_total();
stats.increment_allowed();
stats.increment_rejected();
stats.increment_banned();
stats.increment_error();
let snapshot = stats.snapshot();
assert_eq!(snapshot.total_requests, 1);
assert_eq!(snapshot.allowed_requests, 1);
assert_eq!(snapshot.rejected_requests, 1);
assert_eq!(snapshot.banned_requests, 1);
assert_eq!(snapshot.error_count, 1);
}
#[test]
fn test_stats_manager_reset() {
let stats = StatsManager::new();
stats.increment_total();
stats.increment_allowed();
stats.increment_rejected();
stats.increment_banned();
stats.increment_error();
stats.reset();
let snapshot = stats.snapshot();
assert_eq!(snapshot.total_requests, 0);
assert_eq!(snapshot.allowed_requests, 0);
assert_eq!(snapshot.rejected_requests, 0);
assert_eq!(snapshot.banned_requests, 0);
assert_eq!(snapshot.error_count, 0);
}
#[test]
fn test_stats_manager_individual_getters() {
let stats = StatsManager::new();
stats.increment_total();
stats.increment_total();
stats.increment_allowed();
stats.increment_rejected();
stats.increment_banned();
stats.increment_error();
assert_eq!(stats.total(), 2);
assert_eq!(stats.allowed(), 1);
assert_eq!(stats.rejected(), 1);
assert_eq!(stats.banned(), 1);
assert_eq!(stats.errors(), 1);
}
#[test]
fn test_stats_manager_rates() {
let stats = StatsManager::new();
for _ in 0..10 {
stats.increment_total();
}
for _ in 0..6 {
stats.increment_allowed();
}
for _ in 0..2 {
stats.increment_rejected();
}
stats.increment_banned();
stats.increment_error();
assert!((stats.allow_rate() - 0.6).abs() < 0.001);
assert!((stats.reject_rate() - 0.2).abs() < 0.001);
assert!((stats.ban_rate() - 0.1).abs() < 0.001);
assert!((stats.error_rate() - 0.1).abs() < 0.001);
}
#[test]
fn test_stats_manager_rates_zero_total() {
let stats = StatsManager::new();
assert_eq!(stats.allow_rate(), 0.0);
assert_eq!(stats.reject_rate(), 0.0);
assert_eq!(stats.ban_rate(), 0.0);
assert_eq!(stats.error_rate(), 0.0);
}
#[test]
fn test_stats_manager_concurrent_increment() {
use std::sync::Arc;
use std::thread;
let stats = Arc::new(StatsManager::new());
let mut handles = vec![];
for _ in 0..10 {
let stats_clone = Arc::clone(&stats);
handles.push(thread::spawn(move || {
for _ in 0..100 {
stats_clone.increment_total();
stats_clone.increment_allowed();
}
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(stats.total(), 1000);
assert_eq!(stats.allowed(), 1000);
}
#[test]
fn test_stats_snapshot_last_updated() {
let stats = StatsManager::new();
let snapshot = stats.snapshot();
assert!(snapshot.last_updated.is_some());
}
#[test]
fn test_stats_manager_default() {
let stats = StatsManager::default();
let snapshot = stats.snapshot();
assert_eq!(snapshot.total_requests, 0);
assert_eq!(snapshot.allowed_requests, 0);
assert_eq!(snapshot.rejected_requests, 0);
assert_eq!(snapshot.banned_requests, 0);
assert_eq!(snapshot.error_count, 0);
}
#[test]
fn test_stats_manager_concurrent_snapshot_monotonic() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering as StdOrdering};
use std::thread;
use std::time::Duration;
let stats = Arc::new(StatsManager::new());
let stop = Arc::new(AtomicBool::new(false));
let violations = Arc::new(std::sync::atomic::AtomicU64::new(0));
let stats_worker = Arc::clone(&stats);
let stop_worker = Arc::clone(&stop);
let worker = thread::spawn(move || {
while !stop_worker.load(StdOrdering::SeqCst) {
stats_worker.increment_total();
stats_worker.increment_allowed();
stats_worker.increment_rejected();
stats_worker.increment_banned();
stats_worker.increment_error();
}
});
let stats_snap = Arc::clone(&stats);
let stop_snap = Arc::clone(&stop);
let violations_snap = Arc::clone(&violations);
let snapshotter = thread::spawn(move || {
let mut prev = stats_snap.snapshot();
for _ in 0..1000 {
let curr = stats_snap.snapshot();
if curr.total_requests < prev.total_requests
|| curr.allowed_requests < prev.allowed_requests
|| curr.rejected_requests < prev.rejected_requests
|| curr.banned_requests < prev.banned_requests
|| curr.error_count < prev.error_count
{
violations_snap.fetch_add(1, StdOrdering::SeqCst);
}
prev = curr;
}
let _ = stop_snap;
});
thread::sleep(Duration::from_millis(200));
stop.store(true, StdOrdering::SeqCst);
worker.join().unwrap();
snapshotter.join().unwrap();
assert_eq!(
violations.load(StdOrdering::SeqCst),
0,
"snapshot 观测到计数器倒退:违反单调性(increment 期间不应有 reset)"
);
}
#[test]
fn test_stats_manager_concurrent_check_pattern_eventual_consistency() {
use std::sync::Arc;
use std::thread;
let stats = Arc::new(StatsManager::new());
const THREADS: usize = 8;
const ITERS: u64 = 1000;
let mut handles = vec![];
for _ in 0..THREADS {
let stats_clone = Arc::clone(&stats);
handles.push(thread::spawn(move || {
for i in 0..ITERS {
stats_clone.increment_total();
match i % 3 {
0 => stats_clone.increment_allowed(),
1 => stats_clone.increment_rejected(),
_ => stats_clone.increment_error(),
}
}
}));
}
for h in handles {
h.join().unwrap();
}
let snap = stats.snapshot();
let expected_total = (THREADS as u64) * ITERS;
let classified = snap.allowed_requests + snap.rejected_requests + snap.error_count;
assert_eq!(snap.total_requests, expected_total, "total 计数丢失更新");
assert_eq!(classified, expected_total, "分类计数丢失更新");
assert_eq!(
classified, snap.total_requests,
"SeqCst 应保证 fetch_add 无丢失更新:classified 应等于 total"
);
}
}