Skip to main content

rustack_ses_core/
statistics.rs

1//! Send statistics and quota tracking.
2//!
3//! Tracks send counts, bounces, complaints, and delivery attempts using
4//! atomic counters for lock-free concurrent access.
5
6use std::sync::atomic::{AtomicU64, Ordering};
7
8/// Tracks send statistics for `GetSendStatistics` and `GetSendQuota`.
9#[derive(Debug)]
10pub struct SendStatistics {
11    /// Number of successful sends.
12    send_count: AtomicU64,
13    /// Number of bounces (always 0 in local dev).
14    bounce_count: AtomicU64,
15    /// Number of complaints (always 0 in local dev).
16    complaint_count: AtomicU64,
17    /// Number of delivery attempts.
18    delivery_attempts: AtomicU64,
19    /// Number of rejects (always 0 in local dev).
20    reject_count: AtomicU64,
21}
22
23impl Default for SendStatistics {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl SendStatistics {
30    /// Create a new `SendStatistics` with all counters at zero.
31    #[must_use]
32    pub fn new() -> Self {
33        Self {
34            send_count: AtomicU64::new(0),
35            bounce_count: AtomicU64::new(0),
36            complaint_count: AtomicU64::new(0),
37            delivery_attempts: AtomicU64::new(0),
38            reject_count: AtomicU64::new(0),
39        }
40    }
41
42    /// Record a successful send.
43    pub fn record_send(&self) {
44        self.send_count.fetch_add(1, Ordering::Relaxed);
45        self.delivery_attempts.fetch_add(1, Ordering::Relaxed);
46    }
47
48    /// Get a snapshot of current statistics.
49    #[must_use]
50    pub fn get_stats(&self) -> SendStats {
51        SendStats {
52            send_count: self.send_count.load(Ordering::Relaxed),
53            bounce_count: self.bounce_count.load(Ordering::Relaxed),
54            complaint_count: self.complaint_count.load(Ordering::Relaxed),
55            delivery_attempts: self.delivery_attempts.load(Ordering::Relaxed),
56            reject_count: self.reject_count.load(Ordering::Relaxed),
57        }
58    }
59}
60
61/// Snapshot of send statistics.
62#[derive(Debug, Clone)]
63pub struct SendStats {
64    /// Number of successful sends.
65    pub send_count: u64,
66    /// Number of bounces.
67    pub bounce_count: u64,
68    /// Number of complaints.
69    pub complaint_count: u64,
70    /// Number of delivery attempts.
71    pub delivery_attempts: u64,
72    /// Number of rejects.
73    pub reject_count: u64,
74}
75
76/// Send quota configuration.
77#[derive(Debug, Clone)]
78pub struct SendQuotaConfig {
79    /// Max sends per 24 hours.
80    pub max_24_hour_send: f64,
81    /// Max sends per second.
82    pub max_send_rate: f64,
83}
84
85impl Default for SendQuotaConfig {
86    fn default() -> Self {
87        Self {
88            max_24_hour_send: 200.0,
89            max_send_rate: 1.0,
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_should_start_with_zero_counts() {
100        let stats = SendStatistics::new();
101        let snapshot = stats.get_stats();
102        assert_eq!(snapshot.send_count, 0);
103        assert_eq!(snapshot.bounce_count, 0);
104        assert_eq!(snapshot.complaint_count, 0);
105        assert_eq!(snapshot.delivery_attempts, 0);
106        assert_eq!(snapshot.reject_count, 0);
107    }
108
109    #[test]
110    fn test_should_increment_on_send() {
111        let stats = SendStatistics::new();
112        stats.record_send();
113        stats.record_send();
114        let snapshot = stats.get_stats();
115        assert_eq!(snapshot.send_count, 2);
116        assert_eq!(snapshot.delivery_attempts, 2);
117        assert_eq!(snapshot.bounce_count, 0);
118    }
119
120    #[test]
121    fn test_should_create_default_quota() {
122        let quota = SendQuotaConfig::default();
123        assert!((quota.max_24_hour_send - 200.0).abs() < f64::EPSILON);
124        assert!((quota.max_send_rate - 1.0).abs() < f64::EPSILON);
125    }
126}