1use std::future::Future;
2use std::pin::Pin;
3use std::time::{Duration, Instant};
4
5use camel_api::CAMEL_STOP;
6use camel_api::{CamelError, ConfigValidationError, ThrottleStrategy, ThrottlerConfig};
7
8pub struct RateLimiter {
9 tokens: f64,
10 max_tokens: f64,
11 refill_rate: f64,
12 last_refill: Instant,
13}
14
15impl RateLimiter {
16 fn new(max_requests: usize, period: Duration) -> Self {
17 let refill_rate = max_requests as f64 / period.as_secs_f64();
18 Self {
19 tokens: max_requests as f64,
20 max_tokens: max_requests as f64,
21 refill_rate,
22 last_refill: Instant::now(),
23 }
24 }
25
26 fn try_acquire(&mut self) -> bool {
27 let now = Instant::now();
28 let elapsed = now.duration_since(self.last_refill).as_secs_f64();
29 if elapsed > 0.0 {
30 self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max_tokens);
31 self.last_refill = now;
32 }
33 if self.tokens >= 1.0 {
34 self.tokens -= 1.0;
35 true
36 } else {
37 false
38 }
39 }
40
41 fn time_until_next_token(&self) -> Duration {
42 if self.tokens >= 1.0 {
43 Duration::ZERO
44 } else {
45 let tokens_needed = 1.0 - self.tokens;
46 Duration::from_secs_f64(tokens_needed / self.refill_rate)
47 }
48 }
49}
50
51pub struct ThrottleSegment {
56 pub config: ThrottlerConfig,
57 pub limiter: std::sync::Arc<std::sync::Mutex<RateLimiter>>,
58 pub body: camel_api::OutcomeSegment,
59}
60
61impl ThrottleSegment {
62 pub fn new(config: ThrottlerConfig, body: camel_api::OutcomeSegment) -> Self {
65 Self::try_new(config, body).expect("ThrottleSegment::new invariants violated") }
69
70 pub fn try_new(
72 config: ThrottlerConfig,
73 body: camel_api::OutcomeSegment,
74 ) -> Result<Self, CamelError> {
75 if config.period == Duration::ZERO {
76 return Err(CamelError::Config(
77 "ThrottlerConfig.period must be > 0".to_string(),
78 ));
79 }
80 if config.max_requests == 0 {
81 return Err(CamelError::from(
82 ConfigValidationError::ThrottlerMaxRequestsZero,
83 ));
84 }
85 Ok(Self {
86 limiter: std::sync::Arc::new(std::sync::Mutex::new(RateLimiter::new(
87 config.max_requests,
88 config.period,
89 ))),
90 config,
91 body,
92 })
93 }
94}
95
96impl Clone for ThrottleSegment {
97 fn clone(&self) -> Self {
98 Self {
99 config: self.config.clone(),
100 limiter: std::sync::Arc::clone(&self.limiter),
101 body: self.body.clone(),
102 }
103 }
104}
105
106impl camel_api::OutcomePipeline for ThrottleSegment {
107 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
108 Box::new(self.clone())
109 }
110
111 fn run<'a>(
112 &'a mut self,
113 exchange: camel_api::Exchange,
114 ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
115 Box::pin(async move {
116 let acquired = {
117 let mut limiter = self.limiter.lock().unwrap(); limiter.try_acquire()
119 };
120 if acquired {
121 return self.body.run(exchange).await;
122 }
123 match self.config.strategy {
124 ThrottleStrategy::Reject => {
125 camel_api::PipelineOutcome::Failed(camel_api::CamelError::ProcessorError(
126 "Throttled: rate limit exceeded".to_string(),
127 ))
128 }
129 ThrottleStrategy::Drop => {
130 let mut ex = exchange;
131 ex.set_property(CAMEL_STOP, camel_api::Value::Bool(true));
132 camel_api::PipelineOutcome::Stopped(ex)
133 }
134 _ => {
136 loop {
137 let wait_time = {
138 let limiter = self.limiter.lock().unwrap(); limiter.time_until_next_token()
140 };
141 if wait_time > Duration::ZERO {
142 tokio::time::sleep(wait_time).await;
143 }
144 let acquired = {
145 let mut limiter = self.limiter.lock().unwrap(); limiter.try_acquire()
147 };
148 if acquired {
149 break;
150 }
151 tokio::task::yield_now().await;
152 }
153 self.body.run(exchange).await
154 }
155 }
156 })
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::test_processor_segment::processor_segment;
164 use camel_api::{
165 Body, BoxProcessor, BoxProcessorExt, Exchange, Message, OutcomePipeline, PipelineOutcome,
166 };
167
168 fn passthrough() -> BoxProcessor {
169 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
170 }
171
172 fn segment(config: ThrottlerConfig) -> ThrottleSegment {
173 ThrottleSegment::new(config, processor_segment(passthrough()))
174 }
175
176 #[test]
181 fn throttle_segment_zero_period_returns_err() {
182 let config = ThrottlerConfig::new(5, Duration::ZERO);
183 let err = ThrottleSegment::try_new(config, processor_segment(passthrough()))
184 .err()
185 .expect("zero period must be Err from try_new, not Ok");
186 let msg = err.to_string();
187 assert!(
188 msg.contains("period") && msg.contains("> 0"),
189 "error must mention period > 0: {msg}"
190 );
191 }
192
193 #[tokio::test]
194 async fn throttle_segment_allows_under_limit() {
195 let config = ThrottlerConfig::new(5, Duration::from_secs(1));
196 let mut seg = segment(config);
197
198 for _ in 0..5 {
199 let ex = Exchange::new(Message::new("test"));
200 let outcome = seg.run(ex).await;
201 assert!(matches!(outcome, PipelineOutcome::Completed(_)));
202 }
203 }
204
205 #[tokio::test]
206 async fn throttle_segment_delay_strategy_queues_message() {
207 let config = ThrottlerConfig::new(1, Duration::from_millis(100));
208 let mut seg = segment(config);
209
210 let first = seg.run(Exchange::new(Message::new("first"))).await;
211 assert!(matches!(first, PipelineOutcome::Completed(_)));
212
213 let start = Instant::now();
214 let second = seg.run(Exchange::new(Message::new("second"))).await;
215 assert!(matches!(second, PipelineOutcome::Completed(_)));
216 assert!(start.elapsed() >= Duration::from_millis(50));
217 }
218
219 #[test]
227 fn throttle_segment_zero_max_requests_returns_err() {
228 let config = ThrottlerConfig::new(0, Duration::from_secs(1));
229 let err = ThrottleSegment::try_new(config, processor_segment(passthrough()))
230 .err()
231 .expect("zero max_requests must be Err, not Ok");
232 assert!(
233 matches!(
234 err,
235 CamelError::ConfigValidation(
236 camel_api::ConfigValidationError::ThrottlerMaxRequestsZero,
237 )
238 ),
239 "expected ConfigValidation(ThrottlerMaxRequestsZero), got: {err}"
240 );
241 }
242
243 #[tokio::test]
246 async fn throttle_segment_reject_strategy_returns_failed() {
247 #[derive(Clone)]
248 struct NoopSeg;
249 impl OutcomePipeline for NoopSeg {
250 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
251 Box::new(NoopSeg)
252 }
253 fn run<'a>(
254 &'a mut self,
255 ex: Exchange,
256 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
257 Box::pin(async move { PipelineOutcome::Completed(ex) })
258 }
259 }
260
261 let config = ThrottlerConfig {
265 max_requests: 1,
266 period: Duration::from_secs(1),
267 strategy: ThrottleStrategy::Reject,
268 };
269 let body = camel_api::OutcomeSegment::new(Box::new(NoopSeg));
270 let mut seg = ThrottleSegment::new(config, body);
271 let ex = Exchange::new(Message::new("test"));
272 let _first = seg.run(ex).await;
274 let ex2 = Exchange::new(Message::new("test2"));
276 let outcome = seg.run(ex2).await;
277 let err = match outcome {
278 PipelineOutcome::Failed(err) => err,
279 other => panic!("Reject strategy must return Failed when tokens exhausted: {other:?}"),
280 };
281 assert!(
282 matches!(err, CamelError::ProcessorError(_)),
283 "throttle rejection must be ProcessorError, got: {err:?}"
284 );
285 assert!(
286 err.to_string().contains("Throttled"),
287 "throttle rejection error must mention Throttled: {err}"
288 );
289 }
290
291 #[tokio::test]
292 async fn throttle_segment_drop_strategy_returns_stopped() {
293 #[derive(Clone)]
294 struct NoopSeg;
295 impl OutcomePipeline for NoopSeg {
296 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
297 Box::new(NoopSeg)
298 }
299 fn run<'a>(
300 &'a mut self,
301 ex: Exchange,
302 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
303 Box::pin(async move { PipelineOutcome::Completed(ex) })
304 }
305 }
306
307 let config = ThrottlerConfig {
310 max_requests: 1,
311 period: Duration::from_secs(1),
312 strategy: ThrottleStrategy::Drop,
313 };
314 let body = camel_api::OutcomeSegment::new(Box::new(NoopSeg));
315 let mut seg = ThrottleSegment::new(config, body);
316 let ex = Exchange::new(Message::new("test"));
317 let _first = seg.run(ex).await;
319 let ex2 = Exchange::new(Message::new("test2"));
321 let outcome = seg.run(ex2).await;
322 match outcome {
323 PipelineOutcome::Stopped(returned_ex) => {
324 let stopped_flag = returned_ex.property(CAMEL_STOP).and_then(|v| v.as_bool());
325 assert_eq!(
326 stopped_flag,
327 Some(true),
328 "Drop strategy must set CamelStop=true property"
329 );
330 }
331 other => panic!("Drop must return Stopped, got {:?}", other),
332 }
333 }
334
335 #[tokio::test]
336 async fn throttle_segment_delay_strategy_propagates_stopped_body() {
337 #[derive(Clone)]
338 struct StoppingSeg;
339 impl OutcomePipeline for StoppingSeg {
340 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
341 Box::new(StoppingSeg)
342 }
343 fn run<'a>(
344 &'a mut self,
345 mut ex: Exchange,
346 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
347 Box::pin(async move {
348 ex.input.body = Body::Bytes(b"stopped-mut".to_vec().into());
349 PipelineOutcome::Stopped(ex)
350 })
351 }
352 }
353
354 let config = ThrottlerConfig {
355 max_requests: 1, period: Duration::from_secs(1),
357 strategy: ThrottleStrategy::Delay,
358 };
359 let body = camel_api::OutcomeSegment::new(Box::new(StoppingSeg));
360 let mut seg = ThrottleSegment::new(config, body);
361 let ex = Exchange::new(Message::new("test"));
362 let outcome = seg.run(ex).await;
363 match outcome {
364 PipelineOutcome::Stopped(returned_ex) => {
365 if let Body::Bytes(b) = &returned_ex.input.body {
366 assert_eq!(
367 b.as_ref(),
368 b"stopped-mut",
369 "BUG: throttle body Stop must preserve mutations"
370 );
371 } else {
372 panic!("expected Body::Bytes");
373 }
374 }
375 other => panic!("expected Stopped propagation, got {:?}", other),
376 }
377 }
378}