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}