rskit_resilience/
rate_limiter.rs1use std::num::NonZeroU32;
2use std::sync::Arc;
3
4use governor::{DefaultDirectRateLimiter, Quota, RateLimiter as GovRateLimiter};
5use rskit_errors::{AppError, AppResult};
6use tokio_util::sync::CancellationToken;
7
8#[derive(Debug, Clone)]
10pub struct RateLimiterConfig {
11 pub name: String,
13 pub per_second: u32,
15 pub burst: u32,
17}
18
19impl RateLimiterConfig {
20 #[must_use]
22 pub fn new(name: impl Into<String>, per_second: u32, burst: u32) -> Self {
23 Self {
24 name: name.into(),
25 per_second,
26 burst,
27 }
28 }
29
30 #[must_use]
32 pub fn with_per_second(mut self, per_second: u32) -> Self {
33 self.per_second = per_second;
34 self
35 }
36
37 #[must_use]
39 pub fn with_burst(mut self, burst: u32) -> Self {
40 self.burst = burst;
41 self
42 }
43
44 pub fn validate(&self) -> AppResult<()> {
46 if self.per_second == 0 {
47 return Err(AppError::invalid_input(
48 "per_second",
49 "rate limit must be greater than zero",
50 ));
51 }
52 if self.burst == 0 {
53 return Err(AppError::invalid_input(
54 "burst",
55 "rate limit burst must be greater than zero",
56 ));
57 }
58 Ok(())
59 }
60}
61
62#[derive(Clone)]
64pub struct RateLimiter {
65 inner: Arc<DefaultDirectRateLimiter>,
66 name: String,
67}
68
69impl std::fmt::Debug for RateLimiter {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("RateLimiter")
72 .field("name", &self.name)
73 .finish()
74 }
75}
76
77impl RateLimiter {
78 pub fn new(name: impl Into<String>, per_second: u32, burst: u32) -> AppResult<Self> {
85 let config = RateLimiterConfig::new(name, per_second, burst);
86 Self::from_config(config)
87 }
88
89 pub fn from_config(config: RateLimiterConfig) -> AppResult<Self> {
95 let per_sec = non_zero(
96 "per_second",
97 config.per_second,
98 "rate limit must be greater than zero",
99 )?;
100 let burst_size = non_zero(
101 "burst",
102 config.burst,
103 "rate limit burst must be greater than zero",
104 )?;
105 let quota = Quota::per_second(per_sec).allow_burst(burst_size);
106 Ok(Self {
107 inner: Arc::new(GovRateLimiter::direct(quota)),
108 name: config.name,
109 })
110 }
111
112 pub fn check(&self) -> AppResult<()> {
119 self.inner
120 .check()
121 .map_err(|_| AppError::rate_limited().with_detail("rate_limiter", self.name.clone()))
122 }
123
124 pub async fn until_ready(&self, cancel: Option<CancellationToken>) -> AppResult<()> {
131 match cancel {
132 Some(token) => {
133 tokio::select! {
134 _ = self.inner.until_ready() => Ok(()),
135 _ = token.cancelled() => {
136 Err(AppError::service_unavailable("rate limiter cancelled"))
137 }
138 }
139 }
140 None => {
141 self.inner.until_ready().await;
142 Ok(())
143 }
144 }
145 }
146}
147
148fn non_zero(field: &'static str, value: u32, message: &'static str) -> AppResult<NonZeroU32> {
149 NonZeroU32::new(value).ok_or_else(|| AppError::invalid_input(field, message))
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[tokio::test]
157 async fn check_allows_up_to_burst_limit() {
158 let rl = RateLimiter::new("test", 1, 5).unwrap();
159 for _ in 0..5 {
160 assert!(rl.check().is_ok());
161 }
162 }
163
164 #[tokio::test]
165 async fn check_rejects_when_bucket_exhausted() {
166 let rl = RateLimiter::new("test", 1, 3).unwrap();
167 for _ in 0..3 {
168 let _ = rl.check();
169 }
170 let result = rl.check();
171 assert!(result.is_err());
172 }
173
174 #[tokio::test]
175 async fn check_returns_rate_limited_error_code() {
176 use rskit_errors::ErrorCode;
177 let rl = RateLimiter::new("test", 1, 1).unwrap();
178 let _ = rl.check();
179 let err = rl.check().unwrap_err();
180 assert_eq!(err.code(), ErrorCode::RateLimited);
181 }
182
183 #[tokio::test]
184 async fn until_ready_cancels_when_token_cancelled() {
185 let rl = RateLimiter::new("test", 1, 1).unwrap();
186 let _ = rl.check();
187
188 let cancel = CancellationToken::new();
189 let cancel_clone = cancel.clone();
190 cancel_clone.cancel();
191
192 let result = rl.until_ready(Some(cancel)).await;
193 assert!(result.is_err());
194 }
195
196 #[test]
197 fn from_config_builds_rate_limiter() {
198 let limiter = RateLimiter::from_config(RateLimiterConfig::new("cfg", 10, 2)).unwrap();
199 assert!(limiter.check().is_ok());
200 }
201
202 #[test]
203 fn from_config_rejects_zero_limits() {
204 assert!(RateLimiter::from_config(RateLimiterConfig::new("zero-rate", 0, 1)).is_err());
205 assert!(RateLimiter::from_config(RateLimiterConfig::new("zero-burst", 1, 0)).is_err());
206 }
207}