msy 0.4.6

Modern musl rsync alternative - Fast, parallel file synchronization
Documentation
use crate::error::SyncError;
use std::future::Future;
use std::time::Duration;

/// Configuration for retry logic with exponential backoff
#[derive(Debug, Clone)]
pub struct RetryConfig {
	/// Maximum number of retry attempts
	pub max_attempts: u32,
	/// Initial delay before first retry
	pub initial_delay: Duration,
	/// Maximum delay between retries
	pub max_delay: Duration,
	/// Backoff multiplier for exponential growth
	pub backoff_multiplier: f64,
}

impl Default for RetryConfig {
	fn default() -> Self {
		Self { max_attempts: 3, initial_delay: Duration::from_secs(1), max_delay: Duration::from_secs(30), backoff_multiplier: 2.0 }
	}
}

impl RetryConfig {
	/// Create a new RetryConfig with custom settings
	pub fn new(max_attempts: u32, initial_delay: Duration) -> Self {
		Self { max_attempts, initial_delay, ..Default::default() }
	}

	/// Set the maximum delay between retries
	#[allow(dead_code)] // Configuration builder - may be used for custom retry strategies
	pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
		self.max_delay = max_delay;
		self
	}

	/// Set the backoff multiplier
	#[allow(dead_code)] // Configuration builder - may be used for custom retry strategies
	pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
		self.backoff_multiplier = multiplier;
		self
	}

	/// Calculate delay for a given attempt number (0-indexed)
	fn calculate_delay(&self, attempt: u32) -> Duration {
		let delay_secs = self.initial_delay.as_secs_f64() * self.backoff_multiplier.powi(attempt as i32);
		let delay = Duration::from_secs_f64(delay_secs);
		std::cmp::min(delay, self.max_delay)
	}
}

/// Retry an async operation with exponential backoff
///
/// This function will retry the operation up to `config.max_attempts` times
/// if it fails with a retryable error. The delay between retries increases
/// exponentially according to the backoff configuration.
///
/// # Example
///
/// ```ignore
/// let config = RetryConfig::default();
/// let result = retry_with_backoff(&config, || async {
///     // Your async operation here
///     Ok(())
/// }).await?;
/// ```
pub async fn retry_with_backoff<F, Fut, T>(config: &RetryConfig, mut operation: F) -> Result<T, SyncError>
where
	F: FnMut() -> Fut,
	Fut: Future<Output = Result<T, SyncError>>,
{
	let mut attempt = 0;

	loop {
		match operation().await {
			Ok(result) => return Ok(result),
			Err(e) if attempt >= config.max_attempts => {
				return Err(e);
			}
			Err(e) if e.is_retryable() => {
				let delay = config.calculate_delay(attempt);

				eprintln!("Attempt {}/{} failed: {}. Retrying in {:?}...", attempt + 1, config.max_attempts, e, delay);

				tokio::time::sleep(delay).await;
				attempt += 1;
			}
			Err(e) => {
				// Non-retryable error, fail immediately
				return Err(e);
			}
		}
	}
}

/// Convenience wrapper for retrying with default config
#[allow(dead_code)] // Helper function - may be used for simple retry scenarios
pub async fn retry_default<F, Fut, T>(operation: F) -> Result<T, SyncError>
where
	F: FnMut() -> Fut,
	Fut: Future<Output = Result<T, SyncError>>,
{
	retry_with_backoff(&RetryConfig::default(), operation).await
}

#[cfg(test)]
mod tests {
	use super::*;
	use std::sync::Arc;
	use std::sync::atomic::{AtomicU32, Ordering};

	#[test]
	fn test_retry_config_default() {
		let config = RetryConfig::default();
		assert_eq!(config.max_attempts, 3);
		assert_eq!(config.initial_delay, Duration::from_secs(1));
		assert_eq!(config.max_delay, Duration::from_secs(30));
		assert_eq!(config.backoff_multiplier, 2.0);
	}

	#[test]
	fn test_retry_config_custom() {
		let config = RetryConfig::new(5, Duration::from_millis(500)).with_max_delay(Duration::from_secs(60)).with_backoff_multiplier(3.0);

		assert_eq!(config.max_attempts, 5);
		assert_eq!(config.initial_delay, Duration::from_millis(500));
		assert_eq!(config.max_delay, Duration::from_secs(60));
		assert_eq!(config.backoff_multiplier, 3.0);
	}

	#[test]
	fn test_calculate_delay_exponential() {
		let config = RetryConfig::default();

		// First retry: 1s * 2^0 = 1s
		assert_eq!(config.calculate_delay(0), Duration::from_secs(1));

		// Second retry: 1s * 2^1 = 2s
		assert_eq!(config.calculate_delay(1), Duration::from_secs(2));

		// Third retry: 1s * 2^2 = 4s
		assert_eq!(config.calculate_delay(2), Duration::from_secs(4));

		// Fourth retry: 1s * 2^3 = 8s
		assert_eq!(config.calculate_delay(3), Duration::from_secs(8));
	}

	#[test]
	fn test_calculate_delay_capped_at_max() {
		let config = RetryConfig { initial_delay: Duration::from_secs(10), max_delay: Duration::from_secs(15), backoff_multiplier: 2.0, max_attempts: 5 };

		// First retry: 10s * 2^0 = 10s (under max)
		assert_eq!(config.calculate_delay(0), Duration::from_secs(10));

		// Second retry: 10s * 2^1 = 20s, capped at 15s
		assert_eq!(config.calculate_delay(1), Duration::from_secs(15));

		// Third retry: 10s * 2^2 = 40s, capped at 15s
		assert_eq!(config.calculate_delay(2), Duration::from_secs(15));
	}

	#[tokio::test]
	async fn test_retry_success_first_attempt() {
		let counter = Arc::new(AtomicU32::new(0));
		let counter_clone = counter.clone();

		let config = RetryConfig::default();
		let result = retry_with_backoff(&config, || {
			let c = counter_clone.clone();
			async move {
				c.fetch_add(1, Ordering::SeqCst);
				Ok::<_, SyncError>(42)
			}
		})
		.await;

		assert_eq!(result.unwrap(), 42);
		assert_eq!(counter.load(Ordering::SeqCst), 1);
	}

	#[tokio::test]
	async fn test_retry_success_after_retries() {
		let counter = Arc::new(AtomicU32::new(0));
		let counter_clone = counter.clone();

		let config = RetryConfig::new(3, Duration::from_millis(10));
		let result = retry_with_backoff(&config, || {
			let c = counter_clone.clone();
			async move {
				let count = c.fetch_add(1, Ordering::SeqCst);
				if count < 2 {
					// Fail first 2 attempts with retryable error
					Err(SyncError::NetworkTimeout { duration: Duration::from_secs(1) })
				} else {
					Ok(42)
				}
			}
		})
		.await;

		assert_eq!(result.unwrap(), 42);
		assert_eq!(counter.load(Ordering::SeqCst), 3); // 2 failures + 1 success
	}

	#[tokio::test]
	async fn test_retry_exhausted() {
		let counter = Arc::new(AtomicU32::new(0));
		let counter_clone = counter.clone();

		let config = RetryConfig::new(2, Duration::from_millis(10));
		let result = retry_with_backoff(&config, || {
			let c = counter_clone.clone();
			async move {
				c.fetch_add(1, Ordering::SeqCst);
				Err::<i32, _>(SyncError::NetworkTimeout { duration: Duration::from_secs(1) })
			}
		})
		.await;

		assert!(result.is_err());
		assert!(matches!(result.unwrap_err(), SyncError::NetworkTimeout { .. }));
		// Initial attempt + 2 retries = 3 total
		assert_eq!(counter.load(Ordering::SeqCst), 3);
	}

	#[tokio::test]
	async fn test_retry_non_retryable_error() {
		let counter = Arc::new(AtomicU32::new(0));
		let counter_clone = counter.clone();

		let config = RetryConfig::default();
		let result = retry_with_backoff(&config, || {
			let c = counter_clone.clone();
			async move {
				c.fetch_add(1, Ordering::SeqCst);
				Err::<i32, _>(SyncError::NetworkFatal { message: "Fatal error".to_string() })
			}
		})
		.await;

		assert!(result.is_err());
		assert!(matches!(result.unwrap_err(), SyncError::NetworkFatal { .. }));
		// Should fail immediately without retries
		assert_eq!(counter.load(Ordering::SeqCst), 1);
	}

	#[tokio::test]
	async fn test_retry_default_wrapper() {
		let counter = Arc::new(AtomicU32::new(0));
		let counter_clone = counter.clone();

		let result = retry_default(|| {
			let c = counter_clone.clone();
			async move {
				c.fetch_add(1, Ordering::SeqCst);
				Ok::<_, SyncError>(100)
			}
		})
		.await;

		assert_eq!(result.unwrap(), 100);
		assert_eq!(counter.load(Ordering::SeqCst), 1);
	}
}