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        crate::server::console::stderr_line(format_args!("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        crate::server::console::stderr_line(format_args!(
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///
398/// Clones share one bucket map. It tracks at most 100,000 keys: buckets that
399/// have refilled completely are dropped (a new bucket behaves identically),
400/// and beyond the cap the least recently used buckets are evicted, which gives
401/// those clients a fresh burst.
402#[derive(Clone)]
403pub struct RateLimiter {
404    pub(crate) config: RateLimitConfig,
405    buckets: Arc<DashMap<String, TokenBucket>>,
406    key_extractor: Arc<dyn Fn(&Request) -> String + Send + Sync>,
407    new_keys: Arc<AtomicUsize>,
408    max_buckets: usize,
409}
410
411impl RateLimiter {
412    /// Creates a new `RateLimiter` from the given config, using the transport
413    /// peer address as the default key (IPv4 per address, IPv6 per /64).
414    ///
415    /// Forwarded headers are untrusted and deliberately ignored. Deployments
416    /// behind a trusted proxy can install an explicit key extractor after
417    /// validating the proxy chain.
418    pub fn new(config: RateLimitConfig) -> Self {
419        Self {
420            config,
421            buckets: Arc::new(DashMap::new()),
422            key_extractor: Arc::new(default_key_extractor),
423            new_keys: Arc::new(AtomicUsize::new(0)),
424            max_buckets: buckets::MAX_RATE_LIMIT_BUCKETS,
425        }
426    }
427
428    /// Overrides the key extraction method (e.g. to limit by username or auth token).
429    pub fn with_key_extractor<F>(mut self, extractor: F) -> Self
430    where
431        F: Fn(&Request) -> String + Send + Sync + 'static,
432    {
433        self.key_extractor = Arc::new(extractor);
434        self
435    }
436
437    /// Evaluates if the bucket for `key` can consume 1 token, refilling dynamic tokens incrementally.
438    pub fn check_and_consume(&self, key: &str) -> bool {
439        let now = Instant::now();
440        if !self.buckets.contains_key(key) {
441            self.make_room_for_new_key(now);
442        }
443        let mut entry = self
444            .buckets
445            .entry(key.to_string())
446            .or_insert_with(|| TokenBucket {
447                tokens: self.config.max_tokens,
448                last_refill: now,
449            });
450
451        let elapsed = now.duration_since(entry.last_refill).as_secs_f64();
452        entry.tokens = refill_token_count(entry.tokens, elapsed, &self.config);
453        entry.last_refill = now;
454
455        if entry.tokens >= 1.0 {
456            entry.tokens -= 1.0;
457            true
458        } else {
459            false
460        }
461    }
462}
463
464/// Default key extractor based only on Axum's transport peer address.
465///
466/// IPv4 peers are keyed per address (`192.0.2.7`) and IPv6 peers per /64
467/// prefix (`2001:db8:1:2::/64`); IPv4-mapped IPv6 peers are keyed as IPv4.
468pub fn default_key_extractor(req: &Request) -> String {
469    if let Some(conn_info) = req
470        .extensions()
471        .get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
472    {
473        return buckets::peer_rate_limit_key(conn_info.0.ip());
474    }
475
476    "missing-peer-address".to_string()
477}
478
479/// Native Axum middleware enforcing rate limiting.
480pub async fn rate_limit_middleware(limiter: RateLimiter, req: Request, next: Next) -> Response {
481    let key = (limiter.key_extractor)(&req);
482    if limiter.check_and_consume(&key) {
483        next.run(req).await
484    } else {
485        match Response::builder()
486            .status(StatusCode::TOO_MANY_REQUESTS)
487            .header(
488                axum::http::header::CONTENT_TYPE,
489                "text/plain; charset=utf-8",
490            )
491            .body(axum::body::Body::from(
492                "Rate limit exceeded. Please try again later.",
493            )) {
494            Ok(res) => res,
495            Err(_) => {
496                let mut res = Response::new(axum::body::Body::empty());
497                *res.status_mut() = StatusCode::TOO_MANY_REQUESTS;
498                res
499            }
500        }
501    }
502}
503
504#[cfg(test)]
505#[allow(clippy::unwrap_used, clippy::expect_used)]
506mod tests {
507    use super::*;
508    use axum::http::Request;
509    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
510
511    #[test]
512    fn traffic_shield_construction_is_runtime_independent() {
513        let shield = TrafficShield::new(TrafficShieldConfig::new().with_db_probe(false));
514
515        assert!(!shield.is_running());
516        assert_eq!(shield.start(), Err(TrafficShieldError::RuntimeUnavailable));
517        assert!(!shield.is_running());
518    }
519
520    #[tokio::test]
521    async fn traffic_shield_has_explicit_shared_shutdown() {
522        let shield = TrafficShield::new(TrafficShieldConfig::new().with_db_probe(false));
523        let clone = shield.clone();
524
525        shield.start().unwrap();
526        assert!(clone.is_running());
527        clone.shutdown();
528        assert!(!shield.is_running());
529        assert_eq!(shield.start(), Err(TrafficShieldError::AlreadyShutDown));
530    }
531
532    #[tokio::test]
533    async fn dropping_final_shield_aborts_monitor_tasks() {
534        let shield = TrafficShield::new(TrafficShieldConfig::new().with_db_probe(false));
535        let monitored_value = Arc::downgrade(&shield.event_loop_lag_ms);
536        shield.start().unwrap();
537        drop(shield);
538
539        tokio::time::timeout(Duration::from_millis(100), async {
540            while monitored_value.upgrade().is_some() {
541                tokio::task::yield_now().await;
542            }
543        })
544        .await
545        .unwrap();
546    }
547
548    #[test]
549    fn test_default_key_extractor() {
550        let req1 = Request::builder()
551            .header("x-forwarded-for", "192.168.1.1, 10.0.0.1")
552            .body(axum::body::Body::empty())
553            .unwrap();
554        assert_eq!(default_key_extractor(&req1), "missing-peer-address");
555
556        let req2 = Request::builder()
557            .header("x-real-ip", "10.0.0.2")
558            .body(axum::body::Body::empty())
559            .unwrap();
560        assert_eq!(default_key_extractor(&req2), "missing-peer-address");
561
562        let mut req3 = Request::builder().body(axum::body::Body::empty()).unwrap();
563        let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080);
564        req3.extensions_mut()
565            .insert(axum::extract::ConnectInfo(socket));
566        assert_eq!(default_key_extractor(&req3), "127.0.0.1");
567
568        let req4 = Request::builder().body(axum::body::Body::empty()).unwrap();
569        assert_eq!(default_key_extractor(&req4), "missing-peer-address");
570    }
571
572    #[tokio::test]
573    async fn test_traffic_shield_active_requests() {
574        let config = TrafficShieldConfig::new().with_db_probe(false);
575        let shield = TrafficShield::new(config);
576
577        assert_eq!(shield.active_requests(), 0);
578        shield
579            .active_requests
580            .fetch_add(5, std::sync::atomic::Ordering::SeqCst);
581        assert_eq!(shield.active_requests(), 5);
582    }
583
584    #[test]
585    fn test_traffic_pressure_classification() {
586        let config = TrafficShieldConfig::new().with_db_probe(false);
587
588        assert_eq!(
589            classify_traffic_pressure(&config, Duration::ZERO, Duration::ZERO, 0),
590            TrafficPressure::Normal
591        );
592        assert_eq!(
593            classify_traffic_pressure(&config, config.max_event_loop_lag / 2, Duration::ZERO, 0,),
594            TrafficPressure::Moderate
595        );
596        assert_eq!(
597            classify_traffic_pressure(&config, config.max_event_loop_lag, Duration::ZERO, 0,),
598            TrafficPressure::Critical
599        );
600        assert_eq!(
601            classify_traffic_pressure(&config, Duration::ZERO, config.max_db_latency, 0,),
602            TrafficPressure::Normal
603        );
604    }
605
606    #[test]
607    fn test_rate_limit_config_per_minute() {
608        let config = RateLimitConfig::per_minute(60.0);
609        assert_eq!(config.max_tokens, 60.0);
610        assert_eq!(config.refill_rate, 1.0);
611    }
612
613    #[test]
614    fn test_rate_limit_config_per_second() {
615        let config = RateLimitConfig::per_second(10.0);
616        assert_eq!(config.max_tokens, 10.0);
617        assert_eq!(config.refill_rate, 10.0);
618    }
619
620    #[test]
621    fn test_rate_limit_config_per_hour() {
622        let config = RateLimitConfig::per_hour(3600.0);
623        assert_eq!(config.max_tokens, 3600.0);
624        assert_eq!(config.refill_rate, 1.0);
625    }
626
627    #[tokio::test]
628    async fn test_traffic_shield_db_latency() {
629        let config = TrafficShieldConfig::new().with_db_probe(false);
630        let shield = TrafficShield::new(config);
631
632        assert_eq!(shield.db_latency().as_millis(), 0);
633        shield
634            .db_latency_ms
635            .store(50, std::sync::atomic::Ordering::Relaxed);
636        assert_eq!(shield.db_latency().as_millis(), 50);
637    }
638
639    #[tokio::test]
640    async fn test_traffic_shield_event_loop_lag() {
641        let config = TrafficShieldConfig::new().with_db_probe(false);
642        let shield = TrafficShield::new(config);
643
644        assert_eq!(shield.event_loop_lag().as_millis(), 0);
645        shield
646            .event_loop_lag_ms
647            .store(100, std::sync::atomic::Ordering::Relaxed);
648        assert_eq!(shield.event_loop_lag().as_millis(), 100);
649    }
650
651    #[test]
652    fn test_check_and_consume() {
653        let config = RateLimitConfig::per_second(2.0); // 2 tokens per second
654        let limiter = RateLimiter::new(config);
655
656        // Consume 1st token
657        assert!(limiter.check_and_consume("test_key"));
658        // Consume 2nd token
659        assert!(limiter.check_and_consume("test_key"));
660        // 3rd token should fail (out of tokens)
661        assert!(!limiter.check_and_consume("test_key"));
662    }
663    #[tokio::test]
664    async fn test_traffic_shield_config_builders() {
665        let config = TrafficShieldConfig::new()
666            .with_max_event_loop_lag(Duration::from_millis(999))
667            .with_max_db_latency(Duration::from_millis(888))
668            .with_max_active_requests(777);
669
670        assert_eq!(config.max_event_loop_lag, Duration::from_millis(999));
671        assert_eq!(config.max_db_latency, Duration::from_millis(888));
672        assert_eq!(config.max_active_requests, 777);
673    }
674
675    #[tokio::test]
676    async fn test_check_and_consume_refill() {
677        let config = RateLimitConfig::per_second(2.0); // max 2, refill 2/s
678        let limiter = RateLimiter::new(config);
679
680        assert!(limiter.check_and_consume("test_key"));
681        assert!(limiter.check_and_consume("test_key"));
682        assert!(!limiter.check_and_consume("test_key")); // 0 tokens left
683
684        // wait for refill
685        tokio::time::sleep(Duration::from_millis(600)).await; // 0.6s * 2 = 1.2 tokens
686        assert!(limiter.check_and_consume("test_key"));
687        assert!(!limiter.check_and_consume("test_key")); // Should only be 1 token restored
688    }
689
690    #[tokio::test]
691    async fn test_backpressure_middleware_critical() {
692        use axum::{Router, routing::get};
693        use tower::ServiceExt;
694
695        let config = TrafficShieldConfig::new().with_max_active_requests(0); // instant shed
696        let shield = TrafficShield::new(config);
697
698        let app =
699            Router::new()
700                .route("/", get(|| async { "OK" }))
701                .layer(axum::middleware::from_fn(move |req, next| {
702                    let s = shield.clone();
703                    async move { backpressure_middleware(s, req, next).await }
704                }));
705
706        let req = axum::http::Request::builder()
707            .uri("/")
708            .body(axum::body::Body::empty())
709            .unwrap();
710        let res = app.oneshot(req).await.unwrap();
711        assert_eq!(res.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);
712    }
713
714    #[tokio::test]
715    async fn test_backpressure_middleware_moderate() {
716        use axum::{Router, routing::get};
717        use std::time::Instant;
718        use tower::ServiceExt;
719
720        let config = TrafficShieldConfig::new().with_max_active_requests(100);
721        let shield = TrafficShield::new(config);
722        shield
723            .active_requests
724            .store(50, std::sync::atomic::Ordering::SeqCst); // moderate (>= max/2)
725
726        let app =
727            Router::new()
728                .route("/", get(|| async { "OK" }))
729                .layer(axum::middleware::from_fn(move |req, next| {
730                    let s = shield.clone();
731                    async move { backpressure_middleware(s, req, next).await }
732                }));
733
734        let req = axum::http::Request::builder()
735            .uri("/")
736            .body(axum::body::Body::empty())
737            .unwrap();
738        let start = Instant::now();
739        let res = app.oneshot(req).await.unwrap();
740        let elapsed = start.elapsed();
741
742        assert_eq!(res.status(), axum::http::StatusCode::OK);
743        assert!(elapsed >= Duration::from_millis(25)); // moderate load causes 25ms sleep
744    }
745
746    #[tokio::test]
747    async fn test_rate_limit_middleware_rejection() {
748        use axum::{Router, routing::get};
749        use tower::ServiceExt;
750
751        let config = RateLimitConfig::per_second(1.0); // 1 request per second capacity
752        let limiter = RateLimiter::new(config);
753
754        let app =
755            Router::new()
756                .route("/", get(|| async { "OK" }))
757                .layer(axum::middleware::from_fn(move |req, next| {
758                    let l = limiter.clone();
759                    async move { rate_limit_middleware(l, req, next).await }
760                }));
761
762        // 1st request should succeed
763        let req1 = axum::http::Request::builder()
764            .uri("/")
765            .body(axum::body::Body::empty())
766            .unwrap();
767        let res1 = app.clone().oneshot(req1).await.unwrap();
768        assert_eq!(res1.status(), axum::http::StatusCode::OK);
769
770        // 2nd request should be rejected by rate_limit_middleware
771        let req2 = axum::http::Request::builder()
772            .uri("/")
773            .body(axum::body::Body::empty())
774            .unwrap();
775        let res2 = app.oneshot(req2).await.unwrap();
776        assert_eq!(res2.status(), axum::http::StatusCode::TOO_MANY_REQUESTS);
777
778        let body_bytes = axum::body::to_bytes(res2.into_body(), 1024).await.unwrap();
779        assert_eq!(
780            String::from_utf8_lossy(&body_bytes),
781            "Rate limit exceeded. Please try again later."
782        );
783    }
784}
785
786#[path = "resilience_buckets.rs"]
787mod buckets;
788
789#[cfg(test)]
790#[path = "resilience_contract_tests.rs"]
791mod contract_tests;
792
793#[cfg(test)]
794#[path = "resilience_rate_limit_tests.rs"]
795mod rate_limit_tests;
796
797#[cfg(kani)]
798#[cfg_attr(mutants, mutants::skip)]
799mod kani_proofs {
800    use super::*;
801
802    #[kani::proof]
803    fn verify_token_bucket_math_safety() {
804        let max_tokens: f64 = kani::any();
805        let refill_rate: f64 = kani::any();
806        let current_tokens: f64 = kani::any();
807        let elapsed_secs: f64 = kani::any();
808
809        // Constrain to reasonable limits to avoid trivial infinity
810        kani::assume(max_tokens > 0.0 && max_tokens < 1_000_000.0);
811        kani::assume(refill_rate >= 0.0 && refill_rate < 100_000.0);
812        kani::assume(current_tokens >= 0.0 && current_tokens <= max_tokens);
813        kani::assume(elapsed_secs >= 0.0 && elapsed_secs < 31_536_000.0); // Up to 1 year
814
815        let config = RateLimitConfig::new(max_tokens, refill_rate);
816        let final_tokens = refill_token_count(current_tokens, elapsed_secs, &config);
817
818        // Prove that the math never yields NaN or Infinity under normal constraints
819        assert!(!final_tokens.is_nan());
820        assert!(!final_tokens.is_infinite());
821        assert!(final_tokens >= 0.0);
822        assert!(final_tokens <= max_tokens);
823    }
824
825    #[kani::proof]
826    fn verify_traffic_shield_thresholds() {
827        let max_event_loop_lag_ns: u64 = kani::any();
828        let max_db_latency_ns: u64 = kani::any();
829        let max_active_requests: usize = kani::any();
830        let enable_db_probe: bool = kani::any();
831        let lag_ns: u64 = kani::any();
832        let db_latency_ns: u64 = kani::any();
833        let active: usize = kani::any();
834
835        let config = TrafficShieldConfig {
836            max_event_loop_lag: Duration::from_nanos(max_event_loop_lag_ns),
837            max_db_latency: Duration::from_nanos(max_db_latency_ns),
838            max_active_requests,
839            enable_db_probe,
840        };
841        let lag = Duration::from_nanos(lag_ns);
842        let db_latency = Duration::from_nanos(db_latency_ns);
843        let pressure = classify_traffic_pressure(&config, lag, db_latency, active);
844
845        let critical = lag >= config.max_event_loop_lag
846            || (enable_db_probe && db_latency >= config.max_db_latency)
847            || active >= max_active_requests;
848        let moderate = lag >= config.max_event_loop_lag / 2
849            || (enable_db_probe && db_latency >= config.max_db_latency / 2)
850            || active >= max_active_requests / 2;
851
852        match pressure {
853            TrafficPressure::Critical => {
854                assert!(critical);
855            }
856            TrafficPressure::Moderate => {
857                assert!(!critical);
858                assert!(moderate);
859            }
860            TrafficPressure::Normal => {
861                assert!(!critical);
862                assert!(!moderate);
863            }
864        }
865    }
866}