#[ cfg( feature = "retry-logic" ) ]
#[ allow( clippy::missing_inline_in_public_items ) ]
mod private
{
#[ cfg( feature = "error-handling" ) ]
use crate::error::{AnthropicError, AnthropicResult, RateLimitError};
#[ cfg( not( feature = "error-handling" ) ) ]
use error_tools::{err, Result as AnthropicResult};
#[ cfg( not( feature = "error-handling" ) ) ]
type AnthropicError = error_tools::Error;
#[ cfg( not( feature = "error-handling" ) ) ]
#[ derive( Debug, Clone ) ]
pub struct RateLimitError
{
message : String,
retry_after : Option< u64 >,
limit_type : String,
}
#[ cfg( not( feature = "error-handling" ) ) ]
impl RateLimitError
{
pub fn new(limit_type : String, retry_after : Option< u64 >, message : String) -> Self
{
Self { message, retry_after, limit_type }
}
pub fn retry_after(&self) -> Option< u64 >
{
self.retry_after
}
pub fn limit_type(&self) -> &str
{
&self.limit_type
}
}
#[ cfg( not( feature = "error-handling" ) ) ]
pub struct BackoffCalculator;
#[ cfg( not( feature = "error-handling" ) ) ]
impl BackoffCalculator
{
pub fn calculate_backoff(_error : &RateLimitError) -> AnthropicResult< BackoffStrategyDetails >
{
Err(err!("BackoffCalculator not available without error-handling feature"))
}
}
use core::time::Duration;
use std::collections::HashMap;
#[ derive( Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize ) ]
pub struct RetryConfig
{
max_attempts : u32,
base_delay_ms : u64,
max_delay_ms : u64,
backoff_multiplier : f64,
jitter_enabled : bool,
}
impl Default for RetryConfig
{
fn default() -> Self
{
Self::new()
}
}
impl RetryConfig
{
pub fn with_explicit_config( max_attempts : u32, base_delay_ms : u64, max_delay_ms : u64, backoff_multiplier : f64, jitter_enabled : bool ) -> Self
{
Self {
max_attempts,
base_delay_ms,
max_delay_ms,
backoff_multiplier,
jitter_enabled,
}
}
}
impl RetryConfig
{
pub fn new() -> Self
{
Self::with_explicit_config(
3, 1000, 60000, 2.0, true, )
}
#[ must_use ]
pub fn with_max_attempts( mut self, max_attempts : u32 ) -> Self
{
self.max_attempts = max_attempts;
self
}
#[ must_use ]
pub fn with_base_delay_ms( mut self, base_delay_ms : u64 ) -> Self
{
self.base_delay_ms = base_delay_ms;
self
}
#[ must_use ]
pub fn with_max_delay_ms( mut self, max_delay_ms : u64 ) -> Self
{
self.max_delay_ms = max_delay_ms;
self
}
#[ must_use ]
#[ allow( clippy::cast_possible_truncation ) ]
pub fn with_initial_delay( self, delay : Duration ) -> Self
{
self.with_base_delay_ms( delay.as_millis() as u64 )
}
#[ must_use ]
#[ allow( clippy::cast_possible_truncation ) ]
pub fn with_max_delay( self, delay : Duration ) -> Self
{
self.with_max_delay_ms( delay.as_millis() as u64 )
}
#[ must_use ]
pub fn with_backoff_multiplier( mut self, backoff_multiplier : f64 ) -> Self
{
self.backoff_multiplier = backoff_multiplier;
self
}
#[ must_use ]
pub fn with_jitter( mut self, jitter_enabled : bool ) -> Self
{
self.jitter_enabled = jitter_enabled;
self
}
pub fn max_attempts( &self ) -> u32
{
self.max_attempts
}
pub fn base_delay_ms( &self ) -> u64
{
self.base_delay_ms
}
#[ must_use ]
pub fn initial_delay( &self ) -> Duration
{
Duration::from_millis( self.base_delay_ms )
}
pub fn max_delay_ms( &self ) -> u64
{
self.max_delay_ms
}
pub fn backoff_multiplier( &self ) -> f64
{
self.backoff_multiplier
}
pub fn jitter_enabled( &self ) -> bool
{
self.jitter_enabled
}
pub fn validate( &self ) -> AnthropicResult< () >
{
if self.max_attempts == 0
{
return Err( AnthropicError::InvalidArgument( "max_attempts must be >= 1".to_string() ) );
}
if self.base_delay_ms == 0
{
return Err( AnthropicError::InvalidArgument( "base_delay_ms must be > 0".to_string() ) );
}
if self.max_delay_ms < self.base_delay_ms
{
return Err( AnthropicError::InvalidArgument( "max_delay_ms must be >= base_delay_ms".to_string() ) );
}
if self.backoff_multiplier < 1.0
{
return Err( AnthropicError::InvalidArgument( "backoff_multiplier must be >= 1.0".to_string() ) );
}
Ok(())
}
#[ must_use ]
pub fn is_valid( &self ) -> bool
{
self.validate().is_ok()
}
}
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum RetryStrategyType
{
ExponentialBackoff,
LinearBackoff,
FixedDelay,
}
#[ derive( Debug, Clone ) ]
pub struct RetryStrategy
{
strategy_type : RetryStrategyType,
config : RetryConfig,
}
impl RetryStrategy
{
pub fn exponential_backoff_with_config( config : RetryConfig ) -> Self
{
Self {
strategy_type : RetryStrategyType::ExponentialBackoff,
config,
}
}
pub fn linear_backoff_with_config( config : RetryConfig ) -> Self
{
Self {
strategy_type : RetryStrategyType::LinearBackoff,
config,
}
}
pub fn fixed_delay_with_config( config : RetryConfig ) -> Self
{
Self {
strategy_type : RetryStrategyType::FixedDelay,
config,
}
}
pub fn exponential_backoff() -> Self
{
Self::exponential_backoff_with_config( RetryConfig::new() )
}
pub fn linear_backoff() -> Self
{
Self::linear_backoff_with_config( RetryConfig::new() )
}
pub fn fixed_delay() -> Self
{
Self::fixed_delay_with_config( RetryConfig::new() )
}
#[ must_use ]
pub fn with_config( mut self, config : RetryConfig ) -> Self
{
self.config = config;
self
}
pub fn strategy_type( &self ) -> RetryStrategyType
{
self.strategy_type
}
pub fn config( &self ) -> &RetryConfig
{
&self.config
}
pub fn should_retry( &self, error : &AnthropicError, attempt : u32 ) -> bool
{
if attempt >= self.config.max_attempts
{
return false;
}
#[ cfg( feature = "error-handling" ) ]
{
match error
{
AnthropicError::Http( http_error ) =>
{
if let Some( status_code ) = http_error.status_code()
{
(500..600).contains(&status_code)
}
else
{
let message = http_error.message().to_lowercase();
message.contains("timeout") || message.contains("connection") || message.contains("failed") || message.contains("temporary")
}
},
AnthropicError::RateLimit( _ ) | AnthropicError::Stream( _ ) | AnthropicError::Internal( _ ) => true,
_ => false,
}
}
#[ cfg( not( feature = "error-handling" ) ) ]
{
let error_msg = error.to_string().to_lowercase();
error_msg.contains("timeout") ||
error_msg.contains("network") ||
error_msg.contains("rate limit") ||
error_msg.contains("5") }
}
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
pub fn calculate_delay_with_jitter_config( &self, attempt : u32, jitter_min_factor : Option< f64 >, jitter_max_factor : Option< f64 > ) -> u64
{
let base_delay = self.config.base_delay_ms;
let calculated_delay = match self.strategy_type
{
RetryStrategyType::ExponentialBackoff =>
{
let exponent = f64::from(attempt - 1);
let delay = base_delay as f64 * self.config.backoff_multiplier.powf( exponent );
delay as u64
},
RetryStrategyType::LinearBackoff =>
{
base_delay * u64::from(attempt)
},
RetryStrategyType::FixedDelay =>
{
base_delay
},
};
let capped_delay = calculated_delay.min( self.config.max_delay_ms );
if self.config.jitter_enabled
{
if let ( Some( min_factor ), Some( max_factor ) ) = ( jitter_min_factor, jitter_max_factor )
{
Self::apply_jitter_with_config( capped_delay, min_factor, max_factor )
}
else
{
capped_delay
}
}
else
{
capped_delay
}
}
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
pub fn calculate_delay( &self, attempt : u32 ) -> u64
{
if self.config.jitter_enabled
{
self.calculate_delay_with_jitter_config( attempt, Some( 0.9 ), Some( 1.1 ) )
}
else
{
self.calculate_delay_with_jitter_config( attempt, None, None )
}
}
pub fn calculate_delay_for_error_with_config(
&self,
error : &RateLimitError,
attempt : u32,
jitter_min_factor : Option< f64 >,
jitter_max_factor : Option< f64 >,
) -> u64
{
let base_delay = self.calculate_delay_with_jitter_config( attempt, jitter_min_factor, jitter_max_factor );
if let Some( retry_after ) = error.retry_after()
{
let retry_after_ms = *retry_after * 1000;
base_delay.max( retry_after_ms )
}
else
{
base_delay
}
}
pub fn calculate_delay_for_error( &self, error : &RateLimitError, attempt : u32 ) -> u64
{
self.calculate_delay_for_error_with_config( error, attempt, None, None )
}
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
fn apply_jitter_with_config( delay : u64, jitter_min_factor : f64, jitter_max_factor : f64 ) -> u64
{
use core::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
std::time::SystemTime::now().duration_since( std::time::UNIX_EPOCH )
.unwrap_or_default().as_nanos().hash( &mut hasher );
let hash = hasher.finish();
let factor_range = jitter_max_factor - jitter_min_factor;
let jitter_factor = jitter_min_factor + ( hash % 1000 ) as f64 / 1000.0 * factor_range;
( delay as f64 * jitter_factor ) as u64
}
#[ deprecated(note = "Use apply_jitter_with_config() for explicit jitter configuration") ]
#[ allow(dead_code) ]
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
fn apply_jitter( _delay : u64 ) -> u64
{
panic!("apply_jitter() is deprecated. Use apply_jitter_with_config() for explicit jitter configuration")
}
}
#[ derive( Debug ) ]
pub struct RetryExecutor
{
strategy : RetryStrategy,
current_attempt : u32,
}
impl RetryExecutor
{
pub fn new( strategy : RetryStrategy ) -> Self
{
Self {
strategy,
current_attempt : 0,
}
}
pub fn strategy( &self ) -> &RetryStrategy
{
&self.strategy
}
pub fn current_attempt( &self ) -> u32
{
self.current_attempt
}
pub fn has_exceeded_max_attempts( &self ) -> bool
{
self.current_attempt >= self.strategy.config.max_attempts
}
pub async fn execute< F, Fut, T >( &self, operation : F ) -> AnthropicResult< T >
where
F: Fn() -> Fut,
Fut : core::future::Future< Output = AnthropicResult< T > >,
{
let mut attempt = 1;
loop
{
match operation().await
{
Ok( result ) => return Ok( result ),
Err( error ) =>
{
if !self.strategy.should_retry( &error, attempt )
{
return Err( error );
}
let delay_ms = {
#[ cfg( feature = "error-handling" ) ]
{
match &error
{
AnthropicError::RateLimit( rate_limit_error ) =>
{
self.strategy.calculate_delay_for_error( rate_limit_error, attempt )
},
_ => self.strategy.calculate_delay( attempt ),
}
}
#[ cfg( not( feature = "error-handling" ) ) ]
{
self.strategy.calculate_delay( attempt )
}
};
tokio::time::sleep( Duration::from_millis( delay_ms ) ).await;
attempt += 1;
}
}
}
}
}
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum BackoffStrategyType
{
ExponentialBackoff,
LinearBackoff,
FixedDelay,
}
#[ derive( Debug, Clone ) ]
pub struct BackoffStrategyDetails
{
strategy_type : BackoffStrategyType,
suggested_delay_ms : u64,
jitter_enabled : bool,
}
impl BackoffStrategyDetails
{
pub fn new( strategy_type : BackoffStrategyType, suggested_delay_ms : u64, jitter_enabled : bool ) -> Self
{
Self {
strategy_type,
suggested_delay_ms,
jitter_enabled,
}
}
pub fn strategy_type( &self ) -> BackoffStrategyType
{
self.strategy_type
}
pub fn suggested_delay_ms( &self ) -> u64
{
self.suggested_delay_ms
}
pub fn jitter_enabled( &self ) -> bool
{
self.jitter_enabled
}
}
#[ derive( Debug, Clone ) ]
pub struct RetryMetrics
{
total_attempts : u64,
successful_retries : u64,
failed_attempts : u64,
total_delay_ms : u64,
error_counts : HashMap< String, u64 >,
}
impl Default for RetryMetrics
{
fn default() -> Self
{
Self::new()
}
}
impl RetryMetrics
{
pub fn new() -> Self
{
Self {
total_attempts : 0,
successful_retries : 0,
failed_attempts : 0,
total_delay_ms : 0,
error_counts : HashMap::new(),
}
}
pub fn record_attempt( &mut self, attempt : u32, delay_ms : u64 )
{
self.total_attempts += 1;
self.total_delay_ms += delay_ms;
if attempt > 1
{
}
}
pub fn record_success( &mut self, final_attempt : u32 )
{
if final_attempt > 1
{
self.successful_retries += 1;
}
}
pub fn record_failure( &mut self, error : &AnthropicError )
{
self.failed_attempts += 1;
let error_type = {
#[ cfg( feature = "error-handling" ) ]
{
match error
{
AnthropicError::RateLimit( _ ) => "rate_limit",
AnthropicError::Http( _ ) => "http",
AnthropicError::Stream( _ ) => "stream",
AnthropicError::Internal( _ ) => "internal",
AnthropicError::Authentication( _ ) => "authentication",
AnthropicError::InvalidArgument( _ ) => "invalid_argument",
AnthropicError::InvalidRequest( _ ) => "invalid_request",
AnthropicError::Api( _ ) => "api",
AnthropicError::File( _ ) => "file",
AnthropicError::Parsing( _ ) => "parsing",
AnthropicError::MissingEnvironment( _ ) => "missing_environment",
AnthropicError::NotImplemented( _ ) => "not_implemented",
_ => "other",
}
}
#[ cfg( not( feature = "error-handling" ) ) ]
{
let error_msg = error.to_string().to_lowercase();
if error_msg.contains("rate limit") { "rate_limit" }
else if error_msg.contains("timeout") { "timeout" }
else if error_msg.contains("network") { "network" }
else if error_msg.contains("http") { "http" }
else if error_msg.contains("auth") { "authentication" }
else if error_msg.contains("invalid") { "invalid_argument" }
else { "other" }
}
};
*self.error_counts.entry( error_type.to_string() ).or_insert( 0 ) += 1;
}
pub fn total_attempts( &self ) -> u64
{
self.total_attempts
}
pub fn successful_retries( &self ) -> u64
{
self.successful_retries
}
pub fn failed_attempts( &self ) -> u64
{
self.failed_attempts
}
pub fn total_delay_ms( &self ) -> u64
{
self.total_delay_ms
}
pub fn average_delay_ms( &self ) -> f64
{
if self.total_attempts == 0
{
0.0
}
else
{
self.total_delay_ms as f64 / self.total_attempts as f64
}
}
pub fn reset( &mut self )
{
self.total_attempts = 0;
self.successful_retries = 0;
self.failed_attempts = 0;
self.total_delay_ms = 0;
self.error_counts.clear();
}
}
}
#[ cfg( feature = "retry-logic" ) ]
crate::mod_interface!
{
exposed use RetryConfig;
exposed use RetryStrategy;
exposed use RetryStrategyType;
exposed use RetryExecutor;
exposed use BackoffStrategyType;
exposed use BackoffStrategyDetails;
exposed use RetryMetrics;
}