Skip to main content

do_over/
wrap.rs

1//! Policy composition using the Wrap combinator.
2//!
3//! The [`Wrap`] struct allows you to compose multiple policies together,
4//! creating sophisticated resilience strategies.
5//!
6//! # Execution Order
7//!
8//! Execution flows from outer → inner → operation. The outer policy
9//! wraps the inner policy, which wraps your operation.
10//!
11//! # Recommended Policy Ordering
12//!
13//! From outer to inner:
14//! 1. **Bulkhead** - Limit concurrency first
15//! 2. **Circuit Breaker** - Fast-fail if too many errors
16//! 3. **Rate Limiter** - Throttle requests
17//! 4. **Retry** - Handle transient failures
18//! 5. **Timeout** - Bound individual attempts
19//!
20//! # Examples
21//!
22//! ```rust
23//! use do_over::{policy::Policy, wrap::Wrap, retry::RetryPolicy, timeout::TimeoutPolicy, error::DoOverError};
24//! use std::time::Duration;
25//!
26//! # async fn example() {
27//! // Simple composition: retry with timeout
28//! let policy = Wrap::new(
29//!     RetryPolicy::fixed(3, Duration::from_millis(100)),
30//!     TimeoutPolicy::new(Duration::from_secs(5)),
31//! );
32//!
33//! let result: Result<&str, DoOverError<&str>> = policy.execute(|| async {
34//!     Ok("success")
35//! }).await;
36//! # }
37//! ```
38
39use std::future::Future;
40use crate::policy::Policy;
41
42/// Composes two policies together.
43///
44/// The outer policy wraps the inner policy. Execution flows:
45/// `outer → inner → operation`
46///
47/// # Type Parameters
48///
49/// * `O` - The outer policy type
50/// * `I` - The inner policy type
51///
52/// # Examples
53///
54/// Basic composition:
55///
56/// ```rust
57/// use do_over::{wrap::Wrap, retry::RetryPolicy, timeout::TimeoutPolicy};
58/// use std::time::Duration;
59///
60/// let policy = Wrap::new(
61///     RetryPolicy::fixed(3, Duration::from_millis(100)),
62///     TimeoutPolicy::new(Duration::from_secs(5)),
63/// );
64/// ```
65///
66/// Multi-layer composition:
67///
68/// ```rust
69/// use do_over::{wrap::Wrap, bulkhead::Bulkhead, retry::RetryPolicy, timeout::TimeoutPolicy};
70/// use std::time::Duration;
71///
72/// let policy = Wrap::new(
73///     Bulkhead::new(10),
74///     Wrap::new(
75///         RetryPolicy::fixed(3, Duration::from_millis(100)),
76///         TimeoutPolicy::new(Duration::from_secs(5)),
77///     ),
78/// );
79/// ```
80#[derive(Clone)]
81pub struct Wrap<O, I> {
82    /// The outer policy (executed first).
83    pub outer: O,
84    /// The inner policy (executed second, wrapping the operation).
85    pub inner: I,
86}
87
88impl<O, I> Wrap<O, I> {
89    /// Create a new policy composition.
90    ///
91    /// # Arguments
92    ///
93    /// * `outer` - The outer policy (executed first)
94    /// * `inner` - The inner policy (wraps the operation)
95    ///
96    /// # Examples
97    ///
98    /// ```rust
99    /// use do_over::{wrap::Wrap, retry::RetryPolicy, timeout::TimeoutPolicy};
100    /// use std::time::Duration;
101    ///
102    /// let policy = Wrap::new(
103    ///     RetryPolicy::fixed(3, Duration::from_millis(100)),
104    ///     TimeoutPolicy::new(Duration::from_secs(5)),
105    /// );
106    /// ```
107    pub fn new(outer: O, inner: I) -> Self {
108        Self { outer, inner }
109    }
110}
111
112#[async_trait::async_trait]
113impl<O, I, E> Policy<E> for Wrap<O, I>
114where
115    O: Policy<E>,
116    I: Policy<E>,
117    E: Send + Sync,
118{
119    async fn execute<F, Fut, T>(&self, f: F) -> Result<T, E>
120    where
121        F: Fn() -> Fut + Send + Sync,
122        Fut: Future<Output = Result<T, E>> + Send,
123        T: Send,
124    {
125        self.outer.execute(|| self.inner.execute(&f)).await
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::retry::RetryPolicy;
133    use crate::timeout::TimeoutPolicy;
134    use crate::error::DoOverError;
135    use std::time::Duration;
136    use std::sync::atomic::{AtomicUsize, Ordering};
137    use std::sync::Arc;
138
139    #[tokio::test]
140    async fn test_wrap_retry_and_timeout() {
141        let attempts = Arc::new(AtomicUsize::new(0));
142        let attempts_clone = Arc::clone(&attempts);
143
144        let retry = RetryPolicy::fixed(2, Duration::from_millis(10));
145        let timeout = TimeoutPolicy::new(Duration::from_secs(1));
146
147        let wrapped = Wrap::new(retry, timeout);
148
149        let result: Result<String, DoOverError<std::io::Error>> = wrapped
150            .execute(|| {
151                let a = Arc::clone(&attempts_clone);
152                async move {
153                    let count = a.fetch_add(1, Ordering::SeqCst);
154                    if count < 1 {
155                        Err(DoOverError::Inner(std::io::Error::new(
156                            std::io::ErrorKind::Other,
157                            "temporary failure",
158                        )))
159                    } else {
160                        Ok("success".to_string())
161                    }
162                }
163            })
164            .await;
165
166        assert!(result.is_ok());
167        assert_eq!(result.unwrap(), "success");
168        assert_eq!(attempts.load(Ordering::SeqCst), 2);
169    }
170
171    #[tokio::test]
172    async fn test_wrap_composition() {
173        let timeout = TimeoutPolicy::new(Duration::from_millis(100));
174        let retry = RetryPolicy::fixed(1, Duration::from_millis(10));
175
176        let wrapped = Wrap::new(timeout, retry);
177
178        // This should timeout because the inner operation takes too long
179        let result: Result<(), DoOverError<()>> = wrapped
180            .execute(|| async {
181                tokio::time::sleep(Duration::from_millis(200)).await;
182                Ok(())
183            })
184            .await;
185
186        assert!(matches!(result, Err(DoOverError::Timeout)));
187    }
188}