1use std::sync::atomic::{AtomicUsize, Ordering};
24use std::sync::Arc;
25use std::time::Duration;
26
27use tokio::sync::{OwnedSemaphorePermit, Semaphore};
28
29use super::RouterError;
30
31const DEFAULT_WAIT_TIMEOUT: Duration = Duration::from_secs(60);
33
34#[derive(Debug, Clone)]
42pub struct ModelRateLimit {
43 pub(crate) requests_per_window: usize,
45 pub(crate) window: Duration,
47 pub(crate) max_concurrent: usize,
49 pub(crate) max_queue: usize,
51 pub(crate) wait_timeout: Duration,
53}
54
55impl ModelRateLimit {
56 pub fn per_minute(requests_per_minute: usize) -> Self {
59 Self {
60 requests_per_window: requests_per_minute,
61 window: Duration::from_secs(60),
62 max_concurrent: 0,
63 max_queue: 0,
64 wait_timeout: DEFAULT_WAIT_TIMEOUT,
65 }
66 }
67
68 pub fn per_second(requests_per_second: usize) -> Self {
70 Self {
71 requests_per_window: requests_per_second,
72 window: Duration::from_secs(1),
73 max_concurrent: 0,
74 max_queue: 0,
75 wait_timeout: Duration::from_secs(1),
76 }
77 }
78
79 pub fn with_window(mut self, window: Duration) -> Self {
84 self.window = window;
85 self
86 }
87
88 pub fn with_max_concurrent(mut self, max_concurrent: usize) -> Self {
92 self.max_concurrent = max_concurrent;
93 self
94 }
95
96 pub fn with_max_queue(mut self, max_queue: usize) -> Self {
101 self.max_queue = max_queue;
102 self
103 }
104
105 pub fn with_wait_timeout(mut self, wait_timeout: Duration) -> Self {
108 self.wait_timeout = wait_timeout;
109 self
110 }
111
112 pub fn requests_per_window(&self) -> usize {
114 self.requests_per_window
115 }
116
117 pub fn window(&self) -> Duration {
119 self.window
120 }
121
122 pub fn max_concurrent(&self) -> usize {
124 self.max_concurrent
125 }
126
127 pub fn max_queue(&self) -> usize {
129 self.max_queue
130 }
131
132 pub fn wait_timeout(&self) -> Duration {
134 self.wait_timeout
135 }
136}
137
138pub(super) struct ModelGate {
141 rate: Option<RateGate>,
143 concurrency: Arc<Semaphore>,
145 wait_timeout: Duration,
148}
149
150impl ModelGate {
151 pub(super) fn new(config: &ModelRateLimit) -> Arc<Self> {
152 let concurrency = if config.max_concurrent == 0 {
153 Semaphore::MAX_PERMITS
154 } else {
155 config.max_concurrent
156 };
157 Arc::new(Self {
158 rate: if config.requests_per_window > 0 {
159 Some(RateGate::new(config))
160 } else {
161 None
162 },
163 concurrency: Arc::new(Semaphore::new(concurrency)),
164 wait_timeout: config.wait_timeout,
165 })
166 }
167
168 pub(super) async fn acquire(&self, model: &str) -> Result<GatePermit, RouterError> {
176 if let Some(rate) = &self.rate {
177 rate.acquire(model).await?;
178 }
179 let acquired =
183 tokio::time::timeout(self.wait_timeout, self.concurrency.clone().acquire_owned()).await;
184 let concurrency = match acquired {
185 Ok(Ok(permit)) => permit,
186 Ok(Err(_closed)) => panic!("router concurrency semaphore is never closed"),
187 Err(_elapsed) => {
188 return Err(RouterError::RateLimited {
189 model: model.to_string(),
190 reason: RateLimitReason::Timeout {
191 waited: self.wait_timeout,
192 },
193 });
194 }
195 };
196 Ok(GatePermit {
197 _concurrency: concurrency,
198 })
199 }
200}
201
202pub(super) struct GatePermit {
204 _concurrency: OwnedSemaphorePermit,
205}
206
207impl std::fmt::Debug for GatePermit {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.debug_struct("GatePermit").finish_non_exhaustive()
210 }
211}
212
213struct RateGate {
215 semaphore: Arc<Semaphore>,
217 window: Duration,
218 queued: AtomicUsize,
220 max_queue: usize,
221 wait_timeout: Duration,
222}
223
224impl RateGate {
225 fn new(config: &ModelRateLimit) -> Self {
226 Self {
227 semaphore: Arc::new(Semaphore::new(config.requests_per_window)),
228 window: config.window,
229 queued: AtomicUsize::new(0),
230 max_queue: config.max_queue,
231 wait_timeout: config.wait_timeout,
232 }
233 }
234
235 async fn acquire(&self, model: &str) -> Result<(), RouterError> {
236 let position = self.queued.fetch_add(1, Ordering::AcqRel);
239 if self.max_queue > 0 && position >= self.max_queue {
240 self.queued.fetch_sub(1, Ordering::AcqRel);
241 return Err(RouterError::RateLimited {
242 model: model.to_string(),
243 reason: RateLimitReason::QueueFull {
244 max_queue: self.max_queue,
245 },
246 });
247 }
248
249 let acquired =
250 tokio::time::timeout(self.wait_timeout, self.semaphore.clone().acquire_owned()).await;
251 self.queued.fetch_sub(1, Ordering::AcqRel);
252
253 let permit = match acquired {
254 Ok(Ok(permit)) => permit,
255 Ok(Err(_closed)) => panic!("router rate semaphore is never closed"),
256 Err(_elapsed) => {
257 return Err(RouterError::RateLimited {
258 model: model.to_string(),
259 reason: RateLimitReason::Timeout {
260 waited: self.wait_timeout,
261 },
262 });
263 }
264 };
265
266 let window = self.window;
270 tokio::spawn(async move {
271 tokio::time::sleep(window).await;
272 drop(permit);
273 });
274 Ok(())
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum RateLimitReason {
281 QueueFull {
283 max_queue: usize,
285 },
286 Timeout {
288 waited: Duration,
290 },
291}
292
293impl std::fmt::Display for RateLimitReason {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 match self {
296 RateLimitReason::QueueFull { max_queue } => {
297 write!(f, "admission queue full (max_queue={max_queue})")
298 }
299 RateLimitReason::Timeout { waited } => {
300 write!(f, "no permit within {} ms", waited.as_millis())
301 }
302 }
303 }
304}
305
306impl std::error::Error for RateLimitReason {}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 fn gate(rl: &ModelRateLimit) -> Arc<ModelGate> {
313 ModelGate::new(rl)
314 }
315
316 #[tokio::test(start_paused = true)]
317 async fn admits_burst_then_queues_fifo_across_window() {
318 let rl = ModelRateLimit::per_second(1)
321 .with_window(Duration::from_millis(100))
322 .with_max_concurrent(1);
323 let g = gate(&rl);
324
325 let finished = Arc::new(tokio::sync::Mutex::new(Vec::new()));
326 let mut handles = Vec::new();
327 for id in 0..3u8 {
328 let g = g.clone();
329 let finished = finished.clone();
330 handles.push(tokio::spawn(async move {
331 let _permit = g.acquire("m").await.unwrap();
332 tokio::time::sleep(Duration::from_millis(10)).await;
334 finished.lock().await.push(id);
335 }));
336 }
337 for h in handles {
338 h.await.unwrap();
339 }
340 assert_eq!(*finished.lock().await, vec![0, 1, 2]);
343 }
344
345 #[tokio::test(start_paused = true)]
346 async fn full_queue_rejects_instead_of_waiting() {
347 let rl = ModelRateLimit::per_minute(1).with_max_queue(1);
348 let g = gate(&rl);
349
350 let _p0 = g.acquire("m").await.unwrap();
351 let g1 = g.clone();
353 let waiter = tokio::spawn(async move { g1.acquire("m").await });
354 tokio::task::yield_now().await;
356 tokio::task::yield_now().await;
357 let err = g.acquire("m").await.unwrap_err();
360 assert!(
361 matches!(
362 err,
363 RouterError::RateLimited {
364 reason: RateLimitReason::QueueFull { max_queue: 1 },
365 ..
366 }
367 ),
368 "got {err:?}"
369 );
370 tokio::time::sleep(Duration::from_secs(60)).await;
373 assert!(waiter.await.unwrap().is_ok());
374 }
375
376 #[tokio::test(start_paused = true)]
377 async fn wait_timeout_falls_through() {
378 let rl = ModelRateLimit::per_minute(1).with_wait_timeout(Duration::from_secs(5));
379 let g = gate(&rl);
380 let _p0 = g.acquire("m").await.unwrap();
381 let started = tokio::time::Instant::now();
382 let err = g.acquire("primary").await.unwrap_err();
383 assert_eq!(started.elapsed(), Duration::from_secs(5));
384 assert!(
385 matches!(
386 err,
387 RouterError::RateLimited {
388 reason: RateLimitReason::Timeout { .. },
389 ..
390 }
391 ),
392 "got {err:?}"
393 );
394 }
395
396 #[tokio::test(start_paused = true)]
397 async fn permit_returns_after_window_and_keeps_rate_stable() {
398 let rl = ModelRateLimit::per_second(2).with_window(Duration::from_millis(100));
400 let g = gate(&rl);
401 let _p1 = g.acquire("m").await.unwrap();
402 let _p2 = g.acquire("m").await.unwrap();
403 let g2 = g.clone();
405 let h = tokio::spawn(async move { g2.acquire("m").await });
406 tokio::time::sleep(Duration::from_millis(101)).await;
407 assert!(h.await.unwrap().is_ok());
408 }
409
410 #[test]
411 fn disabled_dimensions_are_represented_as_zero() {
412 let rl = ModelRateLimit::per_minute(10);
413 assert_eq!(rl.max_concurrent(), 0);
414 assert_eq!(rl.max_queue(), 0);
415 assert_eq!(rl.requests_per_window(), 10);
416 let rl2 = rl
417 .with_window(Duration::from_secs(30))
418 .with_wait_timeout(Duration::from_secs(30));
419 assert_eq!(rl2.window(), Duration::from_secs(30));
422 assert_eq!(rl2.wait_timeout(), Duration::from_secs(30));
423 }
424}