Skip to main content

agentic_connect/types/
retry.rs

1//! Retry and circuit breaker types — Invention 4: Intelligent Retry Fabric.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Classification of a connection failure.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum FailureClass {
11    /// Temporary network issue, DNS timeout, 503.
12    Transient,
13    /// 404, 400, invalid auth — won't succeed on retry.
14    Permanent,
15    /// 429 with Retry-After header.
16    RateLimit,
17    /// 401/403 — may succeed after token refresh.
18    AuthFailure,
19    /// Connection refused, DNS failure.
20    NetworkError,
21    /// Server closed connection unexpectedly.
22    ServerError,
23}
24
25/// How to retry based on failure classification.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum RetryStrategy {
29    /// Exponential backoff (base_ms * 2^attempt).
30    ExponentialBackoff {
31        base_ms: u64,
32        max_ms: u64,
33        max_attempts: u32,
34    },
35    /// Wait a fixed duration then retry.
36    FixedDelay { delay_ms: u64, max_attempts: u32 },
37    /// Refresh auth then retry once.
38    RefreshAndRetry,
39    /// Don't retry — fail immediately.
40    FailFast,
41    /// Wait for the server-specified duration.
42    WaitRetryAfter,
43}
44
45impl Default for RetryStrategy {
46    fn default() -> Self {
47        RetryStrategy::ExponentialBackoff {
48            base_ms: 1000,
49            max_ms: 30_000,
50            max_attempts: 3,
51        }
52    }
53}
54
55/// Per-connection retry policy.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct RetryPolicy {
58    pub strategies: HashMap<String, RetryStrategy>,
59    pub default_strategy: RetryStrategy,
60}
61
62impl Default for RetryPolicy {
63    fn default() -> Self {
64        let mut strategies = HashMap::new();
65        strategies.insert("transient".to_string(), RetryStrategy::default());
66        strategies.insert(
67            "rate_limit".to_string(),
68            RetryStrategy::WaitRetryAfter,
69        );
70        strategies.insert(
71            "auth_failure".to_string(),
72            RetryStrategy::RefreshAndRetry,
73        );
74        strategies.insert(
75            "permanent".to_string(),
76            RetryStrategy::FailFast,
77        );
78        Self {
79            strategies,
80            default_strategy: RetryStrategy::default(),
81        }
82    }
83}
84
85/// Circuit breaker state for an endpoint.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct CircuitBreaker {
88    pub endpoint: String,
89    pub state: CircuitState,
90    pub failure_count: u32,
91    pub failure_threshold: u32,
92    pub last_failure: Option<DateTime<Utc>>,
93    pub reset_after_secs: u64,
94    pub half_open_at: Option<DateTime<Utc>>,
95}
96
97/// Circuit breaker FSM states.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum CircuitState {
101    Closed,
102    Open,
103    HalfOpen,
104}
105
106impl CircuitBreaker {
107    pub fn new(endpoint: &str, threshold: u32, reset_secs: u64) -> Self {
108        Self {
109            endpoint: endpoint.to_string(),
110            state: CircuitState::Closed,
111            failure_count: 0,
112            failure_threshold: threshold,
113            last_failure: None,
114            reset_after_secs: reset_secs,
115            half_open_at: None,
116        }
117    }
118
119    pub fn record_failure(&mut self) {
120        self.failure_count += 1;
121        self.last_failure = Some(Utc::now());
122        if self.failure_count >= self.failure_threshold {
123            self.state = CircuitState::Open;
124            self.half_open_at = Some(
125                Utc::now() + chrono::Duration::seconds(self.reset_after_secs as i64),
126            );
127        }
128    }
129
130    pub fn record_success(&mut self) {
131        self.failure_count = 0;
132        self.state = CircuitState::Closed;
133        self.half_open_at = None;
134    }
135
136    pub fn should_allow(&self) -> bool {
137        match self.state {
138            CircuitState::Closed => true,
139            CircuitState::Open => {
140                self.half_open_at.map_or(false, |t| Utc::now() >= t)
141            }
142            CircuitState::HalfOpen => true,
143        }
144    }
145}
146
147/// Rate limit tracking for an endpoint.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct RateLimitWindow {
150    pub endpoint: String,
151    pub limit: u32,
152    pub remaining: u32,
153    pub resets_at: DateTime<Utc>,
154    pub window_secs: u64,
155}