Skip to main content

structured_proxy/shield/
window.rs

1//! Sliding-window counter math for the global (cross-instance) limit view.
2//!
3//! A fixed window has a boundary burst: a client can spend a full window's quota
4//! just before the boundary and another just after. The sliding-window counter
5//! smooths this by blending the current epoch's count with a decaying fraction
6//! of the previous epoch's, which is why the fleet-wide gate has no boundary
7//! burst even though the local shaper is GCRA.
8
9use std::time::Duration;
10
11/// The epoch (window index) for `now`, aligned to wall-clock so every instance
12/// agrees on window boundaries.
13pub fn epoch(now: Duration, window: Duration) -> u64 {
14    now.as_secs() / window.as_secs().max(1)
15}
16
17/// How far into the current window `now` sits.
18pub fn elapsed_in_window(now: Duration, window: Duration) -> Duration {
19    let w = window.as_secs().max(1);
20    Duration::from_secs(now.as_secs() % w) + Duration::from_nanos(u64::from(now.subsec_nanos()))
21}
22
23/// Blend the current-epoch count with the previous epoch's, weighted by how much
24/// of the current window remains: a request early in the window still "sees"
25/// most of the previous window's traffic, decaying to none by the boundary.
26pub fn sliding_estimate(cur: u64, prev: u64, elapsed: Duration, window: Duration) -> f64 {
27    let w = window.as_secs_f64().max(f64::MIN_POSITIVE);
28    let frac = (elapsed.as_secs_f64() / w).clamp(0.0, 1.0);
29    cur as f64 + prev as f64 * (1.0 - frac)
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35
36    const W: Duration = Duration::from_secs(60);
37
38    #[test]
39    fn epoch_advances_once_per_window() {
40        assert_eq!(epoch(Duration::from_secs(0), W), 0);
41        assert_eq!(epoch(Duration::from_secs(59), W), 0);
42        assert_eq!(epoch(Duration::from_secs(60), W), 1);
43        assert_eq!(epoch(Duration::from_secs(125), W), 2);
44    }
45
46    #[test]
47    fn estimate_at_window_start_counts_full_previous() {
48        // At the very start of the window, the whole previous epoch still counts.
49        assert_eq!(sliding_estimate(0, 100, Duration::ZERO, W), 100.0);
50    }
51
52    #[test]
53    fn estimate_at_window_end_drops_previous() {
54        // At the end of the window, the previous epoch has fully decayed.
55        let est = sliding_estimate(10, 100, W, W);
56        assert!((est - 10.0).abs() < 1e-9);
57    }
58
59    #[test]
60    fn estimate_midway_halves_previous() {
61        let est = sliding_estimate(10, 100, Duration::from_secs(30), W);
62        assert!((est - 60.0).abs() < 1e-9); // 10 + 100 * 0.5
63    }
64}