nonce_auth/nonce/
cleanup.rs

1use std::future::Future;
2use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use async_trait::async_trait;
6
7/// Strategy for determining when to perform automatic nonce cleanup.
8///
9/// Cleanup strategies are used by `NonceServer` to automatically trigger
10/// expired nonce cleanup based on various criteria such as request count,
11/// elapsed time, or custom logic.
12#[async_trait]
13pub trait CleanupStrategy: Send + Sync {
14    /// Determines whether cleanup should be triggered.
15    ///
16    /// This method is called after each successful nonce verification to check
17    /// if it's time to perform cleanup.
18    async fn should_cleanup(&self) -> bool;
19
20    /// Marks that cleanup has been performed and resets internal state.
21    ///
22    /// This method is called after cleanup has been triggered to reset
23    /// counters, timestamps, or other internal state tracking.
24    async fn mark_as_cleaned(&self);
25}
26
27/// Default hybrid cleanup strategy that triggers cleanup based on both
28/// request count and elapsed time since the last cleanup.
29///
30/// This strategy maintains an internal counter of verification requests
31/// and tracks the time since the last cleanup. Cleanup is triggered when
32/// either threshold is exceeded.
33pub struct HybridCleanupStrategy {
34    count_threshold: u32,
35    time_threshold: Duration,
36    request_count: AtomicU32,
37    last_cleanup_time: AtomicU64,
38}
39
40impl HybridCleanupStrategy {
41    /// Creates a new hybrid cleanup strategy with the specified thresholds.
42    ///
43    /// # Arguments
44    ///
45    /// * `count_threshold` - Number of verification requests before triggering cleanup
46    /// * `time_threshold` - Maximum time duration between cleanups
47    ///
48    /// # Example
49    ///
50    /// ```
51    /// use std::time::Duration;
52    /// use nonce_auth::nonce::cleanup::HybridCleanupStrategy;
53    ///
54    /// // Cleanup every 100 requests or every 5 minutes
55    /// let strategy = HybridCleanupStrategy::new(100, Duration::from_secs(300));
56    /// ```
57    pub fn new(count_threshold: u32, time_threshold: Duration) -> Self {
58        let now = SystemTime::now()
59            .duration_since(UNIX_EPOCH)
60            .unwrap_or_default()
61            .as_secs();
62
63        Self {
64            count_threshold,
65            time_threshold,
66            request_count: AtomicU32::new(0),
67            last_cleanup_time: AtomicU64::new(now),
68        }
69    }
70
71    /// Updates the thresholds for this strategy.
72    ///
73    /// This method allows modifying the cleanup triggers after creation.
74    pub fn set_thresholds(&mut self, count_threshold: u32, time_threshold: Duration) {
75        self.count_threshold = count_threshold;
76        self.time_threshold = time_threshold;
77    }
78}
79
80#[async_trait]
81impl CleanupStrategy for HybridCleanupStrategy {
82    async fn should_cleanup(&self) -> bool {
83        // Increment request count
84        let count = self.request_count.fetch_add(1, Ordering::SeqCst) + 1;
85
86        // Check count threshold
87        if count >= self.count_threshold {
88            return true;
89        }
90
91        // Check time threshold
92        let now = SystemTime::now()
93            .duration_since(UNIX_EPOCH)
94            .unwrap_or_default()
95            .as_secs();
96        let last_cleanup = self.last_cleanup_time.load(Ordering::SeqCst);
97        let elapsed = now.saturating_sub(last_cleanup);
98
99        elapsed >= self.time_threshold.as_secs()
100    }
101
102    async fn mark_as_cleaned(&self) {
103        self.request_count.store(0, Ordering::SeqCst);
104        let now = SystemTime::now()
105            .duration_since(UNIX_EPOCH)
106            .unwrap_or_default()
107            .as_secs();
108        self.last_cleanup_time.store(now, Ordering::SeqCst);
109    }
110}
111
112impl Default for HybridCleanupStrategy {
113    /// Creates a new hybrid cleanup strategy with default thresholds.
114    ///
115    /// Uses a count threshold of 100 requests and a time threshold of 5 minutes.
116    fn default() -> Self {
117        Self::new(100, Duration::from_secs(300))
118    }
119}
120
121/// Wrapper for custom cleanup strategies provided via closures.
122///
123/// This allows users to provide their own cleanup logic as a closure
124/// that returns a Future<Output = bool>.
125pub struct CustomCleanupStrategy<F, Fut>
126where
127    F: Fn() -> Fut + Send + Sync + 'static,
128    Fut: Future<Output = bool> + Send + 'static,
129{
130    strategy_fn: F,
131}
132
133impl<F, Fut> CustomCleanupStrategy<F, Fut>
134where
135    F: Fn() -> Fut + Send + Sync + 'static,
136    Fut: Future<Output = bool> + Send + 'static,
137{
138    /// Creates a new custom cleanup strategy from a closure.
139    ///
140    /// # Arguments
141    ///
142    /// * `strategy_fn` - A closure that returns a Future<Output = bool>
143    ///   indicating whether cleanup should be triggered
144    ///
145    /// # Example
146    ///
147    /// ```
148    /// use nonce_auth::nonce::cleanup::CustomCleanupStrategy;
149    ///
150    /// let strategy = CustomCleanupStrategy::new(|| async {
151    ///     // Custom cleanup logic - e.g., cleanup every other call
152    ///     static mut COUNTER: u32 = 0;
153    ///     unsafe {
154    ///         COUNTER += 1;
155    ///         COUNTER % 2 == 0
156    ///     }
157    /// });
158    /// ```
159    pub fn new(strategy_fn: F) -> Self {
160        Self { strategy_fn }
161    }
162}
163
164#[async_trait]
165impl<F, Fut> CleanupStrategy for CustomCleanupStrategy<F, Fut>
166where
167    F: Fn() -> Fut + Send + Sync + 'static,
168    Fut: Future<Output = bool> + Send + 'static,
169{
170    async fn should_cleanup(&self) -> bool {
171        (self.strategy_fn)().await
172    }
173
174    async fn mark_as_cleaned(&self) {
175        // For custom strategies, state management is left to the closure
176        // so we don't need to do anything here
177    }
178}
179
180/// Type alias for boxed cleanup strategies to reduce verbosity.
181pub type BoxedCleanupStrategy = Box<dyn CleanupStrategy>;
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use std::sync::Arc;
187    use std::sync::atomic::{AtomicBool, AtomicU32};
188    use tokio::time::{Duration as TokioDuration, sleep};
189
190    #[tokio::test]
191    async fn test_hybrid_strategy_count_threshold() {
192        let strategy = HybridCleanupStrategy::new(3, Duration::from_secs(3600)); // Long time threshold
193
194        // First two requests should not trigger cleanup
195        assert!(!strategy.should_cleanup().await);
196        assert!(!strategy.should_cleanup().await);
197
198        // Third request should trigger cleanup
199        assert!(strategy.should_cleanup().await);
200    }
201
202    #[tokio::test]
203    async fn test_hybrid_strategy_time_threshold() {
204        let strategy = HybridCleanupStrategy::new(100, Duration::from_secs(1)); // 1 second threshold
205
206        // First request should not trigger cleanup (count is 1, time is fresh)
207        let result1 = strategy.should_cleanup().await;
208        assert!(!result1, "First request should not trigger cleanup");
209
210        // Wait for time threshold to pass
211        sleep(TokioDuration::from_millis(1100)).await;
212
213        // Next request should trigger cleanup due to time
214        let result2 = strategy.should_cleanup().await;
215        assert!(
216            result2,
217            "Second request after time threshold should trigger cleanup"
218        );
219    }
220
221    #[tokio::test]
222    async fn test_hybrid_strategy_reset_after_cleanup() {
223        let strategy = HybridCleanupStrategy::new(2, Duration::from_secs(3600));
224
225        // Trigger cleanup with count
226        assert!(!strategy.should_cleanup().await);
227        assert!(strategy.should_cleanup().await);
228
229        // Mark as cleaned
230        strategy.mark_as_cleaned().await;
231
232        // Should reset and not trigger immediately
233        assert!(!strategy.should_cleanup().await);
234    }
235
236    #[tokio::test]
237    async fn test_custom_strategy() {
238        let counter = Arc::new(AtomicU32::new(0));
239        let counter_clone = Arc::clone(&counter);
240
241        let strategy = CustomCleanupStrategy::new(move || {
242            let counter = Arc::clone(&counter_clone);
243            async move {
244                let count = counter.fetch_add(1, Ordering::SeqCst) + 1;
245                count % 2 == 0 // Cleanup every second call
246            }
247        });
248
249        // First call should not trigger cleanup
250        assert!(!strategy.should_cleanup().await);
251
252        // Second call should trigger cleanup
253        assert!(strategy.should_cleanup().await);
254
255        // Third call should not trigger cleanup
256        assert!(!strategy.should_cleanup().await);
257    }
258
259    #[tokio::test]
260    async fn test_custom_strategy_mark_as_cleaned_noop() {
261        let flag = Arc::new(AtomicBool::new(false));
262        let flag_clone = Arc::clone(&flag);
263
264        let strategy = CustomCleanupStrategy::new(move || {
265            let flag = Arc::clone(&flag_clone);
266            async move { flag.load(Ordering::SeqCst) }
267        });
268
269        // Should not trigger cleanup initially
270        assert!(!strategy.should_cleanup().await);
271
272        // mark_as_cleaned should be a no-op for custom strategies
273        strategy.mark_as_cleaned().await;
274
275        // Still should not trigger cleanup
276        assert!(!strategy.should_cleanup().await);
277
278        // Set flag externally
279        flag.store(true, Ordering::SeqCst);
280
281        // Now should trigger cleanup
282        assert!(strategy.should_cleanup().await);
283    }
284}