Skip to main content

rullst_core/
resilience.rs

1use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response};
2use dashmap::DashMap;
3use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize, Ordering};
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6
7const MONITORS_IDLE: u8 = 0;
8const MONITORS_RUNNING: u8 = 1;
9const MONITORS_SHUT_DOWN: u8 = 2;
10
11/// Failures that can occur while managing Traffic Shield monitors.
12#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum TrafficShieldError {
15    /// Monitoring was started outside an active Tokio runtime.
16    #[error("Traffic Shield monitors require an active Tokio runtime")]
17    RuntimeUnavailable,
18    /// Monitoring cannot be restarted after explicit shutdown.
19    #[error("Traffic Shield monitors have already been shut down")]
20    AlreadyShutDown,
21}
22
23/// Configures the limits and behavior of the Adaptive Backpressure & Resilient Traffic Shielding.
24#[non_exhaustive]
25#[derive(Clone, Debug)]
26pub struct TrafficShieldConfig {
27    /// Maximum Tokio event-loop lag before load shedding activates. Default: 100ms.
28    pub max_event_loop_lag: Duration,
29    /// Maximum DB probe round-trip latency before load shedding activates. Default: 500ms.
30    pub max_db_latency: Duration,
31    /// Maximum number of concurrent in-flight requests before load shedding activates. Default: 1000.
32    pub max_active_requests: usize,
33    /// If `true`, spawns a background task that probes the DB with `SELECT 1` every second to measure latency.
34    pub enable_db_probe: bool,
35}
36
37impl Default for TrafficShieldConfig {
38    fn default() -> Self {
39        Self {
40            max_event_loop_lag: Duration::from_millis(100),
41            max_db_latency: Duration::from_millis(500),
42            max_active_requests: 1000,
43            enable_db_probe: true,
44        }
45    }
46}
47
48impl TrafficShieldConfig {
49    /// Creates a `TrafficShieldConfig` with sensible production defaults:
50    /// 100ms event-loop lag threshold, 500ms DB latency threshold, 1000 max active requests.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Sets the maximum event loop latency/lag allowed before shedding load.
56    pub fn with_max_event_loop_lag(mut self, lag: Duration) -> Self {
57        self.max_event_loop_lag = lag;
58        self
59    }
60
61    /// Sets the maximum database probe query latency allowed before shedding load.
62    pub fn with_max_db_latency(mut self, latency: Duration) -> Self {
63        self.max_db_latency = latency;
64        self
65    }
66
67    /// Sets the maximum simultaneous requests allowed.
68    pub fn with_max_active_requests(mut self, limit: usize) -> Self {
69        self.max_active_requests = limit;
70        self
71    }
72
73    /// Configures whether to run a background database probe loop (`SELECT 1`).
74    pub fn with_db_probe(mut self, enable: bool) -> Self {
75        self.enable_db_probe = enable;
76        self
77    }
78}
79
80/// The core resilience monitor that performs real-time diagnostics on Tokio latency and Database roundtrip speeds.
81#[derive(Clone)]
82pub struct TrafficShield {
83    pub(crate) config: TrafficShieldConfig,
84    event_loop_lag_ms: Arc<AtomicU64>,
85    db_latency_ms: Arc<AtomicU64>,
86    active_requests: Arc<AtomicUsize>,
87    monitors: Arc<TrafficShieldMonitors>,
88}
89
90struct TrafficShieldMonitors {
91    state: AtomicU8,
92    shutdown: Arc<tokio::sync::Notify>,
93    tasks: Mutex<Vec<tokio::task::JoinHandle<()>>>,
94}
95
96impl TrafficShieldMonitors {
97    fn abort_tasks(&self) {
98        self.state.store(MONITORS_SHUT_DOWN, Ordering::Release);
99        self.shutdown.notify_waiters();
100        let mut tasks = match self.tasks.lock() {
101            Ok(tasks) => tasks,
102            Err(poisoned) => poisoned.into_inner(),
103        };
104        for task in tasks.drain(..) {
105            task.abort();
106        }
107    }
108}
109
110impl Drop for TrafficShieldMonitors {
111    fn drop(&mut self) {
112        self.abort_tasks();
113    }
114}
115
116impl TrafficShield {
117    /// Creates a new inactive `TrafficShield`.
118    ///
119    /// Call [`TrafficShield::start`] from inside a Tokio runtime, attach the
120    /// shield to [`crate::Server`], or use [`backpressure_middleware`], which
121    /// starts it lazily on its first request. Keeping construction side-effect
122    /// free makes this API safe in synchronous setup code and tests.
123    pub fn new(config: TrafficShieldConfig) -> Self {
124        Self {
125            config,
126            event_loop_lag_ms: Arc::new(AtomicU64::new(0)),
127            db_latency_ms: Arc::new(AtomicU64::new(0)),
128            active_requests: Arc::new(AtomicUsize::new(0)),
129            monitors: Arc::new(TrafficShieldMonitors {
130                state: AtomicU8::new(MONITORS_IDLE),
131                shutdown: Arc::new(tokio::sync::Notify::new()),
132                tasks: Mutex::new(Vec::new()),
133            }),
134        }
135    }
136
137    /// Starts event-loop and optional database monitoring.
138    ///
139    /// The operation is idempotent while running and returns a typed error when
140    /// called outside Tokio or after [`TrafficShield::shutdown`]. All monitor
141    /// tasks are cancelled on explicit shutdown or when the final shield clone
142    /// is dropped.
143    ///
144    /// # Errors
145    /// Returns [`TrafficShieldError::RuntimeUnavailable`] without spawning when
146    /// no Tokio runtime is active, or [`TrafficShieldError::AlreadyShutDown`]
147    /// after the lifecycle has ended.
148    #[cfg_attr(mutants, mutants::skip)]
149    pub fn start(&self) -> Result<(), TrafficShieldError> {
150        match self.monitors.state.load(Ordering::Acquire) {
151            MONITORS_RUNNING => return Ok(()),
152            MONITORS_SHUT_DOWN => return Err(TrafficShieldError::AlreadyShutDown),
153            _ => {}
154        }
155        let runtime = tokio::runtime::Handle::try_current()
156            .map_err(|_| TrafficShieldError::RuntimeUnavailable)?;
157
158        match self.monitors.state.compare_exchange(
159            MONITORS_IDLE,
160            MONITORS_RUNNING,
161            Ordering::AcqRel,
162            Ordering::Acquire,
163        ) {
164            Ok(_) => {}
165            Err(MONITORS_RUNNING) => return Ok(()),
166            Err(_) => return Err(TrafficShieldError::AlreadyShutDown),
167        }
168
169        let mut tasks = match self.monitors.tasks.lock() {
170            Ok(tasks) => tasks,
171            Err(poisoned) => poisoned.into_inner(),
172        };
173        if self.monitors.state.load(Ordering::Acquire) != MONITORS_RUNNING {
174            return Err(TrafficShieldError::AlreadyShutDown);
175        }
176
177        let lag_ms = self.event_loop_lag_ms.clone();
178        let lag_shutdown = Arc::clone(&self.monitors.shutdown);
179        tasks.push(runtime.spawn(async move {
180            let interval = Duration::from_millis(100);
181            loop {
182                let start = Instant::now();
183                tokio::select! {
184                    _ = tokio::time::sleep(interval) => {}
185                    _ = lag_shutdown.notified() => break,
186                }
187                let elapsed = start.elapsed();
188                let lag = elapsed.saturating_sub(interval);
189                lag_ms.store(duration_millis_u64(lag), Ordering::Relaxed);
190            }
191        }));
192
193        #[cfg(feature = "orm")]
194        if self.config.enable_db_probe {
195            let db_lat_ms = self.db_latency_ms.clone();
196            let db_shutdown = Arc::clone(&self.monitors.shutdown);
197            tasks.push(runtime.spawn(async move {
198                let interval = Duration::from_millis(1000);
199                loop {
200                    tokio::select! {
201                        _ = tokio::time::sleep(interval) => {}
202                        _ = db_shutdown.notified() => break,
203                    }
204                    if let Some(pool) = crate::db::safe_pool() {
205                        let start = Instant::now();
206                        let res = sqlx::query("SELECT 1").execute(pool).await;
207                        match res {
208                            Ok(_) => {
209                                let latency = start.elapsed();
210                                db_lat_ms.store(duration_millis_u64(latency), Ordering::Relaxed);
211                            }
212                            Err(_) => {
213                                db_lat_ms.store(9999, Ordering::Relaxed);
214                            }
215                        }
216                    } else {
217                        db_lat_ms.store(0, Ordering::Relaxed);
218                    }
219                }
220            }));
221        }
222
223        Ok(())
224    }
225
226    /// Stops all background monitors and prevents this shared shield lifecycle
227    /// from being restarted. Calling this method more than once is harmless.
228    pub fn shutdown(&self) {
229        self.monitors.abort_tasks();
230    }
231
232    /// Returns whether monitor tasks have been started and not shut down.
233    pub fn is_running(&self) -> bool {
234        self.monitors.state.load(Ordering::Acquire) == MONITORS_RUNNING
235    }
236
237    /// Returns the most recently measured Tokio event-loop lag as a `Duration`.
238    /// Updated every 100ms by the background monitor task.
239    pub fn event_loop_lag(&self) -> Duration {
240        Duration::from_millis(self.event_loop_lag_ms.load(Ordering::Relaxed))
241    }
242
243    /// Returns the most recently measured database probe round-trip latency as a `Duration`.
244    /// Returns `Duration::ZERO` if `enable_db_probe` is `false`, the `orm`
245    /// feature is disabled, or the pool is uninitialized.
246    pub fn db_latency(&self) -> Duration {
247        Duration::from_millis(self.db_latency_ms.load(Ordering::Relaxed))
248    }
249
250    /// Returns the current count of in-flight HTTP requests being tracked by this shield.
251    pub fn active_requests(&self) -> usize {
252        self.active_requests.load(Ordering::Relaxed)
253    }
254}
255
256fn duration_millis_u64(duration: Duration) -> u64 {
257    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
258}
259
260#[derive(Clone, Copy, Debug, PartialEq, Eq)]
261enum TrafficPressure {
262    Normal,
263    Moderate,
264    Critical,
265}
266
267fn classify_traffic_pressure(
268    config: &TrafficShieldConfig,
269    lag: Duration,
270    db_latency: Duration,
271    active_requests: usize,
272) -> TrafficPressure {
273    let critical = lag >= config.max_event_loop_lag
274        || (config.enable_db_probe && db_latency >= config.max_db_latency)
275        || active_requests >= config.max_active_requests;
276    if critical {
277        return TrafficPressure::Critical;
278    }
279
280    let moderate = lag >= config.max_event_loop_lag / 2
281        || (config.enable_db_probe && db_latency >= config.max_db_latency / 2)
282        || active_requests >= config.max_active_requests / 2;
283    if moderate {
284        TrafficPressure::Moderate
285    } else {
286        TrafficPressure::Normal
287    }
288}
289
290struct ActiveRequestGuard<'a>(&'a AtomicUsize);
291
292impl<'a> Drop for ActiveRequestGuard<'a> {
293    fn drop(&mut self) {
294        self.0.fetch_sub(1, Ordering::SeqCst);
295    }
296}
297
298/// Router-level protection middleware that tracks load timing and drops requests under critical saturation.
299#[cfg_attr(mutants, mutants::skip)]
300pub async fn backpressure_middleware(shield: TrafficShield, req: Request, next: Next) -> Response {
301    if let Err(error) = shield.start() {
302        eprintln!("Traffic Shield is unavailable: {error}");
303        let mut response = Response::new(axum::body::Body::from(
304            "Traffic Shield monitoring is unavailable.",
305        ));
306        *response.status_mut() = StatusCode::SERVICE_UNAVAILABLE;
307        return response;
308    }
309
310    let active = shield.active_requests.fetch_add(1, Ordering::SeqCst);
311    let _guard = ActiveRequestGuard(&shield.active_requests);
312
313    let lag = shield.event_loop_lag();
314    let db_lat = shield.db_latency();
315
316    let pressure = classify_traffic_pressure(&shield.config, lag, db_lat, active);
317
318    if pressure == TrafficPressure::Critical {
319        eprintln!(
320            "⚠️ [Rullst Backpressure] Load shedding active! CPU lag: {:?}, DB latency: {:?}, Active requests: {}",
321            lag, db_lat, active
322        );
323
324        match Response::builder()
325            .status(StatusCode::SERVICE_UNAVAILABLE)
326            .header(axum::http::header::RETRY_AFTER, "5")
327            .header(
328                axum::http::header::CONTENT_TYPE,
329                "text/plain; charset=utf-8",
330            )
331            .body(axum::body::Body::from(
332                "Service Temporarily Saturated. Please try again soon.",
333            )) {
334            Ok(res) => return res,
335            Err(_) => {
336                let mut res = Response::new(axum::body::Body::empty());
337                *res.status_mut() = StatusCode::SERVICE_UNAVAILABLE;
338                return res;
339            }
340        }
341    }
342
343    if pressure == TrafficPressure::Moderate {
344        tokio::time::sleep(Duration::from_millis(25)).await;
345    }
346
347    next.run(req).await
348}
349
350/// Extensible configuration for the Token-Bucket Rate Limiter.
351#[non_exhaustive]
352#[derive(Clone, Debug)]
353pub struct RateLimitConfig {
354    /// Maximum number of tokens (burst capacity). A request consumes 1 token.
355    pub max_tokens: f64,
356    /// Token refill rate in **tokens per second**. Use the `per_second`, `per_minute`, `per_hour` factories.
357    pub refill_rate: f64,
358}
359
360impl RateLimitConfig {
361    /// Creates a new `RateLimitConfig` with explicit burst capacity and refill rate.
362    /// For convenience, prefer the factory methods: `per_second`, `per_minute`, `per_hour`.
363    pub fn new(max_tokens: f64, refill_rate: f64) -> Self {
364        Self {
365            max_tokens,
366            refill_rate,
367        }
368    }
369
370    /// Creates a limit of N requests per second.
371    pub fn per_second(limit: f64) -> Self {
372        Self::new(limit, limit)
373    }
374
375    /// Creates a limit of N requests per minute.
376    pub fn per_minute(limit: f64) -> Self {
377        Self::new(limit, limit / 60.0)
378    }
379
380    /// Creates a limit of N requests per hour.
381    pub fn per_hour(limit: f64) -> Self {
382        Self::new(limit, limit / 3600.0)
383    }
384}
385
386#[derive(Clone, Debug)]
387struct TokenBucket {
388    tokens: f64,
389    last_refill: Instant,
390}
391
392fn refill_token_count(current_tokens: f64, elapsed_secs: f64, config: &RateLimitConfig) -> f64 {
393    (current_tokens + elapsed_secs * config.refill_rate).min(config.max_tokens)
394}
395
396/// Thread-safe Token-Bucket rate limiter powered by Shared-Memory DashMap.
397#[derive(Clone)]
398pub struct RateLimiter {
399    pub(crate) config: RateLimitConfig,
400    buckets: Arc<DashMap<String, TokenBucket>>,
401    key_extractor: Arc<dyn Fn(&Request) -> String + Send + Sync>,
402}
403
404impl RateLimiter {
405    /// Creates a new `RateLimiter` from the given config, using the transport
406    /// peer address as the default key.
407    ///
408    /// Forwarded headers are untrusted and deliberately ignored. Deployments
409    /// behind a trusted proxy can install an explicit key extractor after
410    /// validating the proxy chain.
411    pub fn new(config: RateLimitConfig) -> Self {
412        Self {
413            config,
414            buckets: Arc::new(DashMap::new()),
415            key_extractor: Arc::new(default_key_extractor),
416        }
417    }
418
419    /// Overrides the key extraction method (e.g. to limit by username or auth token).
420    pub fn with_key_extractor<F>(mut self, extractor: F) -> Self
421    where
422        F: Fn(&Request) -> String + Send + Sync + 'static,
423    {
424        self.key_extractor = Arc::new(extractor);
425        self
426    }
427
428    /// Evaluates if the bucket for `key` can consume 1 token, refilling dynamic tokens incrementally.
429    pub fn check_and_consume(&self, key: &str) -> bool {
430        let now = Instant::now();
431        let mut entry = self
432            .buckets
433            .entry(key.to_string())
434            .or_insert_with(|| TokenBucket {
435                tokens: self.config.max_tokens,
436                last_refill: now,
437            });
438
439        let elapsed = now.duration_since(entry.last_refill).as_secs_f64();
440        entry.tokens = refill_token_count(entry.tokens, elapsed, &self.config);
441        entry.last_refill = now;
442
443        if entry.tokens >= 1.0 {
444            entry.tokens -= 1.0;
445            true
446        } else {
447            false
448        }
449    }
450}
451
452/// Default key extractor based only on Axum's transport peer address.
453pub fn default_key_extractor(req: &Request) -> String {
454    if let Some(conn_info) = req
455        .extensions()
456        .get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
457    {
458        return conn_info.0.ip().to_string();
459    }
460
461    "missing-peer-address".to_string()
462}
463
464/// Native Axum middleware enforcing rate limiting.
465pub async fn rate_limit_middleware(limiter: RateLimiter, req: Request, next: Next) -> Response {
466    let key = (limiter.key_extractor)(&req);
467    if limiter.check_and_consume(&key) {
468        next.run(req).await
469    } else {
470        match Response::builder()
471            .status(StatusCode::TOO_MANY_REQUESTS)
472            .header(
473                axum::http::header::CONTENT_TYPE,
474                "text/plain; charset=utf-8",
475            )
476            .body(axum::body::Body::from(
477                "Rate limit exceeded. Please try again later.",
478            )) {
479            Ok(res) => res,
480            Err(_) => {
481                let mut res = Response::new(axum::body::Body::empty());
482                *res.status_mut() = StatusCode::TOO_MANY_REQUESTS;
483                res
484            }
485        }
486    }
487}
488
489#[cfg(test)]
490#[allow(clippy::unwrap_used, clippy::expect_used)]
491mod tests {
492    use super::*;
493    use axum::http::Request;
494    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
495
496    #[test]
497    fn traffic_shield_construction_is_runtime_independent() {
498        let shield = TrafficShield::new(TrafficShieldConfig::new().with_db_probe(false));
499
500        assert!(!shield.is_running());
501        assert_eq!(shield.start(), Err(TrafficShieldError::RuntimeUnavailable));
502        assert!(!shield.is_running());
503    }
504
505    #[tokio::test]
506    async fn traffic_shield_has_explicit_shared_shutdown() {
507        let shield = TrafficShield::new(TrafficShieldConfig::new().with_db_probe(false));
508        let clone = shield.clone();
509
510        shield.start().unwrap();
511        assert!(clone.is_running());
512        clone.shutdown();
513        assert!(!shield.is_running());
514        assert_eq!(shield.start(), Err(TrafficShieldError::AlreadyShutDown));
515    }
516
517    #[tokio::test]
518    async fn dropping_final_shield_aborts_monitor_tasks() {
519        let shield = TrafficShield::new(TrafficShieldConfig::new().with_db_probe(false));
520        let monitored_value = Arc::downgrade(&shield.event_loop_lag_ms);
521        shield.start().unwrap();
522        drop(shield);
523
524        tokio::time::timeout(Duration::from_millis(100), async {
525            while monitored_value.upgrade().is_some() {
526                tokio::task::yield_now().await;
527            }
528        })
529        .await
530        .unwrap();
531    }
532
533    #[test]
534    fn test_default_key_extractor() {
535        let req1 = Request::builder()
536            .header("x-forwarded-for", "192.168.1.1, 10.0.0.1")
537            .body(axum::body::Body::empty())
538            .unwrap();
539        assert_eq!(default_key_extractor(&req1), "missing-peer-address");
540
541        let req2 = Request::builder()
542            .header("x-real-ip", "10.0.0.2")
543            .body(axum::body::Body::empty())
544            .unwrap();
545        assert_eq!(default_key_extractor(&req2), "missing-peer-address");
546
547        let mut req3 = Request::builder().body(axum::body::Body::empty()).unwrap();
548        let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080);
549        req3.extensions_mut()
550            .insert(axum::extract::ConnectInfo(socket));
551        assert_eq!(default_key_extractor(&req3), "127.0.0.1");
552
553        let req4 = Request::builder().body(axum::body::Body::empty()).unwrap();
554        assert_eq!(default_key_extractor(&req4), "missing-peer-address");
555    }
556
557    #[tokio::test]
558    async fn test_traffic_shield_active_requests() {
559        let config = TrafficShieldConfig::new().with_db_probe(false);
560        let shield = TrafficShield::new(config);
561
562        assert_eq!(shield.active_requests(), 0);
563        shield
564            .active_requests
565            .fetch_add(5, std::sync::atomic::Ordering::SeqCst);
566        assert_eq!(shield.active_requests(), 5);
567    }
568
569    #[test]
570    fn test_traffic_pressure_classification() {
571        let config = TrafficShieldConfig::new().with_db_probe(false);
572
573        assert_eq!(
574            classify_traffic_pressure(&config, Duration::ZERO, Duration::ZERO, 0),
575            TrafficPressure::Normal
576        );
577        assert_eq!(
578            classify_traffic_pressure(&config, config.max_event_loop_lag / 2, Duration::ZERO, 0,),
579            TrafficPressure::Moderate
580        );
581        assert_eq!(
582            classify_traffic_pressure(&config, config.max_event_loop_lag, Duration::ZERO, 0,),
583            TrafficPressure::Critical
584        );
585        assert_eq!(
586            classify_traffic_pressure(&config, Duration::ZERO, config.max_db_latency, 0,),
587            TrafficPressure::Normal
588        );
589    }
590
591    #[test]
592    fn test_rate_limit_config_per_minute() {
593        let config = RateLimitConfig::per_minute(60.0);
594        assert_eq!(config.max_tokens, 60.0);
595        assert_eq!(config.refill_rate, 1.0);
596    }
597
598    #[test]
599    fn test_rate_limit_config_per_second() {
600        let config = RateLimitConfig::per_second(10.0);
601        assert_eq!(config.max_tokens, 10.0);
602        assert_eq!(config.refill_rate, 10.0);
603    }
604
605    #[test]
606    fn test_rate_limit_config_per_hour() {
607        let config = RateLimitConfig::per_hour(3600.0);
608        assert_eq!(config.max_tokens, 3600.0);
609        assert_eq!(config.refill_rate, 1.0);
610    }
611
612    #[tokio::test]
613    async fn test_traffic_shield_db_latency() {
614        let config = TrafficShieldConfig::new().with_db_probe(false);
615        let shield = TrafficShield::new(config);
616
617        assert_eq!(shield.db_latency().as_millis(), 0);
618        shield
619            .db_latency_ms
620            .store(50, std::sync::atomic::Ordering::Relaxed);
621        assert_eq!(shield.db_latency().as_millis(), 50);
622    }
623
624    #[tokio::test]
625    async fn test_traffic_shield_event_loop_lag() {
626        let config = TrafficShieldConfig::new().with_db_probe(false);
627        let shield = TrafficShield::new(config);
628
629        assert_eq!(shield.event_loop_lag().as_millis(), 0);
630        shield
631            .event_loop_lag_ms
632            .store(100, std::sync::atomic::Ordering::Relaxed);
633        assert_eq!(shield.event_loop_lag().as_millis(), 100);
634    }
635
636    #[test]
637    fn test_check_and_consume() {
638        let config = RateLimitConfig::per_second(2.0); // 2 tokens per second
639        let limiter = RateLimiter::new(config);
640
641        // Consume 1st token
642        assert!(limiter.check_and_consume("test_key"));
643        // Consume 2nd token
644        assert!(limiter.check_and_consume("test_key"));
645        // 3rd token should fail (out of tokens)
646        assert!(!limiter.check_and_consume("test_key"));
647    }
648    #[tokio::test]
649    async fn test_traffic_shield_config_builders() {
650        let config = TrafficShieldConfig::new()
651            .with_max_event_loop_lag(Duration::from_millis(999))
652            .with_max_db_latency(Duration::from_millis(888))
653            .with_max_active_requests(777);
654
655        assert_eq!(config.max_event_loop_lag, Duration::from_millis(999));
656        assert_eq!(config.max_db_latency, Duration::from_millis(888));
657        assert_eq!(config.max_active_requests, 777);
658    }
659
660    #[tokio::test]
661    async fn test_check_and_consume_refill() {
662        let config = RateLimitConfig::per_second(2.0); // max 2, refill 2/s
663        let limiter = RateLimiter::new(config);
664
665        assert!(limiter.check_and_consume("test_key"));
666        assert!(limiter.check_and_consume("test_key"));
667        assert!(!limiter.check_and_consume("test_key")); // 0 tokens left
668
669        // wait for refill
670        tokio::time::sleep(Duration::from_millis(600)).await; // 0.6s * 2 = 1.2 tokens
671        assert!(limiter.check_and_consume("test_key"));
672        assert!(!limiter.check_and_consume("test_key")); // Should only be 1 token restored
673    }
674
675    #[tokio::test]
676    async fn test_backpressure_middleware_critical() {
677        use axum::{Router, routing::get};
678        use tower::ServiceExt;
679
680        let config = TrafficShieldConfig::new().with_max_active_requests(0); // instant shed
681        let shield = TrafficShield::new(config);
682
683        let app =
684            Router::new()
685                .route("/", get(|| async { "OK" }))
686                .layer(axum::middleware::from_fn(move |req, next| {
687                    let s = shield.clone();
688                    async move { backpressure_middleware(s, req, next).await }
689                }));
690
691        let req = axum::http::Request::builder()
692            .uri("/")
693            .body(axum::body::Body::empty())
694            .unwrap();
695        let res = app.oneshot(req).await.unwrap();
696        assert_eq!(res.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);
697    }
698
699    #[tokio::test]
700    async fn test_backpressure_middleware_moderate() {
701        use axum::{Router, routing::get};
702        use std::time::Instant;
703        use tower::ServiceExt;
704
705        let config = TrafficShieldConfig::new().with_max_active_requests(100);
706        let shield = TrafficShield::new(config);
707        shield
708            .active_requests
709            .store(50, std::sync::atomic::Ordering::SeqCst); // moderate (>= max/2)
710
711        let app =
712            Router::new()
713                .route("/", get(|| async { "OK" }))
714                .layer(axum::middleware::from_fn(move |req, next| {
715                    let s = shield.clone();
716                    async move { backpressure_middleware(s, req, next).await }
717                }));
718
719        let req = axum::http::Request::builder()
720            .uri("/")
721            .body(axum::body::Body::empty())
722            .unwrap();
723        let start = Instant::now();
724        let res = app.oneshot(req).await.unwrap();
725        let elapsed = start.elapsed();
726
727        assert_eq!(res.status(), axum::http::StatusCode::OK);
728        assert!(elapsed >= Duration::from_millis(25)); // moderate load causes 25ms sleep
729    }
730
731    #[tokio::test]
732    async fn test_rate_limit_middleware_rejection() {
733        use axum::{Router, routing::get};
734        use tower::ServiceExt;
735
736        let config = RateLimitConfig::per_second(1.0); // 1 request per second capacity
737        let limiter = RateLimiter::new(config);
738
739        let app =
740            Router::new()
741                .route("/", get(|| async { "OK" }))
742                .layer(axum::middleware::from_fn(move |req, next| {
743                    let l = limiter.clone();
744                    async move { rate_limit_middleware(l, req, next).await }
745                }));
746
747        // 1st request should succeed
748        let req1 = axum::http::Request::builder()
749            .uri("/")
750            .body(axum::body::Body::empty())
751            .unwrap();
752        let res1 = app.clone().oneshot(req1).await.unwrap();
753        assert_eq!(res1.status(), axum::http::StatusCode::OK);
754
755        // 2nd request should be rejected by rate_limit_middleware
756        let req2 = axum::http::Request::builder()
757            .uri("/")
758            .body(axum::body::Body::empty())
759            .unwrap();
760        let res2 = app.oneshot(req2).await.unwrap();
761        assert_eq!(res2.status(), axum::http::StatusCode::TOO_MANY_REQUESTS);
762
763        let body_bytes = axum::body::to_bytes(res2.into_body(), 1024).await.unwrap();
764        assert_eq!(
765            String::from_utf8_lossy(&body_bytes),
766            "Rate limit exceeded. Please try again later."
767        );
768    }
769}
770
771#[cfg(test)]
772#[path = "resilience_contract_tests.rs"]
773mod contract_tests;
774
775#[cfg(kani)]
776#[cfg_attr(mutants, mutants::skip)]
777mod kani_proofs {
778    use super::*;
779
780    #[kani::proof]
781    fn verify_token_bucket_math_safety() {
782        let max_tokens: f64 = kani::any();
783        let refill_rate: f64 = kani::any();
784        let current_tokens: f64 = kani::any();
785        let elapsed_secs: f64 = kani::any();
786
787        // Constrain to reasonable limits to avoid trivial infinity
788        kani::assume(max_tokens > 0.0 && max_tokens < 1_000_000.0);
789        kani::assume(refill_rate >= 0.0 && refill_rate < 100_000.0);
790        kani::assume(current_tokens >= 0.0 && current_tokens <= max_tokens);
791        kani::assume(elapsed_secs >= 0.0 && elapsed_secs < 31_536_000.0); // Up to 1 year
792
793        let config = RateLimitConfig::new(max_tokens, refill_rate);
794        let final_tokens = refill_token_count(current_tokens, elapsed_secs, &config);
795
796        // Prove that the math never yields NaN or Infinity under normal constraints
797        assert!(!final_tokens.is_nan());
798        assert!(!final_tokens.is_infinite());
799        assert!(final_tokens >= 0.0);
800        assert!(final_tokens <= max_tokens);
801    }
802
803    #[kani::proof]
804    fn verify_traffic_shield_thresholds() {
805        let max_event_loop_lag_ns: u64 = kani::any();
806        let max_db_latency_ns: u64 = kani::any();
807        let max_active_requests: usize = kani::any();
808        let enable_db_probe: bool = kani::any();
809        let lag_ns: u64 = kani::any();
810        let db_latency_ns: u64 = kani::any();
811        let active: usize = kani::any();
812
813        let config = TrafficShieldConfig {
814            max_event_loop_lag: Duration::from_nanos(max_event_loop_lag_ns),
815            max_db_latency: Duration::from_nanos(max_db_latency_ns),
816            max_active_requests,
817            enable_db_probe,
818        };
819        let lag = Duration::from_nanos(lag_ns);
820        let db_latency = Duration::from_nanos(db_latency_ns);
821        let pressure = classify_traffic_pressure(&config, lag, db_latency, active);
822
823        let critical = lag >= config.max_event_loop_lag
824            || (enable_db_probe && db_latency >= config.max_db_latency)
825            || active >= max_active_requests;
826        let moderate = lag >= config.max_event_loop_lag / 2
827            || (enable_db_probe && db_latency >= config.max_db_latency / 2)
828            || active >= max_active_requests / 2;
829
830        match pressure {
831            TrafficPressure::Critical => {
832                assert!(critical);
833            }
834            TrafficPressure::Moderate => {
835                assert!(!critical);
836                assert!(moderate);
837            }
838            TrafficPressure::Normal => {
839                assert!(!critical);
840                assert!(!moderate);
841            }
842        }
843    }
844}