Skip to main content

ractor/factory/
ratelim.rs

1// Copyright (c) Sean Lawlor
2//
3// This source code is licensed under both the MIT license found in the
4// LICENSE-MIT file in the root directory of this source tree.
5
6//! Rate limiting protocols for factory routers
7
8use 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
22/// The maximum supported balance for leaky bucket rate limiting.
23pub const MAX_LB_BALANCE: usize = isize::MAX as usize;
24
25/// A basic trait which allows controlling rate limiting of message routing
26pub trait RateLimiter: State {
27    /// Check if we have not violated the rate limiter
28    ///
29    /// Returns [false] if we're in violation and should start rate-limiting traffic
30    /// [true] otherwise
31    fn check(&mut self) -> bool;
32
33    /// Bump the rate limit internal counter, as we've routed a message
34    /// to a worker
35    fn bump(&mut self);
36}
37
38/// A generic struct which wraps the message router and adds support for a rate-limiting implementation to rate limit
39/// jobs processed by the factory. This handles the plubming around wrapping a rate limited message router
40#[derive(Debug, bon::Builder)]
41pub struct RateLimitedRouter<TRouter, TRateLimit> {
42    /// The underlying message router which does NOT implement rate limiting
43    pub router: TRouter,
44    /// The rate limiter to apply to the message routing
45    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                // only bump the internal state if we successfully routed a message
70                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/// A basic leaky-bucket rate limiter. This is a synchronous implementation
97/// with no interior locking since it's only used by the [RateLimitedRouter]
98/// uniquely and doesn't share its state
99#[derive(Debug)]
100pub struct LeakyBucketRateLimiter {
101    /// Tokens to add every `per` duration.
102    pub refill: usize,
103    /// Interval in milliseconds to add tokens.
104    pub interval: Duration,
105    /// Max number of tokens associated with the rate limiter.
106    pub max: usize,
107    /// The "balance" of the rate limiter, i.e. the number of tokens still available
108    pub balance: usize,
109    /// The deadline to perform another refill
110    deadline: Instant,
111}
112
113#[bon::bon]
114impl LeakyBucketRateLimiter {
115    /// Create a new [LeakyBucketRateLimiter] instance
116    ///
117    /// * `refill` - Tokens to add every `per` duration.
118    /// * `interval` - Interval to add tokens.
119    /// * `max` - The maximum number of tokens associated with the rate limiter. Default = [MAX_LB_BALANCE]
120    /// * `initial` - The initial starting balance. If [None] will be = to max
121    ///
122    /// Returns a new [LeakyBucketRateLimiter] instance
123    #[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        // Time elapsed in milliseconds since the last deadline.
145        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}