ractor/factory/
ratelim.rs1use std::collections::HashMap;
9
10use crate::concurrency::Duration;
11use crate::concurrency::Instant;
12use crate::factory::routing::RouteResult;
13use crate::factory::routing::Router;
14use crate::factory::Job;
15use crate::factory::JobKey;
16use crate::factory::WorkerId;
17use crate::factory::WorkerProperties;
18use crate::ActorProcessingErr;
19use crate::Message;
20use crate::State;
21
22pub const MAX_LB_BALANCE: usize = isize::MAX as usize;
24
25pub trait RateLimiter: State {
27 fn check(&mut self) -> bool;
32
33 fn bump(&mut self);
36}
37
38#[derive(Debug, bon::Builder)]
41pub struct RateLimitedRouter<TRouter, TRateLimit> {
42 pub router: TRouter,
44 pub rate_limiter: TRateLimit,
46}
47
48impl<TKey, TMsg, TRouter, TRateLimit> Router<TKey, TMsg> for RateLimitedRouter<TRouter, TRateLimit>
49where
50 TKey: JobKey,
51 TMsg: Message,
52 TRouter: Router<TKey, TMsg>,
53 TRateLimit: RateLimiter,
54{
55 fn route_message(
56 &mut self,
57 job: Job<TKey, TMsg>,
58 pool_size: usize,
59 worker_hint: Option<WorkerId>,
60 worker_pool: &mut HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
61 ) -> Result<RouteResult<TKey, TMsg>, ActorProcessingErr> {
62 if !self.rate_limiter.check() {
63 Ok(RouteResult::RateLimited(job))
64 } else {
65 let result = self
66 .router
67 .route_message(job, pool_size, worker_hint, worker_pool);
68 if matches!(result, Ok(RouteResult::Handled)) {
69 self.rate_limiter.bump();
71 }
72 result
73 }
74 }
75
76 fn choose_target_worker(
77 &mut self,
78 job: &Job<TKey, TMsg>,
79 pool_size: usize,
80 worker_hint: Option<WorkerId>,
81 worker_pool: &HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
82 ) -> Option<WorkerId> {
83 self.router
84 .choose_target_worker(job, pool_size, worker_hint, worker_pool)
85 }
86
87 fn is_factory_queueing(&self) -> bool {
88 self.router.is_factory_queueing()
89 }
90
91 fn on_worker_availability_change(&mut self, wid: WorkerId, available: bool) {
92 self.router.on_worker_availability_change(wid, available);
93 }
94}
95
96#[derive(Debug)]
100pub struct LeakyBucketRateLimiter {
101 pub refill: usize,
103 pub interval: Duration,
105 pub max: usize,
107 pub balance: usize,
109 deadline: Instant,
111}
112
113#[bon::bon]
114impl LeakyBucketRateLimiter {
115 #[builder]
124 pub fn new(
125 refill: usize,
126 interval: Duration,
127 #[builder(default = MAX_LB_BALANCE)] max: usize,
128 initial: Option<usize>,
129 ) -> LeakyBucketRateLimiter {
130 LeakyBucketRateLimiter {
131 refill,
132 interval,
133 max,
134 balance: initial.unwrap_or(max),
135 deadline: Instant::now() + interval,
136 }
137 }
138
139 fn refresh(&mut self, now: Instant) {
140 if now < self.deadline {
141 return;
142 }
143
144 let millis = self.interval.as_millis();
146 let since = now.saturating_duration_since(self.deadline).as_millis();
147
148 let periods = usize::try_from(since / millis + 1).unwrap_or(usize::MAX);
149
150 let tokens = periods
151 .checked_mul(self.refill)
152 .unwrap_or(MAX_LB_BALANCE)
153 .min(MAX_LB_BALANCE);
154
155 let remaining_millis_until_next_deadline =
156 u64::try_from(since % millis).unwrap_or(u64::MAX);
157 self.deadline = now
158 + self
159 .interval
160 .saturating_sub(Duration::from_millis(remaining_millis_until_next_deadline));
161 self.balance = (self.balance + tokens).min(self.max);
162 }
163}
164
165impl RateLimiter for LeakyBucketRateLimiter {
166 fn check(&mut self) -> bool {
167 let now = Instant::now();
168 self.refresh(now);
169 self.balance > 0
170 }
171
172 fn bump(&mut self) {
173 if self.balance > 0 {
174 self.balance -= 1;
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::concurrency::sleep;
183
184 #[crate::concurrency::test]
185 async fn test_basic_leaky_bucket() {
186 let mut limiter = LeakyBucketRateLimiter::builder()
187 .refill(1)
188 .initial(1)
189 .interval(Duration::from_millis(100))
190 .build();
191
192 assert!(limiter.check());
193 limiter.bump();
194 assert!(!limiter.check());
195
196 sleep(limiter.interval * 2).await;
197
198 assert!(limiter.check());
199 limiter.bump();
200 assert!(limiter.check());
201 limiter.bump();
202 assert!(!limiter.check());
203 }
204
205 #[crate::concurrency::test]
206 async fn test_leaky_bucket_max() {
207 let mut limiter = LeakyBucketRateLimiter::builder()
208 .refill(1)
209 .initial(1)
210 .max(1)
211 .interval(Duration::from_millis(100))
212 .build();
213
214 assert!(limiter.check());
215 limiter.bump();
216 assert!(!limiter.check());
217
218 sleep(limiter.interval * 2).await;
219
220 assert!(limiter.check());
221 limiter.bump();
222 assert!(!limiter.check());
223 }
224}