ops_rs/wrappers/
timeout.rs1use crate::prelude::*;
2use 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 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 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 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 fn get_trigger_name(&self) -> &str {
52 self.trigger_name.as_deref().unwrap_or("TimeBoundOp")
53 }
54
55 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 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 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 match tokio::time::timeout(self.timeout_duration, self.wrapped_op.perform(dry, wet)).await {
92 Ok(result) => {
93 let duration = start_time.elapsed();
95 self.log_near_timeout_completion(duration);
96 result
97 }
98 Err(_timeout_elapsed) => {
99 self.log_timeout_warning();
101
102 Err(OpError::Timeout {
104 timeout_ms: self.timeout_duration.as_millis() as u64,
105 })
106 }
107 }
108 }
109
110 fn metadata(&self) -> OpMetadata {
111 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
120pub 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
133pub 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 let timeout_wrapper = TimeBoundWrapper::with_name(op, timeout, trigger_name.clone());
145
146 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 #[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 #[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 #[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 #[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 #[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}