codetether_agent/provider/bedrock/retry.rs
1//! Retry helper for transient Bedrock errors.
2//!
3//! Bedrock throttles aggressively under load. This module implements
4//! exponential backoff + jitter for `ThrottlingException` (HTTP 429) and
5//! `ServiceUnavailableException` (HTTP 503). Non-retryable errors (auth,
6//! validation, 4xx other than 429) are returned immediately.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use codetether_agent::provider::bedrock::retry::{RetryPolicy, should_retry_status};
12//!
13//! let policy = RetryPolicy::default();
14//! assert_eq!(policy.max_attempts, 4);
15//! assert!(should_retry_status(429));
16//! assert!(should_retry_status(503));
17//! assert!(!should_retry_status(400));
18//! assert!(!should_retry_status(200));
19//! ```
20
21use rand::{Rng, RngExt};
22use std::time::Duration;
23
24/// Exponential backoff config.
25#[derive(Debug, Clone, Copy)]
26pub struct RetryPolicy {
27 /// Total number of attempts (including the first). `1` disables retry.
28 pub max_attempts: u32,
29 /// Base delay before the first retry (e.g. 500ms).
30 pub base_delay: Duration,
31 /// Cap on any single sleep interval.
32 pub max_delay: Duration,
33}
34
35impl Default for RetryPolicy {
36 fn default() -> Self {
37 Self {
38 max_attempts: 4,
39 base_delay: Duration::from_millis(500),
40 max_delay: Duration::from_secs(8),
41 }
42 }
43}
44
45impl RetryPolicy {
46 /// Compute the sleep for attempt `n` (1-indexed, first *retry* is n=1).
47 /// Applies full jitter: `random(0, min(max_delay, base * 2^(n-1)))`.
48 pub fn delay_for(&self, n: u32) -> Duration {
49 let shift = n.saturating_sub(1).min(10);
50 let ceiling = self
51 .base_delay
52 .saturating_mul(1u32 << shift)
53 .min(self.max_delay);
54 let millis = rand::rng().random_range(0..=ceiling.as_millis() as u64);
55 Duration::from_millis(millis)
56 }
57}
58
59/// Return true for HTTP status codes worth retrying.
60pub fn should_retry_status(status: u16) -> bool {
61 matches!(status, 429 | 500 | 502 | 503 | 504)
62}