Skip to main content

couchbase_core/
retrybesteffort.rs

1/*
2 *
3 *  * Copyright (c) 2025 Couchbase, Inc.
4 *  *
5 *  * Licensed under the Apache License, Version 2.0 (the "License");
6 *  * you may not use this file except in compliance with the License.
7 *  * You may obtain a copy of the License at
8 *  *
9 *  *    http://www.apache.org/licenses/LICENSE-2.0
10 *  *
11 *  * Unless required by applicable law or agreed to in writing, software
12 *  * distributed under the License is distributed on an "AS IS" BASIS,
13 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  * See the License for the specific language governing permissions and
15 *  * limitations under the License.
16 *
17 */
18
19use std::fmt::Debug;
20use std::time::Duration;
21
22use crate::retry::{RetryAction, RetryReason, RetryRequest, RetryStrategy};
23
24/// A retry strategy that retries all eligible operations using a configurable
25/// backoff calculator.
26///
27/// This is the default retry strategy used by the SDK. It retries idempotent
28/// operations unconditionally, and non-idempotent operations only when the
29/// [`RetryReason`] indicates it is safe to do so.
30///
31/// The backoff duration between retries is determined by the [`BackoffCalculator`]
32/// provided at construction time. If none is specified, an
33/// [`ExponentialBackoffCalculator`] with sensible defaults is used.
34///
35/// # Example
36///
37/// ```rust
38/// use couchbase_core::retrybesteffort::BestEffortRetryStrategy;
39/// use std::sync::Arc;
40///
41/// // Use the default exponential backoff:
42/// let strategy = Arc::new(BestEffortRetryStrategy::default());
43/// ```
44#[derive(Debug, Clone)]
45pub struct BestEffortRetryStrategy<Calc> {
46    backoff_calc: Calc,
47}
48
49impl<Calc> BestEffortRetryStrategy<Calc>
50where
51    Calc: BackoffCalculator,
52{
53    /// Creates a new `BestEffortRetryStrategy` with the given backoff calculator.
54    pub fn new(calc: Calc) -> Self {
55        Self { backoff_calc: calc }
56    }
57}
58
59impl Default for BestEffortRetryStrategy<ExponentialBackoffCalculator> {
60    fn default() -> Self {
61        Self::new(ExponentialBackoffCalculator::default())
62    }
63}
64
65impl<Calc> RetryStrategy for BestEffortRetryStrategy<Calc>
66where
67    Calc: BackoffCalculator,
68{
69    fn retry_after(&self, request: &RetryRequest, reason: &RetryReason) -> Option<RetryAction> {
70        if request.is_idempotent() || reason.allows_non_idempotent_retry() {
71            Some(RetryAction::new(
72                self.backoff_calc.backoff(request.retry_attempts()),
73            ))
74        } else {
75            None
76        }
77    }
78}
79
80/// A calculator that determines the backoff duration between retry attempts.
81///
82/// Implement this trait to provide custom backoff logic. The SDK ships with
83/// [`ExponentialBackoffCalculator`] as the default implementation.
84pub trait BackoffCalculator: Debug + Send + Sync {
85    /// Returns the duration to wait before the given retry attempt number.
86    ///
87    /// `retry_attempts` starts at 0 for the first retry.
88    fn backoff(&self, retry_attempts: u32) -> Duration;
89}
90
91/// An exponential backoff calculator that increases the delay between retries
92/// exponentially, clamped between a minimum and maximum duration.
93///
94/// The backoff for attempt `n` is calculated as:
95///
96/// ```text
97/// clamp(min * backoff_factor ^ n, min, max)
98/// ```
99///
100/// # Defaults
101///
102/// | Parameter | Default |
103/// |-----------|---------|
104/// | `min` | 1 ms |
105/// | `max` | 1000 ms |
106/// | `backoff_factor` | 2.0 |
107///
108/// # Example
109///
110/// ```rust
111/// use couchbase_core::retrybesteffort::{BestEffortRetryStrategy, ExponentialBackoffCalculator};
112/// use std::time::Duration;
113/// use std::sync::Arc;
114///
115/// let calc = ExponentialBackoffCalculator::new(
116///     Duration::from_millis(5),   // min
117///     Duration::from_millis(500), // max
118///     2.0,                        // backoff_factor
119/// );
120/// let strategy = Arc::new(BestEffortRetryStrategy::new(calc));
121/// ```
122#[derive(Clone, Debug)]
123pub struct ExponentialBackoffCalculator {
124    min: Duration,
125    max: Duration,
126    backoff_factor: f64,
127}
128
129impl ExponentialBackoffCalculator {
130    /// Creates a new `ExponentialBackoffCalculator`.
131    ///
132    /// * `min` — Minimum backoff duration (floor).
133    /// * `max` — Maximum backoff duration (ceiling).
134    /// * `backoff_factor` — Multiplier applied per retry attempt (typically `2.0`).
135    pub fn new(min: Duration, max: Duration, backoff_factor: f64) -> Self {
136        Self {
137            min,
138            max,
139            backoff_factor,
140        }
141    }
142}
143
144impl BackoffCalculator for ExponentialBackoffCalculator {
145    fn backoff(&self, retry_attempts: u32) -> Duration {
146        let factor = self.backoff_factor.powi(retry_attempts as i32);
147        let factor_u128 = factor as u128;
148
149        if u128::MAX / self.min.as_millis() < factor_u128 {
150            // If the factor is too large, we cap it to prevent overflow.
151            return self.max;
152        }
153
154        let val = self.min.as_millis() * factor_u128;
155
156        if val > u64::MAX as u128 {
157            // If the value exceeds u64::MAX, we cap it to max.
158            return self.max;
159        }
160
161        let mut backoff = Duration::from_millis(val as u64);
162
163        if backoff > self.max {
164            backoff = self.max;
165        }
166        if backoff < self.min {
167            backoff = self.min
168        }
169
170        backoff
171    }
172}
173
174impl Default for ExponentialBackoffCalculator {
175    fn default() -> Self {
176        Self {
177            min: Duration::from_millis(1),
178            max: Duration::from_millis(1000),
179            backoff_factor: 2.0,
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_exponential_backoff() {
190        let calculator = ExponentialBackoffCalculator::new(
191            Duration::from_millis(10),
192            Duration::from_millis(1000),
193            2.0,
194        );
195
196        assert_eq!(calculator.backoff(0), Duration::from_millis(10));
197        assert_eq!(calculator.backoff(1), Duration::from_millis(20));
198        assert_eq!(calculator.backoff(2), Duration::from_millis(40));
199        assert_eq!(calculator.backoff(3), Duration::from_millis(80));
200        assert_eq!(calculator.backoff(4), Duration::from_millis(160));
201        assert_eq!(calculator.backoff(5), Duration::from_millis(320));
202        assert_eq!(calculator.backoff(6), Duration::from_millis(640));
203        assert_eq!(calculator.backoff(7), Duration::from_millis(1000));
204    }
205
206    #[test]
207    fn test_exponential_backoff_overflows_u128() {
208        let calculator = ExponentialBackoffCalculator::new(
209            Duration::from_millis(100),
210            Duration::from_millis(1000),
211            1.5,
212        );
213
214        assert_eq!(calculator.backoff(208), Duration::from_millis(1000));
215    }
216
217    #[test]
218    fn test_exponential_backoff_overflows_u64() {
219        let calculator = ExponentialBackoffCalculator::new(
220            Duration::from_millis(100),
221            Duration::from_millis(1000),
222            1.5,
223        );
224
225        assert_eq!(calculator.backoff(207), Duration::from_millis(1000));
226    }
227}