Skip to main content

google_cloud_gax/
retry_throttler.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines traits for retry throttling and some common implementations.
16//!
17//! The client libraries can be configured to automatically retry RPCs. Most
18//! often retries are only enabled if (1) the request was never sent and the
19//! local error is recoverable, or (2) the failure is a transient errors
20//! **and** the RPC is [idempotent].
21//!
22//! Retry strategies that do not throttle themselves can slow down recovery when
23//! the service is overloaded, or when recovering from a large incident. This is
24//! also known as "retry storms": the retry attempts can grow to be more than
25//! the normal traffic and hinder recovery.
26//!
27//! Advanced applications may want to configure a retry throttler to
28//! [Address Cascading Failures] and when [Handling Overload] conditions.
29//! This module contains the traits and some implementations of retry throttling
30//! strategies.
31//!
32//! To configure the default throttler for a client, use
33//! [ClientBuilder::with_retry_throttler]. To configure the throttler used for
34//! a specific request, use [RequestOptionsBuilder::with_retry_throttler].
35//!
36//! Typically applications should create one retry throttler and share it
37//! across multiple clients.
38//!
39//! [ClientBuilder::with_retry_throttler]: crate::client_builder::ClientBuilder::with_retry_throttler
40//! [RequestOptionsBuilder::with_retry_throttler]: crate::options::RequestOptionsBuilder::with_retry_throttler
41//! [Handling Overload]: https://sre.google/sre-book/handling-overload/
42//! [Address Cascading Failures]: https://sre.google/sre-book/addressing-cascading-failures/
43//!
44//! # Example
45//!
46//! Stochastically reject calls based on observed failure rates:
47//! ```
48//! # use google_cloud_gax::retry_throttler::*;
49//! let throttler = AdaptiveThrottler::new(2.0)?;
50//! # Ok::<(), Error>(())
51//! ```
52//!
53//! Reject calls if the success rate is too low:
54//! ```
55//! # use google_cloud_gax::retry_throttler::*;
56//! let tokens = 1000;
57//! let min_tokens = 250;
58//! let error_cost = 10;
59//! let throttler = CircuitBreaker::new(tokens, min_tokens, error_cost)?;
60//! # Ok::<(), Error>(())
61//! ```
62//!
63//! [idempotent]: https://en.wikipedia.org/wiki/Idempotence
64
65use crate::retry_result::RetryResult;
66use rand::RngExt;
67use std::sync::{Arc, Mutex};
68
69/// The error type for throttler policy creation.
70#[derive(thiserror::Error, Debug)]
71#[non_exhaustive]
72/// Errors that can occur when building a retry throttler.
73pub enum Error {
74    /// The scaling factor is out of range (must be >= 0.0).
75    #[error("the scaling factor ({0}) must be greater or equal than 0.0")]
76    ScalingOutOfRange(f64),
77    /// The minimum tokens must be less than or equal to the initial tokens.
78    #[error(
79        "the minimum tokens ({min}) must be less than or equal to the initial token ({initial}) count"
80    )]
81    TooFewMinTokens {
82        /// The minimum tokens.
83        min: u64,
84        /// The initial tokens.
85        initial: u64,
86    },
87}
88
89/// Implementations of this trait prevent a client from sending too many retries.
90///
91/// Retry throttlers are shared by all the requests in a client, and may even be
92/// shared by multiple clients. The library provides a default implementation
93/// (and instance) on each client. The application may choose a different
94/// implementation or instance if the default is not suitable.
95///
96/// Implementations of this trait must also implement [Debug][std::fmt::Debug]
97/// because the application may need to log the client state. The trait is
98/// passed between async functions, so its implementations must be `Send`
99/// and `Sync`.
100pub trait RetryThrottler: Send + Sync + std::fmt::Debug {
101    /// Called by the retry loop before issuing a retry attempt. Returns `true`
102    /// if the request should be throttled.
103    ///
104    /// The retry loop would simulate a failure when this function returns
105    /// `false`. Note that the retry loop may stop if too many attempts are
106    /// throttled: they are treated as transient errors and may exhaust the
107    /// retry policy.
108    #[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
109    fn throttle_retry_attempt(&self) -> bool;
110
111    /// Called by the retry loop after a retry failure.
112    #[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
113    fn on_retry_failure(&mut self, flow: &RetryResult);
114
115    /// Called by the retry loop when a RPC succeeds.
116    #[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
117    fn on_success(&mut self);
118}
119
120/// Retry throttlers are shared by many clients, so they are wrapped in `Arc<>`.
121/// Consequently, they are used from many threads at the same time, so they are
122/// wrapped in `Mutex`.
123pub type SharedRetryThrottler = Arc<Mutex<dyn RetryThrottler>>;
124
125/// A helper type to use [RetryThrottler] in client and request options.
126#[derive(Clone, Debug)]
127pub struct RetryThrottlerArg(SharedRetryThrottler);
128
129impl<T: RetryThrottler + 'static> From<T> for RetryThrottlerArg {
130    fn from(value: T) -> Self {
131        Self(Arc::new(Mutex::new(value)))
132    }
133}
134
135impl From<SharedRetryThrottler> for RetryThrottlerArg {
136    fn from(value: SharedRetryThrottler) -> Self {
137        Self(value)
138    }
139}
140
141impl From<RetryThrottlerArg> for SharedRetryThrottler {
142    fn from(value: RetryThrottlerArg) -> SharedRetryThrottler {
143        value.0
144    }
145}
146
147/// Implements a probabilistic throttler based on observed failure rates.
148///
149/// This is an implementation of the [Adaptive Throttling] strategy described
150/// in [Site Reliability Engineering] book. The basic idea is to
151/// *stochastically* reject some of the retry attempts, with a rejection
152/// probability that increases as the number of failures increases, and
153/// decreases with the number of successful requests.
154///
155/// The rejection rate probability is defined by:
156///
157/// ```norust
158/// threshold = (requests - factor * accepts) / (requests + 1)
159/// rejection_probability = max(0, threshold)
160/// ```
161///
162/// Where `requests` is the number of requests completed, and `accepts` is the
163/// number of requests accepted by the service, including requests that fail due
164/// to parameter validation, authorization checks, or any non-transient
165/// failures.
166///
167/// Note that `accepts <= requests` but the `threshold` value might be negative
168/// as `factor` can be higher than `1.0`. In fact, the SRE book recommends using
169/// `2.0` as the initial factor.
170///
171/// Setting `factor` to lower values makes the algorithm reject retry attempts
172/// with higher probability. For example, setting it to zero would reject some
173/// retry attempts even if all requests have succeeded. Setting `factor` to
174/// higher values allows more retry attempts.
175///
176/// # Example
177/// ```
178/// # use google_cloud_gax::retry_throttler::*;
179/// let throttler = AdaptiveThrottler::new(2.0)?;
180/// # Ok::<(), Error>(())
181/// ```
182///
183/// [Site Reliability Engineering]: https://sre.google/sre-book/table-of-contents/
184/// [Adaptive Throttling]: https://sre.google/sre-book/handling-overload/
185#[derive(Clone, Debug)]
186pub struct AdaptiveThrottler {
187    accept_count: f64,
188    request_count: f64,
189    factor: f64,
190}
191
192impl AdaptiveThrottler {
193    /// Creates a new adaptive throttler with the given `factor`.
194    ///
195    /// # Parameters
196    /// * `factor` - a factor to adjust the relative weight of transient
197    ///   failures vs. accepted requests. See the struct definition for
198    ///   details.
199    ///
200    /// # Example
201    /// ```
202    /// # use google_cloud_gax::retry_throttler::*;
203    /// let throttler = AdaptiveThrottler::new(2.0)?;
204    /// # Ok::<(), Error>(())
205    /// ```
206    pub fn new(factor: f64) -> Result<Self, Error> {
207        if factor < 0.0 {
208            return Err(Error::ScalingOutOfRange(factor));
209        }
210        let factor = if factor < 0.0 { 0.0 } else { factor };
211        Ok(Self::clamp(factor))
212    }
213
214    /// Creates a new adaptive throttler clamping `factor` to a valid range.
215    ///
216    /// # Parameters
217    /// * `factor` - a factor to adjust the relative weight of transient
218    ///   failures vs. accepted requests. See the struct definition for
219    ///   details. Clamped to zero if the value is negative.
220    ///
221    /// # Example
222    /// ```
223    /// # use google_cloud_gax::retry_throttler::*;
224    /// let throttler = AdaptiveThrottler::clamp(2.0);
225    /// ```
226    pub fn clamp(factor: f64) -> Self {
227        let factor = if factor < 0.0 { 0.0 } else { factor };
228        Self {
229            accept_count: 0.0,
230            request_count: 0.0,
231            factor,
232        }
233    }
234
235    // A testable version of `throttle_retry_attempt()`.
236    fn throttle<R: rand::Rng>(&self, rng: &mut R) -> bool {
237        let reject_probability =
238            (self.request_count - self.factor * self.accept_count) / (self.request_count + 1.0);
239        let reject_probability = if reject_probability < 0.0 {
240            0_f64
241        } else {
242            reject_probability
243        };
244        rng.random_range(0.0..=1.0) <= reject_probability
245    }
246}
247
248impl std::default::Default for AdaptiveThrottler {
249    /// Returns an `AdaptiveThrottler` initialized to the recommended values.
250    ///
251    /// # Example
252    /// ```
253    /// # use google_cloud_gax::retry_throttler::*;
254    /// let throttler = AdaptiveThrottler::default();
255    /// ```
256    fn default() -> Self {
257        Self::clamp(2.0)
258    }
259}
260
261impl RetryThrottler for AdaptiveThrottler {
262    fn throttle_retry_attempt(&self) -> bool {
263        self.throttle(&mut rand::rng())
264    }
265
266    fn on_retry_failure(&mut self, flow: &RetryResult) {
267        self.request_count += 1.0;
268        match flow {
269            RetryResult::Continue(_) | RetryResult::Exhausted(_) => {}
270            RetryResult::Permanent(_) => {
271                self.accept_count += 1.0;
272            }
273        };
274    }
275
276    fn on_success(&mut self) {
277        self.request_count += 1.0;
278        self.accept_count += 1.0;
279    }
280}
281
282/// A `CircuitBreaker` throttler rejects retry attempts if the success rate is too low.
283///
284/// This struct implements the [gRPC throttler] algorithm. The throttler works
285/// by tracking the number of available "tokens" for a retry attempt. If this
286/// number goes below a threshold **all** retry attempts are throttled.
287///
288/// Retry failures decrement the number of tokens by a given cost. Completed
289/// requests (successfully or not) increase the tokens by `1`.
290///
291/// Note: the number of tokens may go below the throttling threshold as
292/// multiple concurrent requests may fail and decrease the token count.
293///
294/// Note: throttling only applies to retry attempts, the initial requests is
295/// never throttled. This may increases the token count even if all retry
296/// attempts are throttled.
297///
298/// # Examples
299/// ```
300/// # use google_cloud_gax::retry_throttler::*;
301/// let tokens = 1000;
302/// let min_tokens = 250;
303/// let error_cost = 10;
304/// let throttler = CircuitBreaker::new(tokens, min_tokens, error_cost)?;
305/// # Ok::<(), Error>(())
306/// ```
307///
308/// [ClientBuilder::with_retry_throttler]: crate::client_builder::ClientBuilder::with_retry_throttler
309/// [gRPC throttler]: https://github.com/grpc/proposal/blob/master/A6-client-retries.md
310#[derive(Clone, Debug)]
311pub struct CircuitBreaker {
312    max_tokens: u64,
313    min_tokens: u64,
314    cur_tokens: u64,
315    error_cost: u64,
316}
317
318impl CircuitBreaker {
319    /// Creates a new instance.
320    ///
321    /// # Parameters
322    /// * `tokens` - the initial number of tokens. This is decreased by
323    ///   `error_cost` on each retry failure, and increased by `1` when a
324    ///   request succeeds.
325    /// * `min_tokens` - stops accepting retry attempts when the number of
326    ///   tokens is at or below this value.
327    /// * `error_cost` - decrease the token count by this value on failed
328    ///   request attempts.
329    ///
330    /// # Example
331    /// ```
332    /// # use google_cloud_gax::retry_throttler::*;
333    /// let throttler = CircuitBreaker::new(1000, 250, 10)?;
334    /// # Ok::<(), Error>(())
335    /// ```
336    pub fn new(tokens: u64, min_tokens: u64, error_cost: u64) -> Result<Self, Error> {
337        if min_tokens > tokens {
338            return Err(Error::TooFewMinTokens {
339                min: min_tokens,
340                initial: tokens,
341            });
342        }
343        Ok(Self {
344            max_tokens: tokens,
345            min_tokens,
346            cur_tokens: tokens,
347            error_cost,
348        })
349    }
350
351    /// Creates a new instance, adjusting `min_tokens` if needed.
352    ///
353    /// # Parameters
354    /// * `tokens` - the initial number of tokens. This is decreased by
355    ///   `error_cost` on each retry failure, and increased by `1` when a
356    ///   request succeeds.
357    /// * `min_tokens` - stops accepting retry attempts when the number of
358    ///   tokens is at or below this value. Clamped to be `<= tokens`.
359    /// * `error_cost` - decrease the token count by this value on failed
360    ///   request attempts.
361    ///
362    /// # Example
363    /// ```
364    /// # use google_cloud_gax::retry_throttler::*;
365    /// let throttler = CircuitBreaker::clamp(1000, 250, 10);
366    /// ```
367    pub fn clamp(tokens: u64, min_tokens: u64, error_cost: u64) -> Self {
368        Self {
369            max_tokens: tokens,
370            min_tokens: std::cmp::min(min_tokens, tokens),
371            cur_tokens: tokens,
372            error_cost,
373        }
374    }
375}
376
377impl std::default::Default for CircuitBreaker {
378    /// Initialize a `CircuitBreaker` configured with recommended values.
379    ///
380    /// # Example
381    /// ```
382    /// # use google_cloud_gax::retry_throttler::*;
383    /// let throttler = CircuitBreaker::default();
384    /// ```
385    fn default() -> Self {
386        CircuitBreaker::clamp(100, 50, 10)
387    }
388}
389
390impl RetryThrottler for CircuitBreaker {
391    fn throttle_retry_attempt(&self) -> bool {
392        self.cur_tokens <= self.min_tokens
393    }
394
395    fn on_retry_failure(&mut self, flow: &RetryResult) {
396        match flow {
397            RetryResult::Continue(_) | RetryResult::Exhausted(_) => {
398                self.cur_tokens = self.cur_tokens.saturating_sub(self.error_cost);
399            }
400            RetryResult::Permanent(_) => {
401                self.on_success();
402            }
403        };
404    }
405
406    fn on_success(&mut self) {
407        self.cur_tokens = std::cmp::min(self.max_tokens, self.cur_tokens.saturating_add(1));
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::mock_rng::MockRng;
415    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
416
417    // Verify `RetryThrottlerArg` can be converted from the desired types.
418    #[test]
419    fn retry_throttler_arg() {
420        let throttler = AdaptiveThrottler::default();
421        let _ = RetryThrottlerArg::from(throttler);
422
423        let throttler: Arc<Mutex<dyn RetryThrottler>> =
424            Arc::new(Mutex::new(CircuitBreaker::default()));
425        let _ = RetryThrottlerArg::from(throttler);
426    }
427
428    #[test]
429    fn adaptive_construction() {
430        let throttler = AdaptiveThrottler::new(-2.0);
431        assert!(
432            matches!(throttler, Err(Error::ScalingOutOfRange { .. })),
433            "{throttler:?}"
434        );
435
436        let throttler = AdaptiveThrottler::new(0.0);
437        assert!(throttler.is_ok(), "{throttler:?}");
438    }
439
440    fn test_error() -> crate::error::Error {
441        use crate::error::{
442            Error,
443            rpc::{Code, Status},
444        };
445        Error::service(Status::default().set_code(Code::Aborted))
446    }
447
448    #[test]
449    fn adaptive() -> TestResult {
450        let mut throttler = AdaptiveThrottler::default();
451        assert_eq!(throttler.request_count, 0.0);
452        assert_eq!(throttler.accept_count, 0.0);
453        assert_eq!(throttler.factor, 2.0);
454
455        assert!(!throttler.throttle_retry_attempt(), "{throttler:?}");
456
457        throttler.on_retry_failure(&RetryResult::Continue(test_error()));
458        assert_eq!(throttler.request_count, 1.0);
459        assert_eq!(throttler.accept_count, 0.0);
460
461        throttler.on_retry_failure(&RetryResult::Continue(test_error()));
462        assert_eq!(throttler.request_count, 2.0);
463        assert_eq!(throttler.accept_count, 0.0);
464
465        throttler.on_success();
466        assert_eq!(throttler.request_count, 3.0);
467        assert_eq!(throttler.accept_count, 1.0);
468
469        throttler.on_retry_failure(&RetryResult::Permanent(test_error()));
470        assert_eq!(throttler.request_count, 4.0);
471        assert_eq!(throttler.accept_count, 2.0);
472
473        let mut throttler = AdaptiveThrottler::default();
474        throttler.on_retry_failure(&RetryResult::Continue(test_error()));
475
476        // StepRng::new(x, 0) always produces the same value. We pick the values
477        // to trigger the desired behavior.
478        let mut rng = MockRng::new(0);
479        assert_eq!(rng.random_range(0.0..=1.0), 0.0);
480        assert!(throttler.throttle(&mut rng), "{throttler:?}");
481
482        let mut rng = MockRng::new(u64::MAX - u64::MAX / 4);
483        assert!(
484            rng.random_range(0.0..=1.0) > 0.5,
485            "{}",
486            rng.random_range(0.0..=1.0)
487        );
488        assert!(!throttler.throttle(&mut rng), "{throttler:?}");
489
490        // This creates a throttler with reject probability == 0.
491        let mut throttler = AdaptiveThrottler::new(100.0)?;
492        throttler.on_success();
493        assert!(!throttler.throttle_retry_attempt(), "{throttler:?}");
494
495        Ok(())
496    }
497
498    #[test]
499    fn circuit_breaker_validation() {
500        let throttler = CircuitBreaker::new(100, 200, 1);
501        assert!(
502            matches!(throttler, Err(Error::TooFewMinTokens { .. })),
503            "{throttler:?}"
504        );
505    }
506
507    #[test]
508    fn circuit_breaker() {
509        let mut throttler = CircuitBreaker::default();
510        assert!(!throttler.throttle_retry_attempt(), "{throttler:?}");
511
512        for _ in 0..4 {
513            throttler.on_retry_failure(&RetryResult::Continue(test_error()));
514            assert!(!throttler.throttle_retry_attempt(), "{throttler:?}");
515        }
516        // This crosses the threshold:
517        throttler.on_retry_failure(&RetryResult::Continue(test_error()));
518        throttler.on_retry_failure(&RetryResult::Continue(test_error()));
519        assert!(throttler.throttle_retry_attempt(), "{throttler:?}");
520
521        // With the default settings, we will need about 10x successful calls
522        // to recover.
523        for _ in 0..10 {
524            throttler.on_success();
525            assert!(throttler.throttle_retry_attempt(), "{throttler:?}");
526        }
527        throttler.on_success();
528        assert!(!throttler.throttle_retry_attempt(), "{throttler:?}");
529
530        // Permanent errors also open back the throttle.
531        throttler.on_retry_failure(&RetryResult::Continue(test_error()));
532        for _ in 0..9 {
533            throttler.on_retry_failure(&RetryResult::Permanent(test_error()));
534            assert!(throttler.throttle_retry_attempt(), "{throttler:?}");
535        }
536        throttler.on_retry_failure(&RetryResult::Permanent(test_error()));
537        assert!(!throttler.throttle_retry_attempt(), "{throttler:?}");
538    }
539}