use anyhow::{anyhow, Result};
use std::sync::Arc;
pub enum FallbackStrategy<T> {
DefaultValue(T),
Alternative(Arc<dyn Fn() -> Result<T> + Send + Sync>),
Cache {
get_cached: Arc<dyn Fn() -> Option<T> + Send + Sync>,
},
Degraded {
operation: Arc<dyn Fn() -> Result<T> + Send + Sync>,
},
Silent,
}
pub struct FallbackChain {
strategies: Vec<Box<dyn Fn() -> Result<String> + Send + Sync>>,
}
#[allow(clippy::derivable_impls)]
impl Default for FallbackChain {
fn default() -> Self {
Self {
strategies: Vec::new(),
}
}
}
impl FallbackChain {
pub fn new() -> Self {
Self::default()
}
pub fn add<F>(&mut self, strategy: F)
where
F: Fn() -> Result<String> + Send + Sync + 'static,
{
self.strategies.push(Box::new(strategy));
}
pub fn execute(&self) -> Result<String> {
for strategy in &self.strategies {
if let Ok(result) = strategy() {
return Ok(result);
}
}
Err(anyhow!("All fallback strategies failed"))
}
}
pub async fn execute_with_fallback<T, F, Fb>(primary: F, fallback: Fb) -> Result<T>
where
F: FnOnce() -> Result<T>,
Fb: FnOnce() -> Result<T>,
{
match primary() {
Ok(result) => Ok(result),
Err(_) => fallback(),
}
}
pub struct GracefulDegradation<T> {
primary: Arc<dyn Fn() -> Result<T> + Send + Sync>,
fallbacks: Vec<Arc<dyn Fn() -> Result<T> + Send + Sync>>,
current_strategy: usize,
}
impl<T> GracefulDegradation<T> {
pub fn new<F>(primary: F) -> Self
where
F: Fn() -> Result<T> + Send + Sync + 'static,
{
Self {
primary: Arc::new(primary),
fallbacks: Vec::new(),
current_strategy: 0,
}
}
pub fn add_fallback<F>(&mut self, fallback: F)
where
F: Fn() -> Result<T> + Send + Sync + 'static,
{
self.fallbacks.push(Arc::new(fallback));
}
pub fn execute(&mut self) -> Result<T> {
if let Ok(result) = (self.primary)() {
self.current_strategy = 0;
return Ok(result);
}
for (idx, fallback) in self.fallbacks.iter().enumerate() {
if let Ok(result) = fallback() {
self.current_strategy = idx + 1;
return Ok(result);
}
}
Err(anyhow!("All strategies failed"))
}
pub fn current_strategy(&self) -> usize {
self.current_strategy
}
pub fn is_degraded(&self) -> bool {
self.current_strategy > 0
}
}
pub struct CachedFallback<T> {
cache: Arc<std::sync::RwLock<Option<CacheEntry<T>>>>,
ttl: std::time::Duration,
}
#[derive(Clone)]
struct CacheEntry<T> {
value: T,
timestamp: std::time::Instant,
}
impl<T: Clone> CachedFallback<T> {
pub fn new(ttl: std::time::Duration) -> Self {
Self {
cache: Arc::new(std::sync::RwLock::new(None)),
ttl,
}
}
pub fn update(&self, value: T) -> Result<()> {
let mut cache = self
.cache
.write()
.map_err(|e| anyhow!("Failed to acquire write lock: {}", e))?;
*cache = Some(CacheEntry {
value,
timestamp: std::time::Instant::now(),
});
Ok(())
}
pub fn get(&self) -> Result<Option<T>> {
let cache = self
.cache
.read()
.map_err(|e| anyhow!("Failed to acquire read lock: {}", e))?;
if let Some(entry) = cache.as_ref() {
if entry.timestamp.elapsed() < self.ttl {
return Ok(Some(entry.value.clone()));
}
}
Ok(None)
}
pub fn execute_with_cache<F>(&self, operation: F) -> Result<T>
where
F: FnOnce() -> Result<T>,
{
match operation() {
Ok(value) => {
self.update(value.clone())?;
Ok(value)
}
Err(_) => {
if let Some(cached) = self.get()? {
Ok(cached)
} else {
Err(anyhow!("No cached value available"))
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_fallback_success_primary() {
let result = execute_with_fallback(|| Ok::<_, anyhow::Error>(42), || Ok(0)).await;
assert_eq!(result.expect("should succeed"), 42);
}
#[tokio::test]
async fn test_fallback_uses_fallback() {
let result =
execute_with_fallback(|| Err::<i32, _>(anyhow!("Primary failed")), || Ok(99)).await;
assert_eq!(result.expect("should succeed"), 99);
}
#[test]
fn test_graceful_degradation() {
let mut degradation = GracefulDegradation::new(|| Err::<i32, _>(anyhow!("Primary fails")));
degradation.add_fallback(|| Err::<i32, _>(anyhow!("First fallback fails")));
degradation.add_fallback(|| Ok(42));
let result = degradation.execute();
assert!(result.is_ok());
assert_eq!(result.expect("should succeed"), 42);
assert!(degradation.is_degraded());
assert_eq!(degradation.current_strategy(), 2);
}
#[test]
fn test_cached_fallback_fresh() {
let cache = CachedFallback::new(std::time::Duration::from_secs(60));
let result = cache.execute_with_cache(|| Ok::<_, anyhow::Error>(42));
assert_eq!(result.expect("should succeed"), 42);
let cached = cache.get().expect("should succeed");
assert_eq!(cached, Some(42));
}
#[test]
fn test_cached_fallback_expired() {
let cache = CachedFallback::new(std::time::Duration::from_millis(10));
cache.update(42).expect("should succeed");
std::thread::sleep(std::time::Duration::from_millis(20));
let cached = cache.get().expect("should succeed");
assert_eq!(cached, None);
}
#[test]
fn test_cached_fallback_on_failure() {
let cache = CachedFallback::new(std::time::Duration::from_secs(60));
let result = cache.execute_with_cache(|| Ok::<_, anyhow::Error>(42));
assert_eq!(result.expect("should succeed"), 42);
let result = cache.execute_with_cache(|| Err::<i32, _>(anyhow!("Failed")));
assert_eq!(result.expect("should succeed"), 42);
}
#[test]
fn test_fallback_chain() {
let mut chain = FallbackChain::new();
chain.add(|| Err(anyhow!("First fails")));
chain.add(|| Err(anyhow!("Second fails")));
chain.add(|| Ok("Third succeeds".to_string()));
let result = chain.execute();
assert!(result.is_ok());
assert_eq!(result.expect("should succeed"), "Third succeeds");
}
}