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::Reject => Err(CamelError::ProcessorError(
125 "Throttled: rate limit exceeded".to_string(),
126 )),
127 ThrottleStrategy::Drop => {
128 exchange.set_property(CAMEL_STOP, Value::Bool(true));
129 Ok(exchange)
130 }
131 _ => {
133 loop {
134 let wait_time = {
135 let limiter = limiter.lock().unwrap(); limiter.time_until_next_token()
137 };
138 if wait_time > Duration::ZERO {
139 tokio::time::sleep(wait_time).await;
140 }
141 let acquired = {
142 let mut limiter = limiter.lock().unwrap(); limiter.try_acquire()
144 };
145 if acquired {
146 break;
147 }
148 tokio::task::yield_now().await;
151 }
152 next.call(exchange).await
153 }
154 }
155 }
156 })
157 }
158}
159
160pub struct ThrottleSegment {
167 pub config: ThrottlerConfig,
168 pub limiter: std::sync::Arc<std::sync::Mutex<RateLimiter>>,
169 pub body: camel_api::OutcomeSegment,
170}
171
172impl ThrottleSegment {
173 pub fn new(config: ThrottlerConfig, body: camel_api::OutcomeSegment) -> Self {
176 Self::try_new(config, body).expect("ThrottleSegment::new invariants violated") }
180
181 pub fn try_new(
183 config: ThrottlerConfig,
184 body: camel_api::OutcomeSegment,
185 ) -> Result<Self, CamelError> {
186 if config.period == Duration::ZERO {
187 return Err(CamelError::Config(
188 "ThrottlerConfig.period must be > 0".to_string(),
189 ));
190 }
191 if config.max_requests == 0 {
192 return Err(CamelError::from(
193 ConfigValidationError::ThrottlerMaxRequestsZero,
194 ));
195 }
196 Ok(Self {
197 limiter: std::sync::Arc::new(std::sync::Mutex::new(RateLimiter::new(
198 config.max_requests,
199 config.period,
200 ))),
201 config,
202 body,
203 })
204 }
205}
206
207impl Clone for ThrottleSegment {
208 fn clone(&self) -> Self {
209 Self {
210 config: self.config.clone(),
211 limiter: std::sync::Arc::clone(&self.limiter),
212 body: self.body.clone(),
213 }
214 }
215}
216
217impl camel_api::OutcomePipeline for ThrottleSegment {
218 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
219 Box::new(self.clone())
220 }
221
222 fn run<'a>(
223 &'a mut self,
224 exchange: camel_api::Exchange,
225 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
226 Box::pin(async move {
227 let acquired = {
228 let mut limiter = self.limiter.lock().unwrap(); limiter.try_acquire()
230 };
231 if acquired {
232 return self.body.run(exchange).await;
233 }
234 match self.config.strategy {
235 ThrottleStrategy::Reject => {
236 camel_api::PipelineOutcome::Failed(camel_api::CamelError::ProcessorError(
237 "Throttled: rate limit exceeded".to_string(),
238 ))
239 }
240 ThrottleStrategy::Drop => {
241 let mut ex = exchange;
242 ex.set_property(CAMEL_STOP, camel_api::Value::Bool(true));
243 camel_api::PipelineOutcome::Stopped(ex)
244 }
245 _ => {
247 loop {
248 let wait_time = {
249 let limiter = self.limiter.lock().unwrap(); limiter.time_until_next_token()
251 };
252 if wait_time > Duration::ZERO {
253 tokio::time::sleep(wait_time).await;
254 }
255 let acquired = {
256 let mut limiter = self.limiter.lock().unwrap(); limiter.try_acquire()
258 };
259 if acquired {
260 break;
261 }
262 tokio::task::yield_now().await;
263 }
264 self.body.run(exchange).await
265 }
266 }
267 })
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use camel_api::{BoxProcessorExt, Message};
275 use tower::ServiceExt;
276
277 fn passthrough() -> BoxProcessor {
278 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
279 }
280
281 #[test]
291 fn test_throttler_zero_period_returns_err() {
292 let config = ThrottlerConfig::new(5, Duration::ZERO);
293 let err = ThrottlerService::try_new(config, passthrough())
294 .err()
295 .expect("zero period must be Err from try_new, not Ok");
296 let msg = err.to_string();
297 assert!(
298 msg.contains("period") && msg.contains("> 0"),
299 "error must mention period > 0: {msg}"
300 );
301 }
302
303 #[tokio::test]
304 async fn test_throttler_allows_under_limit() {
305 let config = ThrottlerConfig::new(5, Duration::from_secs(1));
306 let mut svc = ThrottlerService::new(config, passthrough());
307
308 for _ in 0..5 {
309 let ex = Exchange::new(Message::new("test"));
310 let result = svc.ready().await.unwrap().call(ex).await;
311 assert!(result.is_ok());
312 }
313 }
314
315 #[tokio::test]
316 async fn test_throttler_delay_strategy_queues_message() {
317 let config = ThrottlerConfig::new(1, Duration::from_millis(100));
318 let mut svc = ThrottlerService::new(config, passthrough());
319
320 let ex1 = Exchange::new(Message::new("first"));
321 let result1 = svc.ready().await.unwrap().call(ex1).await;
322 assert!(result1.is_ok());
323
324 let start = Instant::now();
325 let ex2 = Exchange::new(Message::new("second"));
326 let result2 = svc.ready().await.unwrap().call(ex2).await;
327 let elapsed = start.elapsed();
328 assert!(result2.is_ok());
329 assert!(elapsed >= Duration::from_millis(50));
330 }
331
332 #[tokio::test]
333 async fn test_throttler_reject_strategy_returns_error() {
334 let config =
335 ThrottlerConfig::new(1, Duration::from_secs(10)).strategy(ThrottleStrategy::Reject);
336 let mut svc = ThrottlerService::new(config, passthrough());
337
338 let ex1 = Exchange::new(Message::new("first"));
339 let _ = svc.ready().await.unwrap().call(ex1).await;
340
341 let ex2 = Exchange::new(Message::new("second"));
342 let result = svc.ready().await.unwrap().call(ex2).await;
343 assert!(result.is_err());
344 let err = result.unwrap_err().to_string();
345 assert!(err.contains("Throttled"));
346 }
347
348 #[tokio::test]
349 async fn test_throttler_drop_strategy_sets_camel_stop() {
350 let config =
351 ThrottlerConfig::new(1, Duration::from_secs(10)).strategy(ThrottleStrategy::Drop);
352 let mut svc = ThrottlerService::new(config, passthrough());
353
354 let ex1 = Exchange::new(Message::new("first"));
355 let _ = svc.ready().await.unwrap().call(ex1).await;
356
357 let ex2 = Exchange::new(Message::new("second"));
358 let result = svc.ready().await.unwrap().call(ex2).await.unwrap();
359 assert_eq!(result.property(CAMEL_STOP), Some(&Value::Bool(true)));
360 }
361
362 #[tokio::test]
363 async fn test_throttler_token_replenishment() {
364 let config = ThrottlerConfig::new(1, Duration::from_millis(50));
365 let mut svc = ThrottlerService::new(config, passthrough());
366
367 let ex1 = Exchange::new(Message::new("first"));
368 let _ = svc.ready().await.unwrap().call(ex1).await;
369
370 tokio::time::sleep(Duration::from_millis(100)).await;
371
372 let ex2 = Exchange::new(Message::new("second"));
373 let result = svc.ready().await.unwrap().call(ex2).await;
374 assert!(result.is_ok());
375 }
376
377 #[test]
385 fn test_throttler_zero_max_requests_returns_err() {
386 let config = ThrottlerConfig::new(0, Duration::from_secs(1));
393 let result = std::panic::catch_unwind(|| ThrottlerService::try_new(config, passthrough()));
397 match result {
398 Ok(Ok(_)) => panic!("zero max_requests must be Err, not Ok"),
399 Ok(Err(e)) => {
400 assert!(
401 matches!(
402 e,
403 CamelError::ConfigValidation(
404 camel_api::ConfigValidationError::ThrottlerMaxRequestsZero,
405 )
406 ),
407 "expected ConfigValidation(ThrottlerMaxRequestsZero), got: {e}"
408 );
409 }
410 Err(_) => panic!("zero max_requests must return Err, not panic"),
411 }
412 }
413
414 #[tokio::test]
417 async fn throttle_segment_reject_strategy_returns_failed() {
418 use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
419
420 #[derive(Clone)]
421 struct NoopSeg;
422 impl OutcomePipeline for NoopSeg {
423 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
424 Box::new(NoopSeg)
425 }
426 fn run<'a>(
427 &'a mut self,
428 ex: Exchange,
429 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
430 Box::pin(async move { PipelineOutcome::Completed(ex) })
431 }
432 }
433
434 let config = ThrottlerConfig {
438 max_requests: 1,
439 period: Duration::from_secs(1),
440 strategy: ThrottleStrategy::Reject,
441 };
442 let body = camel_api::OutcomeSegment::new(Box::new(NoopSeg));
443 let mut seg = ThrottleSegment::new(config, body);
444 let ex = Exchange::new(Message::new("test"));
445 let _first = seg.run(ex).await;
447 let ex2 = Exchange::new(Message::new("test2"));
449 let outcome = seg.run(ex2).await;
450 assert!(
451 matches!(outcome, PipelineOutcome::Failed(_)),
452 "Reject strategy must return Failed when tokens exhausted"
453 );
454 }
455
456 #[tokio::test]
457 async fn throttle_segment_drop_strategy_returns_stopped() {
458 use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
459
460 #[derive(Clone)]
461 struct NoopSeg;
462 impl OutcomePipeline for NoopSeg {
463 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
464 Box::new(NoopSeg)
465 }
466 fn run<'a>(
467 &'a mut self,
468 ex: Exchange,
469 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
470 Box::pin(async move { PipelineOutcome::Completed(ex) })
471 }
472 }
473
474 let config = ThrottlerConfig {
477 max_requests: 1,
478 period: Duration::from_secs(1),
479 strategy: ThrottleStrategy::Drop,
480 };
481 let body = camel_api::OutcomeSegment::new(Box::new(NoopSeg));
482 let mut seg = ThrottleSegment::new(config, body);
483 let ex = Exchange::new(Message::new("test"));
484 let _first = seg.run(ex).await;
486 let ex2 = Exchange::new(Message::new("test2"));
488 let outcome = seg.run(ex2).await;
489 match outcome {
490 PipelineOutcome::Stopped(returned_ex) => {
491 let stopped_flag = returned_ex.property(CAMEL_STOP).and_then(|v| v.as_bool());
492 assert_eq!(
493 stopped_flag,
494 Some(true),
495 "Drop strategy must set CamelStop=true property"
496 );
497 }
498 other => panic!("Drop must return Stopped, got {:?}", other),
499 }
500 }
501
502 #[tokio::test]
503 async fn throttle_segment_delay_strategy_propagates_stopped_body() {
504 use camel_api::{Body, Exchange, Message, OutcomePipeline, PipelineOutcome};
505
506 #[derive(Clone)]
507 struct StoppingSeg;
508 impl OutcomePipeline for StoppingSeg {
509 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
510 Box::new(StoppingSeg)
511 }
512 fn run<'a>(
513 &'a mut self,
514 mut ex: Exchange,
515 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
516 Box::pin(async move {
517 ex.input.body = Body::Bytes(b"stopped-mut".to_vec().into());
518 PipelineOutcome::Stopped(ex)
519 })
520 }
521 }
522
523 let config = ThrottlerConfig {
524 max_requests: 1, period: Duration::from_secs(1),
526 strategy: ThrottleStrategy::Delay,
527 };
528 let body = camel_api::OutcomeSegment::new(Box::new(StoppingSeg));
529 let mut seg = ThrottleSegment::new(config, body);
530 let ex = Exchange::new(Message::new("test"));
531 let outcome = seg.run(ex).await;
532 match outcome {
533 PipelineOutcome::Stopped(returned_ex) => {
534 if let Body::Bytes(b) = &returned_ex.input.body {
535 assert_eq!(
536 b.as_ref(),
537 b"stopped-mut",
538 "BUG: throttle body Stop must preserve mutations"
539 );
540 } else {
541 panic!("expected Body::Bytes");
542 }
543 }
544 other => panic!("expected Stopped propagation, got {:?}", other),
545 }
546 }
547}