1use std::sync::atomic::{AtomicBool, Ordering};
13use std::time::Duration;
14
15use async_trait::async_trait;
16use chrono::Utc;
17use tokio::time;
18use tracing::debug;
19
20use camel_component_api::{BoxProcessor, CamelError, Exchange, Message};
21use camel_component_api::{Component, Consumer, ConsumerContext, Endpoint, ProducerContext};
22use camel_component_api::{ComponentMetadata, UriConfig};
23
24#[derive(Debug, Clone, UriConfig)]
32#[uri_scheme = "timer"]
33#[uri_config(
34 skip_impl,
35 metadata(
36 scheme = "timer",
37 description = "Generate timer-based events",
38 consumer
39 ),
40 crate = "camel_component_api"
41)]
42pub struct TimerConfig {
43 pub name: String,
45
46 #[allow(dead_code)] #[uri_param(name = "period", default = "1000")]
49 period_ms: u64,
50
51 pub period: Duration,
53
54 #[allow(dead_code)] #[uri_param(name = "delay", default = "0")]
57 delay_ms: u64,
58
59 pub delay: Duration,
61
62 #[uri_param(name = "repeatCount")]
64 pub repeat_count: Option<u32>,
65
66 #[uri_param(name = "fixedRate", default = "false")]
69 pub fixed_rate: bool,
70
71 #[uri_param(name = "includeMetadata", default = "true")]
75 pub include_metadata: bool,
76}
77
78impl TimerConfig {
80 pub fn validate(&self) -> Result<(), CamelError> {
82 if self.name.trim().is_empty() {
83 return Err(CamelError::InvalidUri(
84 "timer name must not be empty".to_string(),
85 ));
86 }
87 if self.period.is_zero() {
88 return Err(CamelError::InvalidUri(
89 "timer period must be greater than 0".to_string(),
90 ));
91 }
92 Ok(())
93 }
94}
95
96impl UriConfig for TimerConfig {
97 fn scheme() -> &'static str {
98 "timer"
99 }
100
101 fn from_uri(uri: &str) -> Result<Self, CamelError> {
102 let parts = camel_component_api::parse_uri(uri)?;
103 Self::from_components(parts)
104 }
105
106 fn from_components(parts: camel_component_api::UriComponents) -> Result<Self, CamelError> {
107 let config = Self::parse_uri_components(parts)?;
108 TimerConfig::validate(&config)?;
109 Ok(config)
110 }
111
112 fn validate(self) -> Result<Self, CamelError> {
113 TimerConfig::validate(&self)?;
115 Ok(self)
116 }
117}
118
119pub struct TimerComponent;
125
126impl TimerComponent {
127 pub fn new() -> Self {
128 Self
129 }
130}
131
132impl Default for TimerComponent {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138impl Component for TimerComponent {
139 fn scheme(&self) -> &str {
140 "timer"
141 }
142
143 fn metadata(&self) -> ComponentMetadata {
144 TimerConfig::metadata()
145 }
146
147 fn create_endpoint(
148 &self,
149 uri: &str,
150 _ctx: &dyn camel_component_api::ComponentContext,
151 ) -> Result<Box<dyn Endpoint>, CamelError> {
152 let config = TimerConfig::from_uri(uri)?;
153 Ok(Box::new(TimerEndpoint {
154 uri: uri.to_string(),
155 config,
156 }))
157 }
158}
159
160pub struct TimerEndpoint {
165 uri: String,
166 config: TimerConfig,
167}
168
169impl Endpoint for TimerEndpoint {
170 fn uri(&self) -> &str {
171 &self.uri
172 }
173
174 fn create_consumer(
175 &self,
176 _rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability>,
177 ) -> Result<Box<dyn Consumer>, CamelError> {
178 Ok(Box::new(TimerConsumer {
179 config: self.config.clone(),
180 started: AtomicBool::new(false),
181 }))
182 }
183
184 fn create_producer(
185 &self,
186 _rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability>,
187 _ctx: &ProducerContext,
188 ) -> Result<BoxProcessor, CamelError> {
189 Err(CamelError::EndpointCreationFailed(
190 "timer endpoint does not support producers".to_string(),
191 ))
192 }
193}
194
195pub struct TimerConsumer {
200 config: TimerConfig,
201 started: AtomicBool,
203}
204
205#[async_trait]
206impl Consumer for TimerConsumer {
207 async fn start(&mut self, context: ConsumerContext) -> Result<(), CamelError> {
208 self.started
210 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
211 .map_err(|_| {
212 CamelError::EndpointCreationFailed("timer consumer already started".to_string())
213 })?;
214
215 TimerConfig::validate(&self.config)?;
216 let config = self.config.clone();
217 let cancel_token = context.cancel_token();
218
219 if !config.delay.is_zero() {
221 tokio::select! {
222 _ = time::sleep(config.delay) => {}
223 _ = cancel_token.cancelled() => {
224 debug!(timer = config.name, "Timer cancelled during initial delay");
225 self.started.store(false, Ordering::SeqCst);
226 return Ok(());
227 }
228 }
229 }
230
231 if config.repeat_count == Some(0) {
233 debug!(timer = config.name, "repeat_count=0, timer will not fire");
234 self.started.store(false, Ordering::SeqCst);
235 return Ok(());
236 }
237
238 let mut interval = time::interval(config.period);
239
240 if config.fixed_rate {
242 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
243 } else {
244 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Burst);
245 }
246
247 let mut count: u32 = 0;
248
249 loop {
250 tokio::select! {
251 _ = cancel_token.cancelled() => {
252 debug!(timer = config.name, "Timer received cancellation, stopping");
253 break;
254 }
255 _ = interval.tick() => {
256 count += 1;
257
258 debug!(timer = config.name, count, "Timer tick");
259
260 let mut exchange = Exchange::new(Message::new(format!(
261 "timer://{} tick #{}",
262 config.name, count
263 )));
264
265 if config.include_metadata {
267 exchange.input.set_header(
268 "CamelTimerName",
269 serde_json::Value::String(config.name.clone()),
270 );
271 exchange
272 .input
273 .set_header("CamelTimerCounter", serde_json::Value::Number(count.into()));
274
275 let now = Utc::now();
277 exchange.input.set_header(
278 "CamelTimerFiredTime",
279 serde_json::Value::String(now.to_rfc3339()),
280 );
281 exchange.input.set_header(
282 "CamelMessageTimestamp",
283 serde_json::Value::Number(
284 now.timestamp_millis().into(),
285 ),
286 );
287 }
288
289 if context.send(exchange).await.is_err() {
290 break;
292 }
293
294 if let Some(max) = config.repeat_count
295 && count >= max
296 {
297 break;
298 }
299 }
300 }
301 }
302
303 self.started.store(false, Ordering::SeqCst);
305 Ok(())
306 }
307
308 async fn stop(&mut self) -> Result<(), CamelError> {
309 self.started.store(false, Ordering::SeqCst);
310 debug!(timer = self.config.name, "timer consumer stopped");
311 Ok(())
312 }
313}
314
315impl TimerConsumer {
316 #[cfg(test)]
318 pub(crate) fn mark_started_for_test(&self) {
319 self.started.store(true, Ordering::SeqCst);
320 }
321}
322
323#[cfg(test)]
328mod tests {
329 use camel_component_api::test_support::PanicRuntimeObservability;
330 fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
331 std::sync::Arc::new(PanicRuntimeObservability)
332 }
333
334 use super::*;
335 use camel_component_api::NoOpComponentContext;
336
337 #[test]
338 fn test_zero_period_rejected() {
339 let result = TimerConfig::from_uri("timer:tick?period=0");
340 assert!(result.is_err(), "period=0 should be rejected");
341 let err_msg = result.unwrap_err().to_string();
342 assert!(err_msg.contains("period"), "error should mention 'period'");
343 }
344
345 #[test]
346 fn test_timer_empty_name_rejected() {
347 let result = TimerConfig::from_uri("timer:");
348 assert!(result.is_err());
349 let err = result.unwrap_err().to_string();
350 assert!(err.contains("must not be empty"), "unexpected error: {err}");
351 }
352
353 #[test]
354 fn test_timer_config_defaults() {
355 let config = TimerConfig::from_uri("timer:tick").unwrap();
356 assert_eq!(config.name, "tick");
357 assert_eq!(config.period, Duration::from_millis(1000));
358 assert_eq!(config.delay, Duration::from_millis(0));
359 assert_eq!(config.repeat_count, None);
360 }
361
362 #[test]
363 fn test_timer_config_with_params() {
364 let config =
365 TimerConfig::from_uri("timer:myTimer?period=500&delay=100&repeatCount=5").unwrap();
366 assert_eq!(config.name, "myTimer");
367 assert_eq!(config.period, Duration::from_millis(500));
368 assert_eq!(config.delay, Duration::from_millis(100));
369 assert_eq!(config.repeat_count, Some(5));
370 }
371
372 #[test]
373 fn test_timer_config_wrong_scheme() {
374 let result = TimerConfig::from_uri("log:info");
375 assert!(result.is_err());
376 }
377
378 #[test]
379 fn test_timer_component_scheme() {
380 let component = TimerComponent::new();
381 assert_eq!(component.scheme(), "timer");
382 }
383
384 #[test]
385 fn test_timer_component_creates_endpoint() {
386 let component = TimerComponent::new();
387 let endpoint = component.create_endpoint("timer:tick?period=1000", &NoOpComponentContext);
388 assert!(endpoint.is_ok());
389 }
390
391 #[test]
392 fn test_timer_endpoint_no_producer() {
393 let ctx = ProducerContext::new();
394 let component = TimerComponent::new();
395 let endpoint = component
396 .create_endpoint("timer:tick", &NoOpComponentContext)
397 .unwrap();
398 let producer = endpoint.create_producer(rt(), &ctx);
399 assert!(producer.is_err());
400 }
401
402 #[test]
403 fn test_rejects_empty_timer_name() {
404 let mut cfg = TimerConfig::from_uri("timer:tick").unwrap();
405 cfg.name = "".into();
406 assert!(cfg.validate().is_err());
407 }
408
409 #[test]
410 fn test_rejects_zero_period() {
411 let mut cfg = TimerConfig::from_uri("timer:tick").unwrap();
412 cfg.period = Duration::ZERO;
413 assert!(cfg.validate().is_err());
414 }
415
416 #[test]
417 fn test_valid_config_passes() {
418 let mut cfg = TimerConfig::from_uri("timer:tick").unwrap();
419 cfg.name = "myTimer".into();
420 cfg.period = Duration::from_millis(1000);
421 assert!(cfg.validate().is_ok());
422 }
423
424 #[tokio::test]
425 async fn test_repeat_count_zero_fires_never() {
426 let component = TimerComponent::new();
427 let endpoint = component
428 .create_endpoint(
429 "timer:zero-test?period=50&repeatCount=0",
430 &NoOpComponentContext,
431 )
432 .unwrap();
433 let mut consumer = endpoint.create_consumer(rt()).unwrap();
434
435 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
436 let ctx = ConsumerContext::new(
437 tx,
438 tokio_util::sync::CancellationToken::new(),
439 "timer-test-route".to_string(),
440 );
441
442 consumer.start(ctx).await.unwrap();
444
445 tokio::time::sleep(Duration::from_millis(200)).await;
447
448 let mut count = 0;
450 while rx.try_recv().is_ok() {
451 count += 1;
452 }
453 assert_eq!(
454 count, 0,
455 "repeat_count=0 should produce zero fires, got {count}"
456 );
457
458 consumer.stop().await.unwrap();
460 }
461
462 #[tokio::test]
463 async fn test_repeat_count_omitted_fires_indefinitely() {
464 use tokio_util::sync::CancellationToken;
465
466 let token = CancellationToken::new();
467 let (tx, mut rx) = tokio::sync::mpsc::channel(32);
468 let ctx = ConsumerContext::new(tx, token.clone(), "timer-infinite-route".to_string());
469
470 let mut consumer = TimerConsumer {
472 config: TimerConfig::from_uri("timer:infinite-test?period=20").unwrap(),
473 started: AtomicBool::new(false),
474 };
475
476 let handle = tokio::spawn(async move {
477 consumer.start(ctx).await.unwrap();
478 });
479
480 tokio::time::sleep(Duration::from_millis(200)).await;
483 token.cancel();
484 let _ = tokio::time::timeout(Duration::from_secs(1), handle).await;
485
486 let mut count = 0;
487 while rx.try_recv().is_ok() {
488 count += 1;
489 }
490 assert!(
491 count >= 6,
492 "Omitted repeatCount should fire indefinitely; expected >= 6 fires in window, got {count}"
493 );
494 }
495
496 #[tokio::test]
497 async fn test_timer_consumer_fires() {
498 let component = TimerComponent::new();
499 let endpoint = component
500 .create_endpoint("timer:test?period=50&repeatCount=3", &NoOpComponentContext)
501 .unwrap();
502 let mut consumer = endpoint.create_consumer(rt()).unwrap();
503
504 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
505 let ctx = ConsumerContext::new(
506 tx,
507 tokio_util::sync::CancellationToken::new(),
508 "timer-test-route".to_string(),
509 );
510
511 tokio::spawn(async move {
513 consumer.start(ctx).await.unwrap();
514 });
515
516 let mut received = Vec::new();
518 while let Some(envelope) = rx.recv().await {
519 received.push(envelope.exchange);
520 if received.len() == 3 {
521 break;
522 }
523 }
524
525 assert_eq!(received.len(), 3);
526
527 let first = &received[0];
529 assert_eq!(
530 first.input.header("CamelTimerName"),
531 Some(&serde_json::Value::String("test".into()))
532 );
533 assert_eq!(
534 first.input.header("CamelTimerCounter"),
535 Some(&serde_json::Value::Number(1.into()))
536 );
537 }
538
539 #[tokio::test]
540 async fn test_timer_consumer_respects_cancellation() {
541 use tokio_util::sync::CancellationToken;
542
543 let token = CancellationToken::new();
544 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
545 let ctx = ConsumerContext::new(tx, token.clone(), "timer-test-route".to_string());
546
547 let mut consumer = TimerConsumer {
548 config: TimerConfig::from_uri("timer:cancel-test?period=50").unwrap(),
549 started: AtomicBool::new(false),
550 };
551
552 let handle = tokio::spawn(async move {
553 consumer.start(ctx).await.unwrap();
554 });
555
556 tokio::time::sleep(Duration::from_millis(180)).await;
558 token.cancel();
559
560 let result = tokio::time::timeout(Duration::from_secs(1), handle).await;
561 assert!(
562 result.is_ok(),
563 "Consumer should have stopped after cancellation"
564 );
565
566 let mut count = 0;
567 while rx.try_recv().is_ok() {
568 count += 1;
569 }
570 assert!(
571 count >= 2,
572 "Expected at least 2 exchanges before cancellation, got {count}"
573 );
574 }
575
576 #[tokio::test]
577 async fn test_timer_consumer_stop_shuts_down() {
578 let component = TimerComponent::new();
579 let endpoint = component
580 .create_endpoint("timer:stop-test?period=50", &NoOpComponentContext)
581 .unwrap();
582 let mut consumer = endpoint.create_consumer(rt()).unwrap();
583
584 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
585 let token = tokio_util::sync::CancellationToken::new();
586 let ctx = ConsumerContext::new(tx, token.clone(), "timer-test-route".to_string());
587
588 tokio::spawn(async move {
590 consumer.start(ctx).await.unwrap();
591 });
592
593 tokio::time::sleep(Duration::from_millis(180)).await;
595
596 let mut count = 0;
598 while rx.try_recv().is_ok() {
599 count += 1;
600 }
601 assert!(count >= 2, "Expected at least 2 exchanges, got {count}");
602
603 token.cancel();
605 }
606
607 #[test]
609 fn test_fixed_rate_default_is_false() {
610 let config = TimerConfig::from_uri("timer:tick").unwrap();
611 assert!(!config.fixed_rate, "fixedRate should default to false");
612 }
613
614 #[test]
615 fn test_fixed_rate_parsed_from_uri() {
616 let config = TimerConfig::from_uri("timer:tick?fixedRate=true").unwrap();
617 assert!(
618 config.fixed_rate,
619 "fixedRate should be true when set in URI"
620 );
621 }
622
623 #[tokio::test]
625 async fn test_double_start_returns_error() {
626 let component = TimerComponent::new();
627 let endpoint = component
628 .create_endpoint(
629 "timer:double?period=50&repeatCount=2",
630 &NoOpComponentContext,
631 )
632 .unwrap(); let mut consumer = TimerConsumer {
635 config: TimerConfig {
636 name: "double-test".to_string(),
637 period: Duration::from_millis(100),
638 period_ms: 100,
639 delay: Duration::ZERO,
640 delay_ms: 0,
641 repeat_count: None,
642 fixed_rate: false,
643 include_metadata: true,
644 },
645 started: AtomicBool::new(false),
646 };
647
648 consumer.mark_started_for_test();
650
651 let (tx, _rx) = tokio::sync::mpsc::channel(16);
652 let cancel_token = tokio_util::sync::CancellationToken::new();
653 let ctx = ConsumerContext::new(tx, cancel_token.clone(), "timer-test-route".to_string());
654
655 let result = consumer.start(ctx).await;
657 assert!(result.is_err(), "expected double-start to return Err");
658 let err_str = format!("{:?}", result.unwrap_err());
659 assert!(
660 err_str.contains("already started"),
661 "unexpected error: {err_str}"
662 );
663
664 drop(endpoint); }
666
667 #[tokio::test]
669 async fn test_timer_fired_time_and_message_timestamp_headers() {
670 let component = TimerComponent::new();
671 let endpoint = component
672 .create_endpoint(
673 "timer:headers?period=50&repeatCount=1",
674 &NoOpComponentContext,
675 )
676 .unwrap();
677 let mut consumer = endpoint.create_consumer(rt()).unwrap();
678
679 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
680 let ctx = ConsumerContext::new(
681 tx,
682 tokio_util::sync::CancellationToken::new(),
683 "timer-test-route".to_string(),
684 );
685
686 tokio::spawn(async move {
687 consumer.start(ctx).await.unwrap();
688 });
689
690 let envelope = tokio::time::timeout(Duration::from_secs(2), rx.recv())
691 .await
692 .expect("should receive exchange")
693 .expect("envelope should exist");
694
695 let exchange = envelope.exchange;
696
697 let fired_time = exchange
699 .input
700 .header("CamelTimerFiredTime")
701 .expect("CamelTimerFiredTime header should be present");
702 assert!(
703 fired_time.is_string(),
704 "CamelTimerFiredTime should be a string"
705 );
706 let fired_str = fired_time.as_str().unwrap();
707 assert!(
709 chrono::DateTime::parse_from_rfc3339(fired_str).is_ok(),
710 "CamelTimerFiredTime should be valid RFC 3339: {fired_str}"
711 );
712
713 let msg_ts = exchange
715 .input
716 .header("CamelMessageTimestamp")
717 .expect("CamelMessageTimestamp header should be present");
718 assert!(
719 msg_ts.is_number(),
720 "CamelMessageTimestamp should be a number"
721 );
722 let ts_millis = msg_ts.as_i64().expect("should be i64");
723 assert!(ts_millis > 0, "timestamp should be positive");
724 }
725
726 #[test]
727 fn test_timer_fired_time_header_format() {
728 let now = chrono::Utc::now();
730 let rfc = now.to_rfc3339();
731 assert!(chrono::DateTime::parse_from_rfc3339(&rfc).is_ok());
732 let millis = now.timestamp_millis();
733 assert!(millis > 0);
734 }
735
736 #[test]
738 fn test_include_metadata_default_is_true() {
739 let config = TimerConfig::from_uri("timer:tick").unwrap();
740 assert!(
741 config.include_metadata,
742 "includeMetadata should default to true"
743 );
744 }
745
746 #[test]
747 fn test_include_metadata_false_from_uri() {
748 let config = TimerConfig::from_uri("timer:tick?includeMetadata=false").unwrap();
749 assert!(
750 !config.include_metadata,
751 "includeMetadata should be false when set in URI"
752 );
753 }
754
755 #[tokio::test]
756 async fn test_include_metadata_false_omits_headers() {
757 let component = TimerComponent::new();
758 let endpoint = component
759 .create_endpoint(
760 "timer:minimal?period=50&repeatCount=1&includeMetadata=false",
761 &NoOpComponentContext,
762 )
763 .unwrap();
764 let mut consumer = endpoint.create_consumer(rt()).unwrap();
765
766 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
767 let ctx = ConsumerContext::new(
768 tx,
769 tokio_util::sync::CancellationToken::new(),
770 "timer-test-route".to_string(),
771 );
772
773 tokio::spawn(async move {
774 consumer.start(ctx).await.unwrap();
775 });
776
777 let envelope = tokio::time::timeout(Duration::from_secs(2), rx.recv())
778 .await
779 .expect("should receive exchange")
780 .expect("envelope should exist");
781
782 let exchange = envelope.exchange;
783
784 assert!(
786 exchange.input.header("CamelTimerName").is_none(),
787 "CamelTimerName should not be present when includeMetadata=false"
788 );
789 assert!(
790 exchange.input.header("CamelTimerCounter").is_none(),
791 "CamelTimerCounter should not be present when includeMetadata=false"
792 );
793 assert!(
794 exchange.input.header("CamelTimerFiredTime").is_none(),
795 "CamelTimerFiredTime should not be present when includeMetadata=false"
796 );
797 assert!(
798 exchange.input.header("CamelMessageTimestamp").is_none(),
799 "CamelMessageTimestamp should not be present when includeMetadata=false"
800 );
801 }
802
803 #[tokio::test]
804 async fn test_include_metadata_true_includes_all_headers() {
805 let component = TimerComponent::new();
806 let endpoint = component
807 .create_endpoint(
808 "timer:full?period=50&repeatCount=1&includeMetadata=true",
809 &NoOpComponentContext,
810 )
811 .unwrap();
812 let mut consumer = endpoint.create_consumer(rt()).unwrap();
813
814 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
815 let ctx = ConsumerContext::new(
816 tx,
817 tokio_util::sync::CancellationToken::new(),
818 "timer-test-route".to_string(),
819 );
820
821 tokio::spawn(async move {
822 consumer.start(ctx).await.unwrap();
823 });
824
825 let envelope = tokio::time::timeout(Duration::from_secs(2), rx.recv())
826 .await
827 .expect("should receive exchange")
828 .expect("envelope should exist");
829
830 let exchange = envelope.exchange;
831
832 assert!(exchange.input.header("CamelTimerName").is_some());
833 assert!(exchange.input.header("CamelTimerCounter").is_some());
834 assert!(exchange.input.header("CamelTimerFiredTime").is_some());
835 assert!(exchange.input.header("CamelMessageTimestamp").is_some());
836 }
837
838 #[test]
840 fn test_timer_endpoint_is_pub() {
841 let component = TimerComponent::new();
842 let endpoint = component
843 .create_endpoint("timer:pub-test", &NoOpComponentContext)
844 .unwrap();
845 assert_eq!(endpoint.uri(), "timer:pub-test");
846 }
847}