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}