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> {
84 let config = RateLimiterConfig::new(name, per_second, burst);
85 Self::from_config(config)
86 }
87
88 pub fn from_config(config: RateLimiterConfig) -> AppResult<Self> {
94 let per_sec = non_zero(
95 "per_second",
96 config.per_second,
97 "rate limit must be greater than zero",
98 )?;
99 let burst_size = non_zero(
100 "burst",
101 config.burst,
102 "rate limit burst must be greater than zero",
103 )?;
104 let quota = Quota::per_second(per_sec).allow_burst(burst_size);
105 Ok(Self {
106 inner: Arc::new(GovRateLimiter::direct(quota)),
107 name: config.name,
108 })
109 }
110
111 pub fn check(&self) -> AppResult<()> {
118 self.inner
119 .check()
120 .map_err(|_| AppError::rate_limited().with_detail("rate_limiter", self.name.clone()))
121 }
122
123 pub async fn until_ready(&self, cancel: Option<CancellationToken>) -> AppResult<()> {
129 match cancel {
130 Some(token) => {
131 tokio::select! {
132 _ = self.inner.until_ready() => Ok(()),
133 _ = token.cancelled() => {
134 Err(AppError::service_unavailable("rate limiter cancelled"))
135 }
136 }
137 }
138 None => {
139 self.inner.until_ready().await;
140 Ok(())
141 }
142 }
143 }
144}
145
146fn non_zero(field: &'static str, value: u32, message: &'static str) -> AppResult<NonZeroU32> {
147 NonZeroU32::new(value).ok_or_else(|| AppError::invalid_input(field, message))
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[tokio::test]
155 async fn check_allows_up_to_burst_limit() {
156 let rl = RateLimiter::new("test", 1, 5).unwrap();
157 for _ in 0..5 {
158 assert!(rl.check().is_ok());
159 }
160 }
161
162 #[tokio::test]
163 async fn check_rejects_when_bucket_exhausted() {
164 let rl = RateLimiter::new("test", 1, 3).unwrap();
165 for _ in 0..3 {
166 let _ = rl.check();
167 }
168 let result = rl.check();
169 assert!(result.is_err());
170 }
171
172 #[tokio::test]
173 async fn check_returns_rate_limited_error_code() {
174 use rskit_errors::ErrorCode;
175 let rl = RateLimiter::new("test", 1, 1).unwrap();
176 let _ = rl.check();
177 let err = rl.check().unwrap_err();
178 assert_eq!(err.code(), ErrorCode::RateLimited);
179 }
180
181 #[tokio::test]
182 async fn until_ready_cancels_when_token_cancelled() {
183 let rl = RateLimiter::new("test", 1, 1).unwrap();
184 let _ = rl.check();
185
186 let cancel = CancellationToken::new();
187 let cancel_clone = cancel.clone();
188 cancel_clone.cancel();
189
190 let result = rl.until_ready(Some(cancel)).await;
191 assert!(result.is_err());
192 }
193
194 #[test]
195 fn from_config_builds_rate_limiter() {
196 let limiter = RateLimiter::from_config(RateLimiterConfig::new("cfg", 10, 2)).unwrap();
197 assert!(limiter.check().is_ok());
198 }
199
200 #[test]
201 fn from_config_rejects_zero_limits() {
202 assert!(RateLimiter::from_config(RateLimiterConfig::new("zero-rate", 0, 1)).is_err());
203 assert!(RateLimiter::from_config(RateLimiterConfig::new("zero-burst", 1, 0)).is_err());
204 }
205}