ppoppo-infra 0.1.0

Backend-agnostic infrastructure traits for caching, queuing, and messaging
Documentation
//! Fail-open decorator for rate limiting.

use async_trait::async_trait;

use crate::rate_limit::RateLimit;
use crate::types::RateLimitResult;

/// Fail-open decorator: inner RateLimit 실패 시 요청을 허용한다.
///
/// KVRocks 등 외부 백엔드 장애(~30초) 동안 L1(in-memory governor)만 동작하며,
/// cross-pod 제한은 일시 해제된다. Composition root에서 선언적으로 조립:
///
/// ```ignore
/// let rate_limit: Arc<dyn RateLimit> = Arc::new(
///     ResilientRateLimit::new(KvRateLimit::new(kv.clone()))
/// );
/// ```
pub struct ResilientRateLimit<T> {
    inner: T,
}

impl<T: RateLimit> ResilientRateLimit<T> {
    /// Wrap a rate limiter with fail-open semantics.
    pub fn new(inner: T) -> Self {
        Self { inner }
    }
}

#[async_trait]
impl<T: RateLimit> RateLimit for ResilientRateLimit<T> {
    async fn check(
        &self,
        key: &str,
        max_requests: i32,
        window_seconds: i32,
    ) -> crate::Result<RateLimitResult> {
        match self.inner.check(key, max_requests, window_seconds).await {
            Ok(result) => Ok(result),
            Err(e) => {
                tracing::warn!(
                    error = %e,
                    key,
                    "L2 rate limit unavailable, falling back to L1 only"
                );
                Ok(RateLimitResult::allowed_fallback(
                    max_requests,
                    window_seconds,
                ))
            }
        }
    }

    async fn peek(
        &self,
        key: &str,
        max_requests: i32,
        window_seconds: i32,
    ) -> crate::Result<RateLimitResult> {
        match self.inner.peek(key, max_requests, window_seconds).await {
            Ok(result) => Ok(result),
            Err(e) => {
                tracing::warn!(
                    error = %e,
                    key,
                    "L2 rate limit peek unavailable, returning fallback"
                );
                Ok(RateLimitResult::allowed_fallback(
                    max_requests,
                    window_seconds,
                ))
            }
        }
    }

    async fn reset(&self, key: &str) -> crate::Result<usize> {
        self.inner.reset(key).await
    }
}