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}