qubit_cas/executor/cas_builder.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Builder for [`crate::CasExecutor`].
11
12use std::marker::PhantomData;
13use std::time::Duration;
14
15use qubit_error::BoxError;
16use qubit_retry::{
17 AttemptTimeoutOption,
18 RetryBuilder,
19 RetryConfigError,
20 RetryDelay,
21 RetryJitter,
22 RetryOptions,
23};
24
25use crate::observability::{
26 CasObservabilityConfig,
27 ContentionThresholds,
28 ListenerPanicPolicy,
29};
30use crate::strategy::CasStrategy;
31
32use super::cas_executor::CasExecutor;
33
34/// Builder for [`CasExecutor`](crate::CasExecutor).
35pub struct CasBuilder<T, E = BoxError> {
36 /// Retry-layer builder that owns all retry-related settings.
37 retry: RetryBuilder<BoxError>,
38 /// Observability settings.
39 observability: CasObservabilityConfig,
40 /// Marker preserving the executor type parameters.
41 marker: PhantomData<fn() -> (T, E)>,
42}
43
44impl<T, E> CasBuilder<T, E> {
45 /// Creates a builder with default retry options.
46 ///
47 /// # Returns
48 /// A [`CasBuilder`] using [`RetryOptions::default`].
49 pub fn new() -> Self {
50 Self {
51 retry: RetryBuilder::new(),
52 observability: CasObservabilityConfig::default(),
53 marker: PhantomData,
54 }
55 }
56
57 /// Replaces builder settings from an existing retry option snapshot.
58 ///
59 /// # Parameters
60 /// - `options`: Retry option snapshot to install.
61 ///
62 /// # Returns
63 /// The updated builder.
64 pub fn options(mut self, options: RetryOptions) -> Self {
65 self.retry = self.retry.options(options);
66 self
67 }
68
69 /// Sets the maximum total attempts.
70 ///
71 /// # Parameters
72 /// - `max_attempts`: Maximum attempts, including the initial attempt.
73 ///
74 /// # Returns
75 /// The updated builder.
76 pub fn max_attempts(mut self, max_attempts: u32) -> Self {
77 self.retry = self.retry.max_attempts(max_attempts);
78 self
79 }
80
81 /// Sets the maximum retries after the initial attempt.
82 ///
83 /// # Parameters
84 /// - `max_retries`: Maximum retries after the first attempt.
85 ///
86 /// # Returns
87 /// The updated builder.
88 #[inline]
89 pub fn max_retries(self, max_retries: u32) -> Self {
90 self.max_attempts(max_retries.saturating_add(1))
91 }
92
93 /// Sets the maximum cumulative user operation elapsed-time budget.
94 ///
95 /// # Parameters
96 /// - `max_operation_elapsed`: Optional cumulative user operation time budget.
97 ///
98 /// # Returns
99 /// The updated builder.
100 #[inline]
101 pub fn max_operation_elapsed(mut self, max_operation_elapsed: Option<Duration>) -> Self {
102 self.retry = self.retry.max_operation_elapsed(max_operation_elapsed);
103 self
104 }
105
106 /// Sets the maximum monotonic elapsed-time budget for the whole retry flow.
107 ///
108 /// # Parameters
109 /// - `max_total_elapsed`: Optional total retry-flow time budget.
110 ///
111 /// # Returns
112 /// The updated builder.
113 #[inline]
114 pub fn max_total_elapsed(mut self, max_total_elapsed: Option<Duration>) -> Self {
115 self.retry = self.retry.max_total_elapsed(max_total_elapsed);
116 self
117 }
118
119 /// Sets the base retry delay strategy.
120 ///
121 /// # Parameters
122 /// - `delay`: Delay strategy used between failed attempts.
123 ///
124 /// # Returns
125 /// The updated builder.
126 #[inline]
127 pub fn delay(mut self, delay: RetryDelay) -> Self {
128 self.retry = self.retry.delay(delay);
129 self
130 }
131
132 /// Uses immediate retries with no sleep.
133 ///
134 /// # Returns
135 /// The updated builder.
136 #[inline]
137 pub fn no_delay(self) -> Self {
138 self.delay(RetryDelay::none())
139 }
140
141 /// Uses one fixed retry delay.
142 ///
143 /// # Parameters
144 /// - `delay`: Delay slept before each retry.
145 ///
146 /// # Returns
147 /// The updated builder.
148 #[inline]
149 pub fn fixed_delay(self, delay: Duration) -> Self {
150 self.delay(RetryDelay::fixed(delay))
151 }
152
153 /// Uses one random retry delay range.
154 ///
155 /// # Parameters
156 /// - `min`: Inclusive minimum delay.
157 /// - `max`: Inclusive maximum delay.
158 ///
159 /// # Returns
160 /// The updated builder.
161 #[inline]
162 pub fn random_delay(self, min: Duration, max: Duration) -> Self {
163 self.delay(RetryDelay::random(min, max))
164 }
165
166 /// Uses exponential backoff with multiplier `2.0`.
167 ///
168 /// # Parameters
169 /// - `initial`: Initial retry delay.
170 /// - `max`: Maximum retry delay.
171 ///
172 /// # Returns
173 /// The updated builder.
174 #[inline]
175 pub fn exponential_backoff(self, initial: Duration, max: Duration) -> Self {
176 self.exponential_backoff_with_multiplier(initial, max, 2.0)
177 }
178
179 /// Uses exponential backoff with a custom multiplier.
180 ///
181 /// # Parameters
182 /// - `initial`: Initial retry delay.
183 /// - `max`: Maximum retry delay.
184 /// - `multiplier`: Multiplier applied after each failed attempt.
185 ///
186 /// # Returns
187 /// The updated builder.
188 #[inline]
189 pub fn exponential_backoff_with_multiplier(
190 self,
191 initial: Duration,
192 max: Duration,
193 multiplier: f64,
194 ) -> Self {
195 self.delay(RetryDelay::exponential(initial, max, multiplier))
196 }
197
198 /// Sets the jitter strategy.
199 ///
200 /// # Parameters
201 /// - `jitter`: Jitter strategy applied to retry delays.
202 ///
203 /// # Returns
204 /// The updated builder.
205 #[inline]
206 pub fn jitter(mut self, jitter: RetryJitter) -> Self {
207 self.retry = self.retry.jitter(jitter);
208 self
209 }
210
211 /// Sets relative jitter by factor.
212 ///
213 /// # Parameters
214 /// - `factor`: Relative jitter factor in `[0.0, 1.0]`.
215 ///
216 /// # Returns
217 /// The updated builder.
218 #[inline]
219 pub fn jitter_factor(self, factor: f64) -> Self {
220 self.jitter(RetryJitter::factor(factor))
221 }
222
223 /// Sets the async per-attempt timeout.
224 ///
225 /// # Parameters
226 /// - `attempt_timeout`: Timeout applied to each async CAS attempt.
227 ///
228 /// # Returns
229 /// The updated builder.
230 #[inline]
231 pub fn attempt_timeout(mut self, attempt_timeout: Option<Duration>) -> Self {
232 self.retry = self.retry.attempt_timeout(attempt_timeout);
233 self
234 }
235
236 /// Sets the complete retry-layer attempt timeout option.
237 ///
238 /// # Parameters
239 /// - `attempt_timeout`: Timeout option owned by the retry layer.
240 ///
241 /// # Returns
242 /// The updated builder.
243 #[inline]
244 pub fn attempt_timeout_option(mut self, attempt_timeout: Option<AttemptTimeoutOption>) -> Self {
245 self.retry = self.retry.attempt_timeout_option(attempt_timeout);
246 self
247 }
248
249 /// Retries attempts that exceed the configured timeout.
250 ///
251 /// # Returns
252 /// The updated builder.
253 #[inline]
254 pub fn retry_on_timeout(mut self) -> Self {
255 self.retry = self.retry.retry_on_timeout();
256 self
257 }
258
259 /// Aborts the CAS flow when one attempt exceeds the timeout.
260 ///
261 /// # Returns
262 /// The updated builder.
263 #[inline]
264 pub fn abort_on_timeout(mut self) -> Self {
265 self.retry = self.retry.abort_on_timeout();
266 self
267 }
268
269 /// Applies a built-in CAS strategy to this builder.
270 ///
271 /// # Parameters
272 /// - `strategy`: Strategy profile to install.
273 ///
274 /// # Returns
275 /// The updated builder.
276 pub fn strategy(self, strategy: CasStrategy) -> Self {
277 let profile = strategy.profile();
278 let builder = self
279 .max_attempts(profile.max_attempts())
280 .max_operation_elapsed(Some(profile.max_operation_elapsed()))
281 .max_total_elapsed(profile.max_total_elapsed());
282 if let Some((initial, max, jitter)) = strategy.backoff() {
283 builder
284 .exponential_backoff(initial, max)
285 .jitter_factor(jitter)
286 } else {
287 builder.no_delay()
288 }
289 }
290
291 /// Installs observability configuration.
292 ///
293 /// # Parameters
294 /// - `observability`: Observability settings to use.
295 ///
296 /// # Returns
297 /// The updated builder.
298 #[inline]
299 pub fn observability(mut self, observability: CasObservabilityConfig) -> Self {
300 self.observability = observability;
301 self
302 }
303
304 /// Enables contention alerting with the supplied thresholds.
305 ///
306 /// # Parameters
307 /// - `thresholds`: Thresholds used to classify hot contention.
308 ///
309 /// # Returns
310 /// The updated builder.
311 #[inline]
312 pub fn alert_on_contention(mut self, thresholds: ContentionThresholds) -> Self {
313 self.observability = self.observability.with_contention_thresholds(thresholds);
314 self
315 }
316
317 /// Enables retry-layer listener panic isolation.
318 ///
319 /// # Returns
320 /// The updated builder.
321 #[inline]
322 pub fn isolate_listener_panics(mut self) -> Self {
323 self.observability = self
324 .observability
325 .with_listener_panic_policy(ListenerPanicPolicy::Isolate);
326 self
327 }
328
329 /// Builds one executor after validating the settings.
330 ///
331 /// # Returns
332 /// A validated [`CasExecutor`].
333 ///
334 /// # Errors
335 /// Returns [`RetryConfigError`] when the configured retry settings are
336 /// invalid.
337 pub fn build(self) -> Result<CasExecutor<T, E>, RetryConfigError> {
338 let retry = self.retry.build()?;
339 Ok(CasExecutor::new(
340 retry.options().clone(),
341 self.observability,
342 ))
343 }
344
345 /// Builds one executor with the contention-adaptive strategy.
346 ///
347 /// # Returns
348 /// A configured [`CasExecutor`] suitable for contended writers.
349 pub fn build_contention_adaptive(self) -> Result<CasExecutor<T, E>, RetryConfigError> {
350 self.strategy(CasStrategy::ContentionAdaptive).build()
351 }
352
353 /// Builds one executor with the latency-first strategy.
354 ///
355 /// # Returns
356 /// A configured [`CasExecutor`] optimized for low latency.
357 pub fn build_latency_first(self) -> Result<CasExecutor<T, E>, RetryConfigError> {
358 self.strategy(CasStrategy::LatencyFirst).build()
359 }
360
361 /// Builds one executor with the reliability-first strategy.
362 ///
363 /// # Returns
364 /// A configured [`CasExecutor`] optimized for long retry windows.
365 pub fn build_reliability_first(self) -> Result<CasExecutor<T, E>, RetryConfigError> {
366 self.strategy(CasStrategy::ReliabilityFirst).build()
367 }
368}
369
370impl<T, E> Default for CasBuilder<T, E> {
371 /// Creates a default CAS builder.
372 ///
373 /// # Returns
374 /// A builder equivalent to [`CasBuilder::new`].
375 #[inline]
376 fn default() -> Self {
377 Self::new()
378 }
379}