use std::sync::Arc;
use std::time::Instant;
use core::time::Duration;
use tokio::sync::RwLock;
use rand::RngExt;
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum FailoverStrategy
{
Priority,
RoundRobin,
Random,
Sticky,
}
#[ derive( Debug, Clone ) ]
pub struct FailoverConfig
{
pub endpoints : Vec< String >,
pub strategy : FailoverStrategy,
pub max_retries : u32,
pub failure_window : Duration,
pub failure_threshold : u32,
}
impl Default for FailoverConfig
{
#[ inline ]
fn default() -> Self
{
Self {
endpoints : vec![ ],
strategy : FailoverStrategy::Priority,
max_retries : 3,
failure_window : Duration::from_secs( 60 ),
failure_threshold : 5,
}
}
}
#[ derive( Debug, Clone ) ]
struct EndpointHealth
{
url : String,
failures : Vec< Instant >,
requests : u64,
successes : u64,
last_success : Option< Instant >,
healthy : bool,
}
impl EndpointHealth
{
#[ inline ]
fn new( url : String ) -> Self
{
Self {
url,
failures : Vec::new( ),
requests : 0,
successes : 0,
last_success : None,
healthy : true,
}
}
#[ inline ]
fn record_success( &mut self )
{
self.requests += 1;
self.successes += 1;
self.last_success = Some( Instant::now( ));
self.healthy = true;
}
#[ inline ]
fn record_failure( &mut self, failure_window : Duration, failure_threshold : u32 )
{
self.requests += 1;
let now = Instant::now( );
self.failures.push( now );
self.failures.retain( |&t| now.duration_since( t ) < failure_window );
if self.failures.len( ) >= failure_threshold as usize
{
self.healthy = false;
}
}
#[ inline ]
fn failure_count( &mut self, failure_window : Duration ) -> usize
{
let now = Instant::now( );
self.failures.retain( |&t| now.duration_since( t ) < failure_window );
self.failures.len( )
}
#[ inline ]
fn success_rate( &self ) -> f64
{
if self.requests == 0
{
1.0
} else {
self.successes as f64 / self.requests as f64
}
}
}
#[ derive( Debug ) ]
struct FailoverState
{
endpoints : Vec< EndpointHealth >,
round_robin_index : usize,
sticky_index : Option< usize >,
}
#[ derive( Debug, Clone ) ]
pub struct FailoverManager
{
config : FailoverConfig,
state : Arc< RwLock< FailoverState > >,
}
impl FailoverManager
{
#[ inline ]
pub fn new( config : FailoverConfig ) -> Result< Self, FailoverError >
{
if config.endpoints.is_empty( )
{
return Err( FailoverError::NoEndpoints );
}
let endpoints = config.endpoints.iter( )
.map( |url| EndpointHealth::new( url.clone( )) )
.collect( );
Ok( Self {
config,
state : Arc::new( RwLock::new( FailoverState {
endpoints,
round_robin_index : 0,
sticky_index : None,
} )),
} )
}
#[ inline ]
pub async fn select_endpoint( &self ) -> Result< String, FailoverError >
{
let mut state = self.state.write( ).await;
match self.config.strategy
{
FailoverStrategy::Priority => {
for endpoint in &state.endpoints
{
if endpoint.healthy
{
return Ok( endpoint.url.clone( ));
}
}
state.endpoints.first( )
.map( |e| e.url.clone( ))
.ok_or( FailoverError::NoEndpoints )
}
FailoverStrategy::RoundRobin => {
let healthy_count = state.endpoints.iter( ).filter( |e| e.healthy ).count( );
if healthy_count == 0
{
return Err( FailoverError::AllEndpointsUnhealthy );
}
let start_index = state.round_robin_index;
loop
{
let current_index = state.round_robin_index;
let endpoint_url = state.endpoints[current_index ].url.clone( );
let endpoint_healthy = state.endpoints[current_index ].healthy;
state.round_robin_index = ( state.round_robin_index + 1 ) % state.endpoints.len( );
if endpoint_healthy
{
return Ok( endpoint_url );
}
if state.round_robin_index == start_index
{
return Err( FailoverError::AllEndpointsUnhealthy );
}
}
}
FailoverStrategy::Random => {
let healthy : Vec< _ > = state.endpoints.iter( )
.filter( |e| e.healthy )
.collect( );
if healthy.is_empty( )
{
return Err( FailoverError::AllEndpointsUnhealthy );
}
let mut rng = rand::rng( );
let index = rng.random_range( 0..healthy.len( ));
Ok( healthy[index ].url.clone( ))
}
FailoverStrategy::Sticky => {
if let Some( idx ) = state.sticky_index
{
if idx < state.endpoints.len( ) && state.endpoints[idx ].healthy
{
return Ok( state.endpoints[idx ].url.clone( ));
}
}
for i in 0..state.endpoints.len( )
{
if state.endpoints[i ].healthy
{
let url = state.endpoints[i ].url.clone( );
state.sticky_index = Some( i );
return Ok( url );
}
}
Err( FailoverError::AllEndpointsUnhealthy )
}
}
}
#[ inline ]
pub async fn execute_with_failover< F, T, E >( &self, mut f : F ) -> Result< T, FailoverError< E > >
where
F : FnMut( String ) -> core::pin::Pin< Box< dyn core::future::Future< Output = Result< T, E > > + Send > >,
E: core::fmt::Display,
{
let mut attempts = 0;
let mut last_error = None;
while attempts <= self.config.max_retries
{
attempts += 1;
let endpoint = match self.select_endpoint( ).await
{
Ok( ep ) => ep,
Err( e ) => return Err( FailoverError::SelectionFailed( e.to_string( )) ),
};
let endpoint_clone = endpoint.clone( );
match f( endpoint.clone( )).await
{
Ok( result ) => {
self.record_success( &endpoint_clone ).await;
return Ok( result );
}
Err( e ) => {
self.record_failure( &endpoint_clone ).await;
last_error = Some( e );
if attempts <= self.config.max_retries
{
let exp = ( attempts - 1 ).min( 13 );
let delay_ms = 500 * 2u64.pow( exp );
tokio::time::sleep( Duration::from_millis( delay_ms.min( 5000 ) ) ).await;
}
}
}
}
if let Some( err ) = last_error
{
Err( FailoverError::AllRetriesFailed {
attempts,
last_error : err.to_string( ),
} )
} else {
Err( FailoverError::AllEndpointsUnhealthy )
}
}
#[ inline ]
pub async fn record_success( &self, endpoint : &str )
{
let mut state = self.state.write( ).await;
if let Some( ep ) = state.endpoints.iter_mut( ).find( |e| e.url == endpoint )
{
ep.record_success( );
}
}
#[ inline ]
pub async fn record_failure( &self, endpoint : &str )
{
let mut state = self.state.write( ).await;
if let Some( ep ) = state.endpoints.iter_mut( ).find( |e| e.url == endpoint )
{
ep.record_failure( self.config.failure_window, self.config.failure_threshold );
}
}
#[ inline ]
pub async fn health_status( &self ) -> Vec< EndpointHealthStatus >
{
let mut state = self.state.write( ).await;
state.endpoints.iter_mut( ).map( |ep| {
EndpointHealthStatus {
url : ep.url.clone( ),
healthy : ep.healthy,
requests : ep.requests,
successes : ep.successes,
success_rate : ep.success_rate( ),
failure_count : ep.failure_count( self.config.failure_window ),
}
} ).collect( )
}
#[ inline ]
pub async fn reset( &self )
{
let mut state = self.state.write( ).await;
for endpoint in &mut state.endpoints
{
endpoint.failures.clear( );
endpoint.requests = 0;
endpoint.successes = 0;
endpoint.last_success = None;
endpoint.healthy = true;
}
state.round_robin_index = 0;
state.sticky_index = None;
}
}
#[ derive( Debug, Clone, PartialEq ) ]
pub struct EndpointHealthStatus
{
pub url : String,
pub healthy : bool,
pub requests : u64,
pub successes : u64,
pub success_rate : f64,
pub failure_count : usize,
}
#[ derive( Debug ) ]
pub enum FailoverError< E = String >
{
NoEndpoints,
AllEndpointsUnhealthy,
SelectionFailed( String ),
AllRetriesFailed {
attempts : u32,
last_error : String,
},
Operation( E ),
}
impl< E > core::fmt::Display for FailoverError< E >
where
E: core::fmt::Display,
{
#[ inline ]
fn fmt( &self, f : &mut core::fmt::Formatter< '_ > ) -> core::fmt::Result
{
match self
{
Self::NoEndpoints => write!( f, "No endpoints configured" ),
Self::AllEndpointsUnhealthy => write!( f, "All endpoints are unhealthy" ),
Self::SelectionFailed( msg ) => write!( f, "Endpoint selection failed : {msg}" ),
Self::AllRetriesFailed { attempts, last_error } => {
write!( f, "All {attempts} retry attempts failed, last error : {last_error}" )
}
Self::Operation( e ) => write!( f, "Operation failed : {e}" ),
}
}
}
impl< E > std::error::Error for FailoverError< E >
where
E: std::error::Error + 'static,
{
#[ inline ]
fn source( &self ) -> Option< &( dyn std::error::Error + 'static ) >
{
match self
{
Self::Operation( e ) => Some( e ),
_ => None,
}
}
}