mockforge_grpc/reflection/
error_handling.rs

1//! Error handling and retry mechanisms for the reflection proxy
2
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5use tokio::time::sleep;
6use tonic::Status;
7use tracing::{debug, error, warn};
8
9/// Configuration for error handling and retries
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ErrorConfig {
12    /// Maximum number of retries for failed requests
13    pub max_retries: u32,
14    /// Base delay between retries (in milliseconds)
15    pub base_delay_ms: u64,
16    /// Maximum delay between retries (in milliseconds)
17    pub max_delay_ms: u64,
18    /// Whether to enable exponential backoff
19    pub exponential_backoff: bool,
20}
21
22impl Default for ErrorConfig {
23    fn default() -> Self {
24        Self {
25            max_retries: 3,
26            base_delay_ms: 100,
27            max_delay_ms: 5000,
28            exponential_backoff: true,
29        }
30    }
31}
32
33/// Handle errors with retry logic
34pub async fn handle_with_retry<F, Fut, T>(
35    mut operation: F,
36    config: &ErrorConfig,
37) -> Result<T, Status>
38where
39    F: FnMut() -> Fut,
40    Fut: std::future::Future<Output = Result<T, Status>>,
41{
42    let mut attempts = 0;
43    let mut delay = Duration::from_millis(config.base_delay_ms);
44
45    loop {
46        match operation().await {
47            Ok(result) => return Ok(result),
48            Err(status) => {
49                attempts += 1;
50
51                // If we've reached the maximum retries, return the error
52                if attempts > config.max_retries {
53                    error!("Operation failed after {} attempts: {}", attempts, status);
54                    return Err(status);
55                }
56
57                // For certain types of errors, we might not want to retry
58                match status.code() {
59                    // Don't retry these errors
60                    tonic::Code::InvalidArgument
61                    | tonic::Code::NotFound
62                    | tonic::Code::AlreadyExists
63                    | tonic::Code::PermissionDenied
64                    | tonic::Code::FailedPrecondition
65                    | tonic::Code::Aborted
66                    | tonic::Code::OutOfRange
67                    | tonic::Code::Unimplemented => {
68                        error!("Non-retryable error: {}", status);
69                        return Err(status);
70                    }
71                    // Retry all other errors
72                    _ => {
73                        warn!(
74                            "Attempt {} failed: {}. Retrying in {:?}...",
75                            attempts, status, delay
76                        );
77
78                        // Wait before retrying
79                        sleep(delay).await;
80
81                        // Calculate next delay with exponential backoff
82                        if config.exponential_backoff {
83                            delay = Duration::from_millis(
84                                (delay.as_millis() * 2).min(config.max_delay_ms as u128) as u64,
85                            );
86                        }
87                    }
88                }
89            }
90        }
91    }
92}
93
94/// Simulate various error conditions for testing
95pub fn simulate_error(error_rate: f64) -> Result<(), Status> {
96    use rand::Rng;
97
98    let mut rng = rand::rng();
99    let random: f64 = rng.random();
100
101    if random < error_rate {
102        // Simulate different types of errors
103        let error_type: u32 = rng.random_range(0..5);
104        match error_type {
105            0 => Err(Status::unavailable("Simulated service unavailable")),
106            1 => Err(Status::deadline_exceeded("Simulated timeout")),
107            2 => Err(Status::internal("Simulated internal error")),
108            3 => Err(Status::resource_exhausted("Simulated resource exhausted")),
109            _ => Err(Status::unknown("Simulated unknown error")),
110        }
111    } else {
112        Ok(())
113    }
114}
115
116/// Add latency to simulate network delays
117pub async fn simulate_latency(latency_ms: u64) {
118    if latency_ms > 0 {
119        debug!("Simulating {}ms latency", latency_ms);
120        sleep(Duration::from_millis(latency_ms)).await;
121    }
122}
123
124#[cfg(test)]
125mod tests {
126
127    #[test]
128    fn test_module_compiles() {}
129}