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}