Skip to main content

ralph_workflow/agents/
retry_timer.rs

1//! Retry timer provider for controlling sleep behavior in retry logic.
2//!
3//! This module provides a trait-based abstraction for `std::thread::sleep`
4//! to make retry logic testable. Production code uses real sleep delays,
5//! while tests can use immediate (no-op) sleeps for fast execution.
6
7use std::sync::Arc;
8use std::time::Duration;
9
10/// Provider for sleep operations in retry logic.
11///
12/// This trait allows different sleep implementations:
13/// - Production: Real `std::thread::sleep` with actual delays
14/// - Testing: Immediate (no-op) sleeps for fast test execution
15pub trait RetryTimerProvider: Send + Sync {
16    /// Sleep for the specified duration.
17    fn sleep(&self, duration: Duration);
18}
19
20/// Production retry timer that actually sleeps.
21#[derive(Debug, Clone)]
22struct ProductionRetryTimer;
23
24impl RetryTimerProvider for ProductionRetryTimer {
25    fn sleep(&self, duration: Duration) {
26        std::thread::sleep(duration);
27    }
28}
29
30/// Create a new production retry timer.
31///
32/// This is used in production code where actual sleep delays are needed.
33pub fn production_timer() -> Arc<dyn RetryTimerProvider> {
34    Arc::new(ProductionRetryTimer)
35}
36
37/// Test retry timer that doesn't actually sleep (immediate return).
38///
39/// This is used in tests to avoid long delays while still exercising
40/// the retry logic. The sleep duration is tracked for assertions.
41#[cfg(test)]
42#[derive(Debug, Clone)]
43pub struct TestRetryTimer {
44    /// Optional tracking of sleep durations for test assertions.
45    /// Uses interior mutability to track sleeps through shared references.
46    tracked: Option<Arc<std::sync::atomic::AtomicU64>>,
47}
48
49#[cfg(test)]
50impl Default for TestRetryTimer {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56#[cfg(test)]
57impl TestRetryTimer {
58    /// Create a new test retry timer without tracking.
59    pub fn new() -> Self {
60        Self { tracked: None }
61    }
62
63    /// Create a new test retry timer that tracks total sleep duration in milliseconds.
64    ///
65    /// This is useful for tests that need to verify retry behavior without
66    /// actually waiting. The tracked duration can be retrieved with `total_sleep_ms()`.
67    #[cfg(test)]
68    pub fn with_tracking() -> (Self, Arc<std::sync::atomic::AtomicU64>) {
69        let tracked = Arc::new(std::sync::atomic::AtomicU64::new(0));
70        (
71            Self {
72                tracked: Some(tracked.clone()),
73            },
74            tracked,
75        )
76    }
77
78    /// Get the total sleep duration in milliseconds (if tracking is enabled).
79    #[cfg(test)]
80    pub fn total_sleep_ms(&self) -> Option<u64> {
81        self.tracked
82            .as_ref()
83            .map(|t| t.load(std::sync::atomic::Ordering::Relaxed))
84    }
85}
86
87#[cfg(test)]
88impl RetryTimerProvider for TestRetryTimer {
89    fn sleep(&self, duration: Duration) {
90        if let Some(tracked) = &self.tracked {
91            tracked.fetch_add(
92                duration.as_millis() as u64,
93                std::sync::atomic::Ordering::Relaxed,
94            );
95        }
96        // No actual sleep - return immediately for fast tests
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_production_retry_timer_sleeps() {
106        let timer = production_timer();
107        let start = std::time::Instant::now();
108        timer.sleep(Duration::from_millis(10));
109        let elapsed = start.elapsed();
110        assert!(elapsed >= Duration::from_millis(10));
111    }
112
113    #[test]
114    fn test_test_retry_timer_immediate() {
115        let timer = TestRetryTimer::new();
116        let start = std::time::Instant::now();
117        timer.sleep(Duration::from_secs(10));
118        let elapsed = start.elapsed();
119        assert!(
120            elapsed < Duration::from_millis(100),
121            "Should return immediately"
122        );
123    }
124
125    #[test]
126    fn test_test_retry_timer_tracking() {
127        let (timer, tracked) = TestRetryTimer::with_tracking();
128
129        timer.sleep(Duration::from_millis(100));
130        timer.sleep(Duration::from_millis(200));
131        timer.sleep(Duration::from_millis(300));
132
133        assert_eq!(timer.total_sleep_ms(), Some(600));
134        assert_eq!(tracked.load(std::sync::atomic::Ordering::Relaxed), 600);
135    }
136
137    #[test]
138    fn test_test_retry_timer_no_tracking() {
139        let timer = TestRetryTimer::new();
140        timer.sleep(Duration::from_millis(100));
141        assert_eq!(timer.total_sleep_ms(), None);
142    }
143}