1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Mutex;
4use std::task::{Context, Poll};
5use std::time::{Duration, Instant};
6
7use tower::Service;
8
9use camel_api::CAMEL_STOP;
10use camel_api::{BoxProcessor, CamelError, Exchange, ThrottleStrategy, ThrottlerConfig, Value};
11
12pub struct RateLimiter {
13 tokens: f64,
14 max_tokens: f64,
15 refill_rate: f64,
16 last_refill: Instant,
17}
18
19impl RateLimiter {
20 fn new(max_requests: usize, period: Duration) -> Self {
21 let refill_rate = max_requests as f64 / period.as_secs_f64();
22 Self {
23 tokens: max_requests as f64,
24 max_tokens: max_requests as f64,
25 refill_rate,
26 last_refill: Instant::now(),
27 }
28 }
29
30 fn try_acquire(&mut self) -> bool {
31 let now = Instant::now();
32 let elapsed = now.duration_since(self.last_refill).as_secs_f64();
33 if elapsed > 0.0 {
34 self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max_tokens);
35 self.last_refill = now;
36 }
37 if self.tokens >= 1.0 {
38 self.tokens -= 1.0;
39 true
40 } else {
41 false
42 }
43 }
44
45 fn time_until_next_token(&self) -> Duration {
46 if self.tokens >= 1.0 {
47 Duration::ZERO
48 } else {
49 let tokens_needed = 1.0 - self.tokens;
50 Duration::from_secs_f64(tokens_needed / self.refill_rate)
51 }
52 }
53}
54
55#[derive(Clone)]
56pub struct ThrottlerService {
57 config: ThrottlerConfig,
58 limiter: std::sync::Arc<Mutex<RateLimiter>>,
59 next: BoxProcessor,
60}
61
62impl ThrottlerService {
63 pub fn new(config: ThrottlerConfig, next: BoxProcessor) -> Self {
66 Self::try_new(config, next).expect("ThrottlerService::new invariants violated") }
70
71 pub fn try_new(config: ThrottlerConfig, next: BoxProcessor) -> Result<Self, CamelError> {
78 if config.period == Duration::ZERO {
79 return Err(CamelError::Config(
80 "ThrottlerConfig.period must be > 0".to_string(),
81 ));
82 }
83 if config.max_requests == 0 {
84 return Err(CamelError::Config(
85 "ThrottlerConfig.max_requests must be > 0".to_string(),
86 ));
87 }
88 let limiter = RateLimiter::new(config.max_requests, config.period);
89 Ok(Self {
90 config,
91 limiter: std::sync::Arc::new(Mutex::new(limiter)),
92 next,
93 })
94 }
95}
96
97impl Service<Exchange> for ThrottlerService {
98 type Response = Exchange;
99 type Error = CamelError;
100 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
101
102 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
103 self.next.poll_ready(cx)
104 }
105
106 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
107 let config = self.config.clone();
108 let limiter = self.limiter.clone();
109 let mut next = self.next.clone();
110
111 Box::pin(async move {
112 let acquired = {
113 let mut limiter = limiter.lock().unwrap(); limiter.try_acquire()
115 };
116
117 if acquired {
118 next.call(exchange).await
119 } else {
120 match config.strategy {
121 ThrottleStrategy::Delay => {
122 loop {
123 let wait_time = {
124 let limiter = limiter.lock().unwrap(); limiter.time_until_next_token()
126 };
127 if wait_time > Duration::ZERO {
128 tokio::time::sleep(wait_time).await;
129 }
130 let acquired = {
131 let mut limiter = limiter.lock().unwrap(); limiter.try_acquire()
133 };
134 if acquired {
135 break;
136 }
137 tokio::task::yield_now().await;
140 }
141 next.call(exchange).await
142 }
143 ThrottleStrategy::Reject => Err(CamelError::ProcessorError(
144 "Throttled: rate limit exceeded".to_string(),
145 )),
146 ThrottleStrategy::Drop => {
147 exchange.set_property(CAMEL_STOP, Value::Bool(true));
148 Ok(exchange)
149 }
150 }
151 }
152 })
153 }
154}
155
156pub struct ThrottleSegment {
163 pub config: ThrottlerConfig,
164 pub limiter: std::sync::Arc<std::sync::Mutex<RateLimiter>>,
165 pub body: camel_api::OutcomeSegment,
166}
167
168impl ThrottleSegment {
169 pub fn new(config: ThrottlerConfig, body: camel_api::OutcomeSegment) -> Self {
172 Self::try_new(config, body).expect("ThrottleSegment::new invariants violated") }
176
177 pub fn try_new(
179 config: ThrottlerConfig,
180 body: camel_api::OutcomeSegment,
181 ) -> Result<Self, CamelError> {
182 if config.period == Duration::ZERO {
183 return Err(CamelError::Config(
184 "ThrottlerConfig.period must be > 0".to_string(),
185 ));
186 }
187 if config.max_requests == 0 {
188 return Err(CamelError::Config(
189 "ThrottlerConfig.max_requests must be > 0".to_string(),
190 ));
191 }
192 Ok(Self {
193 limiter: std::sync::Arc::new(std::sync::Mutex::new(RateLimiter::new(
194 config.max_requests,
195 config.period,
196 ))),
197 config,
198 body,
199 })
200 }
201}
202
203impl Clone for ThrottleSegment {
204 fn clone(&self) -> Self {
205 Self {
206 config: self.config.clone(),
207 limiter: std::sync::Arc::clone(&self.limiter),
208 body: self.body.clone(),
209 }
210 }
211}
212
213impl camel_api::OutcomePipeline for ThrottleSegment {
214 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
215 Box::new(self.clone())
216 }
217
218 fn run<'a>(
219 &'a mut self,
220 exchange: camel_api::Exchange,
221 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
222 Box::pin(async move {
223 let acquired = {
224 let mut limiter = self.limiter.lock().unwrap(); limiter.try_acquire()
226 };
227 if acquired {
228 return self.body.run(exchange).await;
229 }
230 match self.config.strategy {
231 ThrottleStrategy::Delay => {
232 loop {
233 let wait_time = {
234 let limiter = self.limiter.lock().unwrap(); limiter.time_until_next_token()
236 };
237 if wait_time > Duration::ZERO {
238 tokio::time::sleep(wait_time).await;
239 }
240 let acquired = {
241 let mut limiter = self.limiter.lock().unwrap(); limiter.try_acquire()
243 };
244 if acquired {
245 break;
246 }
247 tokio::task::yield_now().await;
248 }
249 self.body.run(exchange).await
250 }
251 ThrottleStrategy::Reject => {
252 camel_api::PipelineOutcome::Failed(camel_api::CamelError::ProcessorError(
253 "Throttled: rate limit exceeded".to_string(),
254 ))
255 }
256 ThrottleStrategy::Drop => {
257 let mut ex = exchange;
258 ex.set_property(CAMEL_STOP, camel_api::Value::Bool(true));
259 camel_api::PipelineOutcome::Stopped(ex)
260 }
261 }
262 })
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use camel_api::{BoxProcessorExt, Message};
270 use tower::ServiceExt;
271
272 fn passthrough() -> BoxProcessor {
273 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
274 }
275
276 #[test]
286 fn test_throttler_zero_period_returns_err() {
287 let config = ThrottlerConfig::new(5, Duration::ZERO);
288 let err = ThrottlerService::try_new(config, passthrough())
289 .err()
290 .expect("zero period must be Err from try_new, not Ok");
291 let msg = err.to_string();
292 assert!(
293 msg.contains("period") && msg.contains("> 0"),
294 "error must mention period > 0: {msg}"
295 );
296 }
297
298 #[tokio::test]
299 async fn test_throttler_allows_under_limit() {
300 let config = ThrottlerConfig::new(5, Duration::from_secs(1));
301 let mut svc = ThrottlerService::new(config, passthrough());
302
303 for _ in 0..5 {
304 let ex = Exchange::new(Message::new("test"));
305 let result = svc.ready().await.unwrap().call(ex).await;
306 assert!(result.is_ok());
307 }
308 }
309
310 #[tokio::test]
311 async fn test_throttler_delay_strategy_queues_message() {
312 let config = ThrottlerConfig::new(1, Duration::from_millis(100));
313 let mut svc = ThrottlerService::new(config, passthrough());
314
315 let ex1 = Exchange::new(Message::new("first"));
316 let result1 = svc.ready().await.unwrap().call(ex1).await;
317 assert!(result1.is_ok());
318
319 let start = Instant::now();
320 let ex2 = Exchange::new(Message::new("second"));
321 let result2 = svc.ready().await.unwrap().call(ex2).await;
322 let elapsed = start.elapsed();
323 assert!(result2.is_ok());
324 assert!(elapsed >= Duration::from_millis(50));
325 }
326
327 #[tokio::test]
328 async fn test_throttler_reject_strategy_returns_error() {
329 let config =
330 ThrottlerConfig::new(1, Duration::from_secs(10)).strategy(ThrottleStrategy::Reject);
331 let mut svc = ThrottlerService::new(config, passthrough());
332
333 let ex1 = Exchange::new(Message::new("first"));
334 let _ = svc.ready().await.unwrap().call(ex1).await;
335
336 let ex2 = Exchange::new(Message::new("second"));
337 let result = svc.ready().await.unwrap().call(ex2).await;
338 assert!(result.is_err());
339 let err = result.unwrap_err().to_string();
340 assert!(err.contains("Throttled"));
341 }
342
343 #[tokio::test]
344 async fn test_throttler_drop_strategy_sets_camel_stop() {
345 let config =
346 ThrottlerConfig::new(1, Duration::from_secs(10)).strategy(ThrottleStrategy::Drop);
347 let mut svc = ThrottlerService::new(config, passthrough());
348
349 let ex1 = Exchange::new(Message::new("first"));
350 let _ = svc.ready().await.unwrap().call(ex1).await;
351
352 let ex2 = Exchange::new(Message::new("second"));
353 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
354 assert_eq!(result.property(CAMEL_STOP), Some(&Value::Bool(true)));
355 }
356
357 #[tokio::test]
358 async fn test_throttler_token_replenishment() {
359 let config = ThrottlerConfig::new(1, Duration::from_millis(50));
360 let mut svc = ThrottlerService::new(config, passthrough());
361
362 let ex1 = Exchange::new(Message::new("first"));
363 let _ = svc.ready().await.unwrap().call(ex1).await;
364
365 tokio::time::sleep(Duration::from_millis(100)).await;
366
367 let ex2 = Exchange::new(Message::new("second"));
368 let result = svc.ready().await.unwrap().call(ex2).await;
369 assert!(result.is_ok());
370 }
371
372 #[test]
380 fn test_throttler_zero_max_requests_returns_err() {
381 let config = ThrottlerConfig::new(0, Duration::from_secs(1));
388 let result = std::panic::catch_unwind(|| ThrottlerService::try_new(config, passthrough()));
392 match result {
393 Ok(Ok(_)) => panic!("zero max_requests must be Err, not Ok"),
394 Ok(Err(e)) => {
395 let msg = e.to_string();
396 assert!(
397 msg.contains("max_requests") && msg.contains("> 0"),
398 "error must mention max_requests > 0: {msg}"
399 );
400 }
401 Err(_) => panic!("zero max_requests must return Err, not panic"),
402 }
403 }
404
405 #[tokio::test]
408 async fn throttle_segment_reject_strategy_returns_failed() {
409 use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
410
411 #[derive(Clone)]
412 struct NoopSeg;
413 impl OutcomePipeline for NoopSeg {
414 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
415 Box::new(NoopSeg)
416 }
417 fn run<'a>(
418 &'a mut self,
419 ex: Exchange,
420 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
421 Box::pin(async move { PipelineOutcome::Completed(ex) })
422 }
423 }
424
425 let config = ThrottlerConfig {
429 max_requests: 1,
430 period: Duration::from_secs(1),
431 strategy: ThrottleStrategy::Reject,
432 };
433 let body = camel_api::OutcomeSegment::new(Box::new(NoopSeg));
434 let mut seg = ThrottleSegment::new(config, body);
435 let ex = Exchange::new(Message::new("test"));
436 let _first = seg.run(ex).await;
438 let ex2 = Exchange::new(Message::new("test2"));
440 let outcome = seg.run(ex2).await;
441 assert!(
442 matches!(outcome, PipelineOutcome::Failed(_)),
443 "Reject strategy must return Failed when tokens exhausted"
444 );
445 }
446
447 #[tokio::test]
448 async fn throttle_segment_drop_strategy_returns_stopped() {
449 use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
450
451 #[derive(Clone)]
452 struct NoopSeg;
453 impl OutcomePipeline for NoopSeg {
454 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
455 Box::new(NoopSeg)
456 }
457 fn run<'a>(
458 &'a mut self,
459 ex: Exchange,
460 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
461 Box::pin(async move { PipelineOutcome::Completed(ex) })
462 }
463 }
464
465 let config = ThrottlerConfig {
468 max_requests: 1,
469 period: Duration::from_secs(1),
470 strategy: ThrottleStrategy::Drop,
471 };
472 let body = camel_api::OutcomeSegment::new(Box::new(NoopSeg));
473 let mut seg = ThrottleSegment::new(config, body);
474 let ex = Exchange::new(Message::new("test"));
475 let _first = seg.run(ex).await;
477 let ex2 = Exchange::new(Message::new("test2"));
479 let outcome = seg.run(ex2).await;
480 match outcome {
481 PipelineOutcome::Stopped(returned_ex) => {
482 let stopped_flag = returned_ex.property(CAMEL_STOP).and_then(|v| v.as_bool());
483 assert_eq!(
484 stopped_flag,
485 Some(true),
486 "Drop strategy must set CamelStop=true property"
487 );
488 }
489 other => panic!("Drop must return Stopped, got {:?}", other),
490 }
491 }
492
493 #[tokio::test]
494 async fn throttle_segment_delay_strategy_propagates_stopped_body() {
495 use camel_api::{Body, Exchange, Message, OutcomePipeline, PipelineOutcome};
496
497 #[derive(Clone)]
498 struct StoppingSeg;
499 impl OutcomePipeline for StoppingSeg {
500 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
501 Box::new(StoppingSeg)
502 }
503 fn run<'a>(
504 &'a mut self,
505 mut ex: Exchange,
506 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
507 Box::pin(async move {
508 ex.input.body = Body::Bytes(b"stopped-mut".to_vec().into());
509 PipelineOutcome::Stopped(ex)
510 })
511 }
512 }
513
514 let config = ThrottlerConfig {
515 max_requests: 1, period: Duration::from_secs(1),
517 strategy: ThrottleStrategy::Delay,
518 };
519 let body = camel_api::OutcomeSegment::new(Box::new(StoppingSeg));
520 let mut seg = ThrottleSegment::new(config, body);
521 let ex = Exchange::new(Message::new("test"));
522 let outcome = seg.run(ex).await;
523 match outcome {
524 PipelineOutcome::Stopped(returned_ex) => {
525 if let Body::Bytes(b) = &returned_ex.input.body {
526 assert_eq!(
527 b.as_ref(),
528 b"stopped-mut",
529 "BUG: throttle body Stop must preserve mutations"
530 );
531 } else {
532 panic!("expected Body::Bytes");
533 }
534 }
535 other => panic!("expected Stopped propagation, got {:?}", other),
536 }
537 }
538}