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