wickra_exchange_core/
ratelimiter.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Acquire {
12 Allowed,
14 Throttled {
16 retry_after_ms: i64,
18 },
19}
20
21#[derive(Debug)]
23pub struct WeightedRateLimiter {
24 capacity: u32,
25 window_ms: i64,
26 used: u32,
27 window_start_ms: Option<i64>,
28 banned_until_ms: Option<i64>,
29}
30
31impl WeightedRateLimiter {
32 #[must_use]
34 pub fn new(capacity: u32, window_ms: i64) -> Self {
35 Self {
36 capacity,
37 window_ms,
38 used: 0,
39 window_start_ms: None,
40 banned_until_ms: None,
41 }
42 }
43
44 #[must_use]
46 pub fn used(&self) -> u32 {
47 self.used
48 }
49
50 pub fn note_rate_limited(&mut self, retry_after_ms: i64, now_ms: i64) {
53 self.banned_until_ms = Some(now_ms + retry_after_ms);
54 }
55
56 pub fn try_acquire(&mut self, weight: u32, now_ms: i64) -> Acquire {
58 if let Some(until) = self.banned_until_ms {
60 if now_ms < until {
61 return Acquire::Throttled {
62 retry_after_ms: until - now_ms,
63 };
64 }
65 self.banned_until_ms = None;
66 }
67
68 let start = match self.window_start_ms {
70 Some(start) if now_ms < start + self.window_ms => start,
71 _ => {
72 self.window_start_ms = Some(now_ms);
73 self.used = 0;
74 now_ms
75 }
76 };
77
78 if self.used + weight > self.capacity {
79 return Acquire::Throttled {
80 retry_after_ms: start + self.window_ms - now_ms,
81 };
82 }
83 self.used += weight;
84 Acquire::Allowed
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn charges_weight_until_the_budget_is_spent() {
94 let mut rl = WeightedRateLimiter::new(100, 60_000);
95 assert_eq!(rl.try_acquire(40, 1_000), Acquire::Allowed);
96 assert_eq!(rl.used(), 40);
97 assert_eq!(rl.try_acquire(40, 1_500), Acquire::Allowed);
98 assert_eq!(rl.used(), 80);
99 assert_eq!(
101 rl.try_acquire(30, 2_000),
102 Acquire::Throttled {
103 retry_after_ms: 1_000 + 60_000 - 2_000
104 }
105 );
106 }
107
108 #[test]
109 fn window_rolls_and_resets_budget() {
110 let mut rl = WeightedRateLimiter::new(100, 60_000);
111 assert_eq!(rl.try_acquire(100, 0), Acquire::Allowed);
112 assert!(matches!(
114 rl.try_acquire(1, 30_000),
115 Acquire::Throttled { .. }
116 ));
117 assert_eq!(rl.try_acquire(50, 60_000), Acquire::Allowed);
119 assert_eq!(rl.used(), 50);
120 }
121
122 #[test]
123 fn venue_cool_off_blocks_until_it_expires() {
124 let mut rl = WeightedRateLimiter::new(100, 60_000);
125 rl.note_rate_limited(5_000, 1_000);
126 assert_eq!(
127 rl.try_acquire(1, 2_000),
128 Acquire::Throttled {
129 retry_after_ms: 4_000
130 }
131 );
132 assert_eq!(rl.try_acquire(1, 6_000), Acquire::Allowed);
134 }
135}