Skip to main content

qubit_retry/
retry_executor_builder.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Retry executor builder.
10//!
11//! The builder collects options, a retry decider, and listeners before
12//! producing a validated [`RetryExecutor`].
13
14use std::time::Duration;
15
16use qubit_common::BoxError;
17use qubit_function::{ArcBiFunction, BiFunction, BiPredicate};
18
19use crate::event::RetryListeners;
20use crate::{
21    RetryAbortContext, RetryAbortListener, RetryAttemptContext, RetryAttemptFailure,
22    RetryConfigError, RetryContext, RetryDecision, RetryDelay, RetryFailureContext,
23    RetryFailureListener, RetryJitter, RetryListener, RetryOptions, RetrySuccessContext,
24    RetrySuccessListener,
25};
26
27use crate::error::RetryDecider;
28use crate::retry_executor::RetryExecutor;
29
30/// Builder for [`RetryExecutor`].
31///
32/// The generic parameter `E` is the application error type that the resulting
33/// executor will pass errors to. If no decider is provided, the built executor retries
34/// every application error until limits stop execution.
35pub struct RetryExecutorBuilder<E = BoxError> {
36    /// Retry limits, delays, jitter, and other tunables accumulated by the builder.
37    options: RetryOptions,
38    /// Optional [`RetryDecider`]; when absent, every application error is treated as retryable.
39    retry_decider: Option<RetryDecider<E>>,
40    /// Hooks invoked on success, failure, abort, and each retry attempt.
41    listeners: RetryListeners<E>,
42    /// Set when `max_attempts` was configured as zero; surfaced from [`Self::build`].
43    max_attempts_error: Option<RetryConfigError>,
44}
45
46impl<E> RetryExecutorBuilder<E> {
47    /// Creates a builder with default options and a retry-all decider.
48    ///
49    /// # Parameters
50    /// This function has no parameters.
51    ///
52    /// # Returns
53    /// A builder with [`RetryOptions::default`] and no listeners.
54    #[inline]
55    pub fn new() -> Self {
56        Self {
57            options: RetryOptions::default(),
58            retry_decider: None,
59            listeners: RetryListeners::default(),
60            max_attempts_error: None,
61        }
62    }
63
64    /// Replaces all options with an existing option snapshot.
65    ///
66    /// # Parameters
67    /// - `options`: Retry options to install in the builder.
68    ///
69    /// # Returns
70    /// The updated builder.
71    ///
72    /// # Errors
73    /// This method does not return errors immediately. Validation occurs in
74    /// [`RetryExecutorBuilder::build`].
75    #[inline]
76    pub fn options(mut self, options: RetryOptions) -> Self {
77        self.options = options;
78        self.max_attempts_error = None;
79        self
80    }
81
82    /// Sets the maximum number of attempts.
83    ///
84    /// # Parameters
85    /// - `max_attempts`: Maximum attempts, including the initial attempt.
86    ///
87    /// # Returns
88    /// The updated builder.
89    ///
90    /// # Errors
91    /// This method records a configuration error when `max_attempts` is zero.
92    /// The error is returned later by [`RetryExecutorBuilder::build`].
93    #[inline]
94    pub fn max_attempts(mut self, max_attempts: u32) -> Self {
95        if let Some(max_attempts) = std::num::NonZeroU32::new(max_attempts) {
96            self.options.max_attempts = max_attempts;
97            self.max_attempts_error = None;
98        } else {
99            self.max_attempts_error = Some(RetryConfigError::invalid_value(
100                RetryOptions::KEY_MAX_ATTEMPTS,
101                "max_attempts must be greater than zero",
102            ));
103        }
104        self
105    }
106
107    /// Sets the maximum total elapsed time.
108    ///
109    /// # Parameters
110    /// - `max_elapsed`: Optional total elapsed-time budget for the whole retry
111    ///   execution. `None` means unlimited.
112    ///
113    /// # Returns
114    /// The updated builder.
115    ///
116    /// # Errors
117    /// This method does not return errors.
118    #[inline]
119    pub fn max_elapsed(mut self, max_elapsed: Option<Duration>) -> Self {
120        self.options.max_elapsed = max_elapsed;
121        self
122    }
123
124    /// Sets the base delay strategy.
125    ///
126    /// # Parameters
127    /// - `delay`: Base delay strategy to use between attempts.
128    ///
129    /// # Returns
130    /// The updated builder.
131    ///
132    /// # Errors
133    /// This method does not return errors immediately. RetryDelay validation occurs
134    /// in [`RetryExecutorBuilder::build`].
135    #[inline]
136    pub fn delay(mut self, delay: RetryDelay) -> Self {
137        self.options.delay = delay;
138        self
139    }
140
141    /// Sets the jitter strategy.
142    ///
143    /// # Parameters
144    /// - `jitter`: RetryJitter strategy to apply to each base delay.
145    ///
146    /// # Returns
147    /// The updated builder.
148    ///
149    /// # Errors
150    /// This method does not return errors immediately. RetryJitter validation occurs
151    /// in [`RetryExecutorBuilder::build`].
152    #[inline]
153    pub fn jitter(mut self, jitter: RetryJitter) -> Self {
154        self.options.jitter = jitter;
155        self
156    }
157
158    /// Sets the jitter strategy from a relative factor.
159    ///
160    /// # Parameters
161    /// - `factor`: Relative jitter range. Valid values are finite and within
162    ///   `[0.0, 1.0]`.
163    ///
164    /// # Returns
165    /// The updated builder.
166    ///
167    /// # Errors
168    /// This method does not return errors immediately. Factor validation occurs
169    /// in [`RetryExecutorBuilder::build`].
170    #[inline]
171    pub fn jitter_factor(self, factor: f64) -> Self {
172        self.jitter(RetryJitter::Factor(factor))
173    }
174
175    /// Uses a boolean retry tester where `true` means retry.
176    ///
177    /// # Parameters
178    /// - `retry_tester`: Predicate that receives the application error and
179    ///   attempt context. Returning `true` maps to [`RetryDecision::Retry`];
180    ///   returning `false` maps to [`RetryDecision::Abort`].
181    ///
182    /// # Returns
183    /// The updated builder.
184    ///
185    /// # Errors
186    /// This method does not return errors.
187    ///
188    /// # Panics
189    /// The built executor propagates any panic raised by `retry_tester`.
190    pub fn retry_if<P>(mut self, retry_tester: P) -> Self
191    where
192        P: BiPredicate<E, RetryAttemptContext> + Send + Sync + 'static,
193    {
194        self.retry_decider = Some(ArcBiFunction::new(move |error, context| {
195            if retry_tester.test(error, context) {
196                RetryDecision::Retry
197            } else {
198                RetryDecision::Abort
199            }
200        }));
201        self
202    }
203
204    /// Chooses [`RetryDecision`] for each failed attempt from the error and
205    /// [`RetryAttemptContext`].
206    ///
207    /// # Parameters
208    /// - `decider`: Any [`BiFunction`] over the application error and
209    ///   [`RetryAttemptContext`] (including closures); it is converted with
210    ///   [`BiFunction::into_arc`]. The decider itself must be `Send + Sync +
211    ///   'static`; the application error type `E` is not required to be `'static`.
212    ///   If type inference fails for a closure, annotate parameters (for example
213    ///   `|e: &E, ctx: &RetryAttemptContext|`).
214    ///
215    /// # Returns
216    /// The updated builder.
217    ///
218    /// # Errors
219    /// This method does not return errors.
220    ///
221    /// # Panics
222    /// The built executor propagates any panic raised by `decider`.
223    pub fn retry_decide<B>(mut self, decider: B) -> Self
224    where
225        B: BiFunction<E, RetryAttemptContext, RetryDecision> + Send + Sync + 'static,
226    {
227        self.retry_decider = Some(decider.into_arc());
228        self
229    }
230
231    /// Registers a listener invoked before retry sleep.
232    ///
233    /// # Parameters
234    /// - `listener`: Callback invoked with [`RetryContext`] plus the triggering
235    ///   [`RetryAttemptFailure`] after a failed attempt and before sleeping.
236    ///
237    /// # Returns
238    /// The updated builder.
239    ///
240    /// # Errors
241    /// This method does not return errors.
242    ///
243    /// # Panics
244    /// The built executor propagates any panic raised by `listener`.
245    pub fn on_retry<F>(mut self, listener: F) -> Self
246    where
247        F: Fn(&RetryContext, &RetryAttemptFailure<E>) + Send + Sync + 'static,
248    {
249        self.listeners.retry = Some(RetryListener::new(listener));
250        self
251    }
252
253    /// Registers a listener invoked when the operation succeeds.
254    ///
255    /// # Parameters
256    /// - `listener`: Callback invoked with a [`RetrySuccessContext`] when the
257    ///   operation eventually succeeds.
258    ///
259    /// # Returns
260    /// The updated builder.
261    ///
262    /// # Errors
263    /// This method does not return errors.
264    ///
265    /// # Panics
266    /// The built executor propagates any panic raised by `listener`.
267    pub fn on_success<F>(mut self, listener: F) -> Self
268    where
269        F: Fn(&RetrySuccessContext) + Send + Sync + 'static,
270    {
271        self.listeners.success = Some(RetrySuccessListener::new(listener));
272        self
273    }
274
275    /// Registers a listener invoked when retry limits are exhausted.
276    ///
277    /// # Parameters
278    /// - `listener`: Callback invoked with [`RetryFailureContext`] metadata plus
279    ///   `Option<RetryAttemptFailure<E>>` when retry limits stop execution.
280    ///
281    /// # Returns
282    /// The updated builder.
283    ///
284    /// # Errors
285    /// This method does not return errors.
286    ///
287    /// # Panics
288    /// The built executor propagates any panic raised by `listener`.
289    pub fn on_failure<F>(mut self, listener: F) -> Self
290    where
291        F: Fn(&RetryFailureContext, &Option<RetryAttemptFailure<E>>) + Send + Sync + 'static,
292    {
293        self.listeners.failure = Some(RetryFailureListener::new(listener));
294        self
295    }
296
297    /// Registers a listener invoked when the retry decider aborts retrying.
298    ///
299    /// # Parameters
300    /// - `listener`: Callback invoked with [`RetryAbortContext`] metadata plus the
301    ///   failure when the retry decider aborts retrying.
302    ///
303    /// # Returns
304    /// The updated builder.
305    ///
306    /// # Errors
307    /// This method does not return errors.
308    ///
309    /// # Panics
310    /// The built executor propagates any panic raised by `listener`.
311    pub fn on_abort<F>(mut self, listener: F) -> Self
312    where
313        F: Fn(&RetryAbortContext, &RetryAttemptFailure<E>) + Send + Sync + 'static,
314    {
315        self.listeners.abort = Some(RetryAbortListener::new(listener));
316        self
317    }
318
319    /// Builds and validates the executor.
320    ///
321    /// # Parameters
322    /// This method has no parameters.
323    ///
324    /// # Returns
325    /// A validated [`RetryExecutor`].
326    ///
327    /// # Errors
328    /// Returns [`RetryConfigError`] when `max_attempts` was set to zero or when
329    /// delay or jitter validation fails.
330    pub fn build(self) -> Result<RetryExecutor<E>, RetryConfigError> {
331        if let Some(error) = self.max_attempts_error {
332            return Err(error);
333        }
334        self.options.validate()?;
335        // If no decider is provided, treat all errors as retryable.
336        let retry_decider = self
337            .retry_decider
338            .unwrap_or_else(|| ArcBiFunction::constant(RetryDecision::Retry));
339        Ok(RetryExecutor::new(
340            self.options,
341            retry_decider,
342            self.listeners,
343        ))
344    }
345}
346
347impl<E> Default for RetryExecutorBuilder<E> {
348    /// Creates a default retry executor builder.
349    ///
350    /// # Parameters
351    /// This function has no parameters.
352    ///
353    /// # Returns
354    /// A builder equivalent to [`RetryExecutorBuilder::new`].
355    ///
356    /// # Errors
357    /// This function does not return errors.
358    #[inline]
359    fn default() -> Self {
360        Self::new()
361    }
362}