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