Skip to main content

ops_rs/wrappers/
timeout.rs

1use crate::prelude::*;
2// TimeBoundWrapper implementation with full Java reference functionality
3// Superior implementation using tokio::time::timeout vs Java's unsafe threading
4
5use crate::error::OpError;
6use crate::op::Op;
7use crate::{DryContext, OpMetadata, WetContext};
8use async_trait::async_trait;
9use std::time::{Duration, Instant};
10use tracing::{info, warn};
11
12pub struct TimeBoundWrapper<T> {
13    wrapped_op: Box<dyn Op<T>>,
14    timeout_duration: Duration,
15    trigger_name: Option<String>,
16    warn_on_timeout: bool,
17}
18
19impl<T> TimeBoundWrapper<T> {
20    /// Create new timeout wrapper with duration
21    pub fn new(op: Box<dyn Op<T>>, timeout: Duration) -> Self {
22        Self {
23            wrapped_op: op,
24            timeout_duration: timeout,
25            trigger_name: None,
26            warn_on_timeout: true,
27        }
28    }
29
30    /// Create timeout wrapper with op name for better logging
31    pub fn with_name(op: Box<dyn Op<T>>, timeout: Duration, name: String) -> Self {
32        Self {
33            wrapped_op: op,
34            timeout_duration: timeout,
35            trigger_name: Some(name),
36            warn_on_timeout: true,
37        }
38    }
39
40    /// Create timeout wrapper with configurable timeout warnings
41    pub fn with_warning_control(op: Box<dyn Op<T>>, timeout: Duration, warn: bool) -> Self {
42        Self {
43            wrapped_op: op,
44            timeout_duration: timeout,
45            trigger_name: None,
46            warn_on_timeout: warn,
47        }
48    }
49
50    /// Get op name for logging (fallback to generic name)
51    fn get_trigger_name(&self) -> &str {
52        self.trigger_name.as_deref().unwrap_or("TimeBoundOp")
53    }
54
55    /// Log timeout warning (matches Java TimeBoundOpWrapper behavior)
56    fn log_timeout_warning(&self) {
57        if self.warn_on_timeout {
58            warn!(
59                "Op '{}' was terminated due to timeout after {:?}",
60                self.get_trigger_name(),
61                self.timeout_duration
62            );
63        }
64    }
65
66    /// Log timeout info for successful completion near deadline
67    fn log_near_timeout_completion(&self, duration: Duration) {
68        let timeout_ratio = duration.as_secs_f64() / self.timeout_duration.as_secs_f64();
69        if timeout_ratio > 0.8 {
70            // Completed using more than 80% of timeout
71            info!(
72                "Op '{}' completed in {:.3}s ({}% of {:?} timeout)",
73                self.get_trigger_name(),
74                duration.as_secs_f64(),
75                (timeout_ratio * 100.0) as u32,
76                self.timeout_duration
77            );
78        }
79    }
80}
81
82#[async_trait]
83impl<T> Op<T> for TimeBoundWrapper<T>
84where
85    T: Send + 'static,
86{
87    async fn perform(&self, dry: &mut DryContext, wet: &mut WetContext) -> OpResult<T> {
88        let start_time = Instant::now();
89
90        // Use tokio's timeout mechanism (superior to Java's manual threading)
91        match tokio::time::timeout(self.timeout_duration, self.wrapped_op.perform(dry, wet)).await {
92            Ok(result) => {
93                // Log near-timeout completions for monitoring
94                let duration = start_time.elapsed();
95                self.log_near_timeout_completion(duration);
96                result
97            }
98            Err(_timeout_elapsed) => {
99                // Log timeout warning (matches Java behavior)
100                self.log_timeout_warning();
101
102                // Return structured timeout error with context
103                Err(OpError::Timeout {
104                    timeout_ms: self.timeout_duration.as_millis() as u64,
105                })
106            }
107        }
108    }
109
110    fn metadata(&self) -> OpMetadata {
111        // Pass through metadata from wrapped op with timeout info
112        let mut metadata = self.wrapped_op.metadata();
113        if let Some(ref name) = self.trigger_name {
114            metadata.description = Some(format!("{} (timeout: {:?})", name, self.timeout_duration));
115        }
116        metadata
117    }
118}
119
120/// Create timeout wrapper with automatic op name detection
121/// Uses caller information for better error reporting
122pub fn create_timeout_wrapper_with_caller_name<T>(
123    op: Box<dyn Op<T>>,
124    timeout: Duration,
125) -> TimeBoundWrapper<T>
126where
127    T: Send + 'static,
128{
129    let caller_name = crate::ops::get_caller_trigger_name();
130    TimeBoundWrapper::with_name(op, timeout, caller_name)
131}
132
133/// Create timeout wrapper with both logging and timeout functionality
134/// Combines LoggingWrapper and TimeBoundWrapper (composition pattern)
135pub fn create_logged_timeout_wrapper<T>(
136    op: Box<dyn Op<T>>,
137    timeout: Duration,
138    trigger_name: String,
139) -> crate::wrappers::logging::LoggingWrapper<T>
140where
141    T: Send + 'static,
142{
143    // First wrap with timeout
144    let timeout_wrapper = TimeBoundWrapper::with_name(op, timeout, trigger_name.clone());
145
146    // Then wrap with logging
147    crate::wrappers::logging::LoggingWrapper::new(
148        Box::new(timeout_wrapper),
149        format!("TimeBound[{}]", trigger_name),
150    )
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::op::Op;
157    use std::time::Duration;
158
159    struct SlowOp;
160
161    #[async_trait]
162    impl Op<i32> for SlowOp {
163        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
164            tokio::time::sleep(Duration::from_millis(50)).await;
165            Ok(42)
166        }
167
168        fn metadata(&self) -> OpMetadata {
169            OpMetadata::builder("SlowOp").build()
170        }
171    }
172
173    // TEST0033: Wrap a fast op in TimeBoundWrapper and confirm it completes before the timeout
174    #[tokio::test]
175    async fn test0033_timeout_wrapper_success() {
176        let mut dry = DryContext::new();
177        let mut wet = WetContext::new();
178
179        let op = Box::new(SlowOp);
180
181        let timeout_wrapper = TimeBoundWrapper::new(op, Duration::from_millis(200));
182        let result = timeout_wrapper.perform(&mut dry, &mut wet).await;
183
184        assert!(result.is_ok());
185        assert_eq!(result.unwrap(), 42);
186    }
187
188    struct VerySlowOp;
189
190    #[async_trait]
191    impl Op<i32> for VerySlowOp {
192        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
193            tokio::time::sleep(Duration::from_millis(200)).await;
194            Ok(42)
195        }
196
197        fn metadata(&self) -> OpMetadata {
198            OpMetadata::builder("VerySlowOp").build()
199        }
200    }
201
202    // TEST0034: Wrap a slow op in TimeBoundWrapper with a short timeout and verify a Timeout error is returned
203    #[tokio::test]
204    async fn test0034_timeout_wrapper_timeout() {
205        let mut dry = DryContext::new();
206        let mut wet = WetContext::new();
207
208        let op = Box::new(VerySlowOp);
209
210        let timeout_wrapper = TimeBoundWrapper::new(op, Duration::from_millis(50));
211        let result = timeout_wrapper.perform(&mut dry, &mut wet).await;
212
213        assert!(result.is_err());
214        match result.unwrap_err() {
215            OpError::Timeout { timeout_ms } => {
216                assert_eq!(timeout_ms, 50);
217            }
218            _ => panic!("Expected Timeout error"),
219        }
220    }
221
222    struct StringOp;
223
224    #[async_trait]
225    impl Op<String> for StringOp {
226        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<String> {
227            Ok("success".to_string())
228        }
229
230        fn metadata(&self) -> OpMetadata {
231            OpMetadata::builder("StringOp").build()
232        }
233    }
234
235    // TEST0035: Create a named TimeBoundWrapper and verify the op succeeds and returns the expected value
236    #[tokio::test]
237    async fn test0035_timeout_wrapper_with_name() {
238        let mut dry = DryContext::new();
239        let mut wet = WetContext::new();
240
241        let op = Box::new(StringOp);
242
243        let timeout_wrapper =
244            TimeBoundWrapper::with_name(op, Duration::from_millis(100), "TestOp".to_string());
245        let result = timeout_wrapper.perform(&mut dry, &mut wet).await;
246
247        assert!(result.is_ok());
248        assert_eq!(result.unwrap(), "success");
249    }
250
251    struct IntOp;
252
253    #[async_trait]
254    impl Op<i32> for IntOp {
255        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
256            Ok(100)
257        }
258
259        fn metadata(&self) -> OpMetadata {
260            OpMetadata::builder("IntOp").build()
261        }
262    }
263
264    // TEST0036: Use create_timeout_wrapper_with_caller_name helper and verify the op result is returned
265    #[tokio::test]
266    async fn test0036_caller_name_wrapper() {
267        let mut dry = DryContext::new();
268        let mut wet = WetContext::new();
269
270        let op = Box::new(IntOp);
271
272        let timeout_wrapper =
273            create_timeout_wrapper_with_caller_name(op, Duration::from_millis(100));
274        let result = timeout_wrapper.perform(&mut dry, &mut wet).await;
275
276        assert!(result.is_ok());
277        assert_eq!(result.unwrap(), 100);
278    }
279
280    struct CompositeOp;
281
282    #[async_trait]
283    impl Op<String> for CompositeOp {
284        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<String> {
285            Ok("logged and timed".to_string())
286        }
287
288        fn metadata(&self) -> OpMetadata {
289            OpMetadata::builder("CompositeOp").build()
290        }
291    }
292
293    // TEST0037: Use create_logged_timeout_wrapper to compose logging and timeout wrappers and verify success
294    #[tokio::test]
295    async fn test0037_logged_timeout_wrapper() {
296        tracing_subscriber::fmt::try_init().ok();
297        let mut dry = DryContext::new();
298        let mut wet = WetContext::new();
299
300        let op = Box::new(CompositeOp);
301
302        let wrapped = create_logged_timeout_wrapper(
303            op,
304            Duration::from_millis(100),
305            "CompositeOp".to_string(),
306        );
307        let result = wrapped.perform(&mut dry, &mut wet).await;
308
309        assert!(result.is_ok());
310        assert_eq!(result.unwrap(), "logged and timed");
311    }
312}