Skip to main content

tracing_throttle/application/
registry.rs

1//! Central registry for tracking event suppression state.
2//!
3//! The registry maintains state for each unique event signature, including
4//! its rate limiting policy and suppression counters.
5
6use crate::application::ports::{Clock, Storage};
7use crate::domain::{policy::Policy, signature::EventSignature, summary::SuppressionCounter};
8
9#[cfg(feature = "human-readable")]
10use crate::domain::metadata::EventMetadata;
11use std::sync::Arc;
12use std::time::Instant;
13
14/// State tracked for each event signature.
15#[derive(Debug, Clone)]
16pub struct EventState {
17    /// Rate limiting policy for this event
18    pub policy: Policy,
19    /// Counter tracking suppressions
20    pub counter: SuppressionCounter,
21    /// Metadata about the event (for human-readable summaries)
22    #[cfg(feature = "human-readable")]
23    pub metadata: Option<EventMetadata>,
24}
25
26impl EventState {
27    /// Create new event state with a policy.
28    pub fn new(policy: Policy, initial_timestamp: Instant) -> Self {
29        Self {
30            policy,
31            counter: SuppressionCounter::new(initial_timestamp),
32            #[cfg(feature = "human-readable")]
33            metadata: None,
34        }
35    }
36
37    /// Create new event state with metadata.
38    #[cfg(feature = "human-readable")]
39    pub fn new_with_metadata(
40        policy: Policy,
41        initial_timestamp: Instant,
42        metadata: EventMetadata,
43    ) -> Self {
44        Self {
45            policy,
46            counter: SuppressionCounter::new(initial_timestamp),
47            metadata: Some(metadata),
48        }
49    }
50
51    /// Update or set the event metadata.
52    ///
53    /// This is called on first occurrence to capture event details.
54    #[cfg(feature = "human-readable")]
55    pub fn set_metadata(&mut self, metadata: EventMetadata) {
56        if self.metadata.is_none() {
57            self.metadata = Some(metadata);
58        }
59    }
60
61    /// Create event state from a snapshot (for deserialization).
62    ///
63    /// This is used by storage backends like Redis to reconstruct state.
64    #[cfg(feature = "redis-storage")]
65    pub fn from_snapshot(
66        policy: Policy,
67        suppressed_count: usize,
68        first_suppressed: Instant,
69        last_suppressed: Instant,
70    ) -> Self {
71        Self::from_snapshot_with_reported(
72            policy,
73            suppressed_count,
74            0,
75            first_suppressed,
76            last_suppressed,
77            first_suppressed,
78        )
79    }
80
81    /// Create event state from a snapshot including reported suppressions.
82    ///
83    /// This is used by storage backends to persist active-emission progress.
84    #[cfg(feature = "redis-storage")]
85    pub fn from_snapshot_with_reported(
86        policy: Policy,
87        suppressed_count: usize,
88        reported_count: usize,
89        first_suppressed: Instant,
90        last_suppressed: Instant,
91        last_reported: Instant,
92    ) -> Self {
93        let first_unreported = if reported_count == 0 {
94            first_suppressed
95        } else {
96            last_reported
97        };
98
99        Self::from_snapshot_with_reported_and_first_unreported(
100            policy,
101            suppressed_count,
102            reported_count,
103            first_suppressed,
104            last_suppressed,
105            last_reported,
106            first_unreported,
107        )
108    }
109
110    /// Create event state from a snapshot including reported and unreported cursors.
111    ///
112    /// This is used by storage backends to persist active-emission progress.
113    #[cfg(feature = "redis-storage")]
114    pub fn from_snapshot_with_reported_and_first_unreported(
115        policy: Policy,
116        suppressed_count: usize,
117        reported_count: usize,
118        first_suppressed: Instant,
119        last_suppressed: Instant,
120        last_reported: Instant,
121        first_unreported: Instant,
122    ) -> Self {
123        Self {
124            policy,
125            counter: SuppressionCounter::from_snapshot_with_reported_and_first_unreported(
126                suppressed_count,
127                reported_count,
128                first_suppressed,
129                last_suppressed,
130                last_reported,
131                first_unreported,
132            ),
133            metadata: None,
134        }
135    }
136}
137
138/// Registry managing all event suppression state.
139///
140/// Uses the Storage port for high-performance concurrent access.
141///
142/// This type is generic over the storage implementation, allowing different
143/// storage backends to be used. In production, use `Arc<ShardedStorage>`.
144#[derive(Clone)]
145pub struct SuppressionRegistry<S>
146where
147    S: Storage<EventSignature, EventState> + Clone,
148{
149    storage: S,
150    clock: Arc<dyn Clock>,
151    default_policy: Policy,
152}
153
154impl<S> SuppressionRegistry<S>
155where
156    S: Storage<EventSignature, EventState> + Clone,
157{
158    /// Create a new registry with storage, clock, and a default policy.
159    ///
160    /// All events will use the default policy unless overridden.
161    pub fn new(storage: S, clock: Arc<dyn Clock>, default_policy: Policy) -> Self {
162        Self {
163            storage,
164            clock,
165            default_policy,
166        }
167    }
168
169    /// Access or create event state for a signature with a callback.
170    ///
171    /// If this is the first time seeing this signature, creates new state
172    /// with the default policy. The callback receives the event state and
173    /// the current timestamp.
174    pub fn with_event_state<F, R>(&self, signature: EventSignature, f: F) -> R
175    where
176        F: FnOnce(&mut EventState, Instant) -> R,
177    {
178        let now = self.clock.now();
179        let default_policy = self.default_policy.clone();
180        self.storage.with_entry_mut(
181            signature,
182            || EventState::new(default_policy, now),
183            |state| f(state, now),
184        )
185    }
186
187    /// Get the default policy.
188    pub fn default_policy(&self) -> &Policy {
189        &self.default_policy
190    }
191
192    /// Create a clone of the default policy.
193    pub fn clone_default_policy(&self) -> Policy {
194        self.default_policy.clone()
195    }
196
197    /// Get the number of tracked signatures.
198    pub fn len(&self) -> usize {
199        self.storage.len()
200    }
201
202    /// Check if the registry is empty.
203    pub fn is_empty(&self) -> bool {
204        self.storage.is_empty()
205    }
206
207    /// Clear all tracked state.
208    pub fn clear(&self) {
209        self.storage.clear();
210    }
211
212    /// Iterate over all event states with a callback.
213    pub fn for_each<F>(&self, f: F)
214    where
215        F: FnMut(&EventSignature, &EventState),
216    {
217        self.storage.for_each(f);
218    }
219
220    /// Remove expired or inactive signatures based on a predicate.
221    pub fn cleanup<F>(&self, f: F)
222    where
223        F: FnMut(&EventSignature, &mut EventState) -> bool,
224    {
225        self.storage.retain(f);
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::domain::policy::Policy;
233    use crate::infrastructure::clock::SystemClock;
234    use crate::infrastructure::storage::ShardedStorage;
235    use std::sync::Arc;
236
237    #[test]
238    fn test_registry_creation() {
239        let storage = Arc::new(ShardedStorage::new());
240        let clock = Arc::new(SystemClock::new());
241        let policy = Policy::count_based(100).unwrap();
242        let registry = SuppressionRegistry::new(storage, clock, policy);
243
244        assert_eq!(registry.len(), 0);
245        assert!(registry.is_empty());
246    }
247
248    #[test]
249    fn test_with_event_state() {
250        let storage = Arc::new(ShardedStorage::new());
251        let clock = Arc::new(SystemClock::new());
252        let policy = Policy::count_based(100).unwrap();
253        let registry = SuppressionRegistry::new(storage, clock, policy);
254        let sig = EventSignature::simple("INFO", "Test message");
255
256        // First access creates state
257        registry.with_event_state(sig, |state, _now| {
258            assert_eq!(state.counter.count(), 0);
259        });
260
261        assert_eq!(registry.len(), 1);
262        assert!(!registry.is_empty());
263
264        // Second access retrieves existing state
265        registry.with_event_state(sig, |state, now| {
266            state.counter.record_suppression(now);
267        });
268
269        assert_eq!(registry.len(), 1);
270    }
271
272    #[test]
273    fn test_clear() {
274        let storage = Arc::new(ShardedStorage::new());
275        let clock = Arc::new(SystemClock::new());
276        let policy = Policy::count_based(100).unwrap();
277        let registry = SuppressionRegistry::new(storage, clock, policy);
278
279        for i in 0..10 {
280            let sig = EventSignature::simple("INFO", &format!("Message {}", i));
281            registry.with_event_state(sig, |_state, _now| {
282                // State is created
283            });
284        }
285
286        assert_eq!(registry.len(), 10);
287
288        registry.clear();
289        assert_eq!(registry.len(), 0);
290        assert!(registry.is_empty());
291    }
292
293    #[test]
294    fn test_concurrent_access() {
295        use std::sync::Arc;
296        use std::thread;
297
298        let storage = Arc::new(ShardedStorage::new());
299        let clock = Arc::new(SystemClock::new());
300        let policy = Policy::count_based(100).unwrap();
301        let registry = Arc::new(SuppressionRegistry::new(storage, clock, policy));
302        let mut handles = vec![];
303
304        for i in 0..10 {
305            let registry_clone = Arc::clone(&registry);
306            let handle = thread::spawn(move || {
307                for j in 0..100 {
308                    let sig = EventSignature::simple("INFO", &format!("Msg_{}_{}", i, j));
309                    registry_clone.with_event_state(sig, |_state, _now| {
310                        // State is created
311                    });
312                }
313            });
314            handles.push(handle);
315        }
316
317        for handle in handles {
318            handle.join().unwrap();
319        }
320
321        assert_eq!(registry.len(), 1000);
322    }
323}