Skip to main content

fraiseql_auth/oauth/
failover.rs

1//! Multi-provider failover management.
2
3use std::sync::Arc;
4
5use chrono::{DateTime, Duration, Utc};
6
7use super::super::error::AuthError;
8
9/// Multi-provider failover manager
10#[derive(Debug, Clone)]
11pub struct ProviderFailoverManager {
12    /// Primary provider name
13    primary_provider:   String,
14    /// Fallback providers in priority order
15    fallback_providers: Vec<String>,
16    /// Providers currently unavailable
17    // std::sync::Mutex is intentional: this lock is never held across .await.
18    // Switch to tokio::sync::Mutex if that constraint ever changes.
19    unavailable: Arc<std::sync::Mutex<Vec<(String, DateTime<Utc>)>>>,
20}
21
22impl ProviderFailoverManager {
23    /// Create new failover manager
24    #[must_use]
25    pub fn new(primary: String, fallbacks: Vec<String>) -> Self {
26        Self {
27            primary_provider:   primary,
28            fallback_providers: fallbacks,
29            unavailable:        Arc::new(std::sync::Mutex::new(Vec::new())),
30        }
31    }
32
33    /// Get next available provider
34    ///
35    /// # Errors
36    ///
37    /// Returns `AuthError::Internal` if the mutex is poisoned or no providers are available.
38    pub fn get_available_provider(&self) -> std::result::Result<String, AuthError> {
39        let unavailable = self.unavailable.lock().map_err(|_| AuthError::Internal {
40            message: "failover manager mutex poisoned".to_string(),
41        })?;
42        let now = Utc::now();
43
44        // Check if primary is available
45        if !unavailable
46            .iter()
47            .any(|(name, exp)| name == &self.primary_provider && *exp > now)
48        {
49            return Ok(self.primary_provider.clone());
50        }
51
52        // Find first available fallback
53        for fallback in &self.fallback_providers {
54            if !unavailable.iter().any(|(name, exp)| name == fallback && *exp > now) {
55                return Ok(fallback.clone());
56            }
57        }
58
59        Err(AuthError::Internal {
60            message: "no OAuth providers available".to_string(),
61        })
62    }
63
64    /// Mark provider as unavailable
65    ///
66    /// # Errors
67    ///
68    /// Returns `AuthError::Internal` if the mutex is poisoned.
69    pub fn mark_unavailable(
70        &self,
71        provider: String,
72        duration_seconds: u64,
73    ) -> std::result::Result<(), AuthError> {
74        let mut unavailable = self.unavailable.lock().map_err(|_| AuthError::Internal {
75            message: "failover manager mutex poisoned".to_string(),
76        })?;
77        unavailable
78            .push((provider, Utc::now() + Duration::seconds(duration_seconds.cast_signed())));
79        Ok(())
80    }
81
82    /// Mark provider as available
83    ///
84    /// # Errors
85    ///
86    /// Returns `AuthError::Internal` if the mutex is poisoned.
87    pub fn mark_available(&self, provider: &str) -> std::result::Result<(), AuthError> {
88        let mut unavailable = self.unavailable.lock().map_err(|_| AuthError::Internal {
89            message: "failover manager mutex poisoned".to_string(),
90        })?;
91        unavailable.retain(|(name, _)| name != provider);
92        Ok(())
93    }
94}