Skip to main content

tower_retry_plus/
config.rs

1use crate::backoff::{ExponentialBackoff, FixedInterval, IntervalFunction};
2use crate::events::RetryEvent;
3use crate::policy::{RetryPolicy, RetryPredicate};
4use std::sync::Arc;
5use std::time::Duration;
6use tower_resilience_core::events::{EventListeners, FnListener};
7
8/// Configuration for the retry middleware.
9pub struct RetryConfig<E> {
10    pub(crate) policy: RetryPolicy<E>,
11    pub(crate) event_listeners: EventListeners<RetryEvent>,
12    pub(crate) name: String,
13}
14
15impl<E> RetryConfig<E> {
16    /// Creates a new builder for retry configuration.
17    pub fn builder() -> RetryConfigBuilder<E> {
18        RetryConfigBuilder::new()
19    }
20
21    /// Returns a layer that can be applied to a service.
22    pub fn layer(self) -> crate::RetryLayer<E> {
23        crate::RetryLayer::new(self)
24    }
25}
26
27/// Builder for [`RetryConfig`].
28pub struct RetryConfigBuilder<E> {
29    max_attempts: usize,
30    interval_fn: Option<Arc<dyn IntervalFunction>>,
31    retry_predicate: Option<RetryPredicate<E>>,
32    event_listeners: EventListeners<RetryEvent>,
33    name: String,
34}
35
36impl<E> Default for RetryConfigBuilder<E> {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl<E> RetryConfigBuilder<E> {
43    /// Creates a new builder with defaults.
44    ///
45    /// Defaults:
46    /// - max_attempts: 3
47    /// - backoff: Exponential with 100ms initial interval
48    /// - name: `"<unnamed>"`
49    pub fn new() -> Self {
50        Self {
51            max_attempts: 3,
52            interval_fn: None,
53            retry_predicate: None,
54            event_listeners: EventListeners::new(),
55            name: "<unnamed>".to_string(),
56        }
57    }
58
59    /// Sets the maximum number of retry attempts.
60    ///
61    /// This includes the initial attempt, so max_attempts=3 means
62    /// 1 initial attempt + 2 retries.
63    pub fn max_attempts(mut self, max_attempts: usize) -> Self {
64        self.max_attempts = max_attempts;
65        self
66    }
67
68    /// Sets a fixed backoff interval.
69    pub fn fixed_backoff(mut self, duration: Duration) -> Self {
70        self.interval_fn = Some(Arc::new(FixedInterval::new(duration)));
71        self
72    }
73
74    /// Sets exponential backoff with default settings.
75    pub fn exponential_backoff(mut self, initial_interval: Duration) -> Self {
76        self.interval_fn = Some(Arc::new(ExponentialBackoff::new(initial_interval)));
77        self
78    }
79
80    /// Sets a custom interval function for backoff.
81    pub fn backoff<I>(mut self, interval_fn: I) -> Self
82    where
83        I: IntervalFunction + 'static,
84    {
85        self.interval_fn = Some(Arc::new(interval_fn));
86        self
87    }
88
89    /// Sets a predicate to determine which errors should be retried.
90    pub fn retry_on<F>(mut self, predicate: F) -> Self
91    where
92        F: Fn(&E) -> bool + Send + Sync + 'static,
93    {
94        self.retry_predicate = Some(Arc::new(predicate));
95        self
96    }
97
98    /// Sets the name for this retry instance (used in events).
99    pub fn name<S: Into<String>>(mut self, name: S) -> Self {
100        self.name = name.into();
101        self
102    }
103
104    /// Registers a callback for retry events.
105    pub fn on_retry<F>(mut self, f: F) -> Self
106    where
107        F: Fn(usize, Duration) + Send + Sync + 'static,
108    {
109        self.event_listeners.add(FnListener::new(move |event| {
110            if let RetryEvent::Retry { attempt, delay, .. } = event {
111                f(*attempt, *delay);
112            }
113        }));
114        self
115    }
116
117    /// Registers a callback for success events.
118    pub fn on_success<F>(mut self, f: F) -> Self
119    where
120        F: Fn(usize) + Send + Sync + 'static,
121    {
122        self.event_listeners.add(FnListener::new(move |event| {
123            if let RetryEvent::Success { attempts, .. } = event {
124                f(*attempts);
125            }
126        }));
127        self
128    }
129
130    /// Registers a callback for error events (exhausted retries).
131    pub fn on_error<F>(mut self, f: F) -> Self
132    where
133        F: Fn(usize) + Send + Sync + 'static,
134    {
135        self.event_listeners.add(FnListener::new(move |event| {
136            if let RetryEvent::Error { attempts, .. } = event {
137                f(*attempts);
138            }
139        }));
140        self
141    }
142
143    /// Registers a callback for ignored error events.
144    pub fn on_ignored_error<F>(mut self, f: F) -> Self
145    where
146        F: Fn() + Send + Sync + 'static,
147    {
148        self.event_listeners.add(FnListener::new(move |event| {
149            if matches!(event, RetryEvent::IgnoredError { .. }) {
150                f();
151            }
152        }));
153        self
154    }
155
156    /// Builds the retry configuration.
157    pub fn build(self) -> RetryConfig<E> {
158        let interval_fn = self
159            .interval_fn
160            .unwrap_or_else(|| Arc::new(ExponentialBackoff::new(Duration::from_millis(100))));
161
162        let mut policy = RetryPolicy::new(self.max_attempts, interval_fn);
163        if let Some(predicate) = self.retry_predicate {
164            policy.retry_predicate = Some(predicate);
165        }
166
167        RetryConfig {
168            policy,
169            event_listeners: self.event_listeners,
170            name: self.name,
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use std::sync::atomic::{AtomicUsize, Ordering};
179
180    #[test]
181    fn test_builder_defaults() {
182        let config: RetryConfig<std::io::Error> = RetryConfig::builder().build();
183        assert_eq!(config.policy.max_attempts, 3);
184        assert_eq!(config.name, "<unnamed>");
185    }
186
187    #[test]
188    fn test_builder_custom_values() {
189        let config: RetryConfig<std::io::Error> = RetryConfig::builder()
190            .max_attempts(5)
191            .fixed_backoff(Duration::from_secs(2))
192            .name("test-retry")
193            .build();
194
195        assert_eq!(config.policy.max_attempts, 5);
196        assert_eq!(config.policy.next_backoff(0), Duration::from_secs(2));
197        assert_eq!(config.name, "test-retry");
198    }
199
200    #[test]
201    fn test_event_listeners() {
202        let retry_count = Arc::new(AtomicUsize::new(0));
203        let success_count = Arc::new(AtomicUsize::new(0));
204
205        let rc = Arc::clone(&retry_count);
206        let sc = Arc::clone(&success_count);
207
208        let config: RetryConfig<std::io::Error> = RetryConfig::builder()
209            .on_retry(move |_, _| {
210                rc.fetch_add(1, Ordering::SeqCst);
211            })
212            .on_success(move |_| {
213                sc.fetch_add(1, Ordering::SeqCst);
214            })
215            .build();
216
217        // Emit events
218        let event = RetryEvent::Retry {
219            pattern_name: "test".to_string(),
220            timestamp: std::time::Instant::now(),
221            attempt: 1,
222            delay: Duration::from_secs(1),
223        };
224        config.event_listeners.emit(&event);
225
226        let event = RetryEvent::Success {
227            pattern_name: "test".to_string(),
228            timestamp: std::time::Instant::now(),
229            attempts: 2,
230        };
231        config.event_listeners.emit(&event);
232
233        assert_eq!(retry_count.load(Ordering::SeqCst), 1);
234        assert_eq!(success_count.load(Ordering::SeqCst), 1);
235    }
236}