1use std::collections::HashMap;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
10use std::sync::{Arc, Mutex};
11use std::task::{Context, Poll};
12use std::time::Duration;
13
14#[cfg(test)]
15use camel_component_api::test_support::NoopRuntimeObservability;
16#[cfg(test)]
17fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
18 std::sync::Arc::new(NoopRuntimeObservability)
22}
23
24use async_trait::async_trait;
25use tokio::sync::{Mutex as AsyncMutex, mpsc, oneshot};
26use tokio::task::JoinHandle;
27use tokio_util::sync::CancellationToken;
28use tower::Service;
29
30use camel_api::BoxProcessorExt;
31use camel_component_api::UriConfig;
32use camel_component_api::parse_uri;
33use camel_component_api::{
34 BoxProcessor, CamelError, Component, ComponentContext, ComponentMetadata, ConcurrencyModel,
35 Consumer, ConsumerContext, ConsumerStartupMode, Endpoint, Exchange, ExchangeEnvelope,
36 ProducerContext,
37};
38use tracing::{info, warn};
39
40const QUEUE_DEPTH_SAMPLE_INTERVAL: Duration = Duration::from_millis(250);
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum WaitForTaskToComplete {
52 Never,
53 IfReplyExpected,
54 Always,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ExchangePattern {
59 InOnly,
60 InOut,
61}
62
63#[derive(Debug, Clone)]
79pub struct SedaConfig {
80 pub name: String,
81 pub size: usize,
82 pub concurrent_consumers: usize,
83 pub multiple_consumers: bool,
84 pub block_when_full: bool,
85 pub discard_if_no_consumers: bool,
86 pub timeout_ms: u64,
87 pub wait_for_task_to_complete: WaitForTaskToComplete,
88 pub exchange_pattern: ExchangePattern,
89}
90
91#[derive(Debug, Clone, UriConfig)]
97#[allow(dead_code)]
98#[uri_scheme = "seda"]
99#[uri_config(
100 skip_impl,
101 metadata(
102 scheme = "seda",
103 description = "Asynchronous staged event-driven architecture with bounded queue",
104 producer,
105 consumer
106 ),
107 crate = "camel_component_api"
108)]
109struct SedaUriConfig {
110 #[allow(dead_code)]
111 _name: String,
112 #[uri_param(
113 name = "size",
114 default = "1000",
115 desc = "Bounded queue capacity. Must be > 0"
116 )]
117 size: usize,
118 #[uri_param(
119 name = "concurrentConsumers",
120 default = "1",
121 desc = "Consumer concurrency. Clamped to 1 minimum"
122 )]
123 concurrent_consumers: usize,
124 #[uri_param(
125 name = "multipleConsumers",
126 default = "false",
127 desc = "Fanout mode — clone to all subscribers"
128 )]
129 multiple_consumers: bool,
130 #[uri_param(
131 name = "blockWhenFull",
132 default = "false",
133 desc = "Block producer when queue full vs fail fast"
134 )]
135 block_when_full: bool,
136 #[uri_param(
137 name = "discardIfNoConsumers",
138 default = "false",
139 desc = "Silently drop if no consumers vs error"
140 )]
141 discard_if_no_consumers: bool,
142 #[uri_param(
143 name = "timeout",
144 default = "30000",
145 desc = "Timeout for enqueue and reply wait in milliseconds"
146 )]
147 timeout_ms: u64,
148 #[uri_param(
149 name = "waitForTaskToComplete",
150 kind = "enum:Never,IfReplyExpected,Always",
151 default = "IfReplyExpected",
152 desc = "When to wait for task completion"
153 )]
154 wait_for_task_to_complete: String,
155 #[uri_param(
156 name = "exchangePattern",
157 kind = "enum:InOnly,InOut",
158 default = "InOnly",
159 desc = "Exchange pattern"
160 )]
161 exchange_pattern: String,
162}
163
164impl SedaConfig {
165 pub fn from_uri(uri: &str) -> Result<Self, CamelError> {
166 let parts = parse_uri(uri)?;
167 if parts.scheme != "seda" {
168 return Err(CamelError::InvalidUri(format!(
169 "invalid scheme '{}', expected 'seda'",
170 parts.scheme
171 )));
172 }
173
174 let name = parts.path;
175 if name.trim().is_empty() {
176 return Err(CamelError::InvalidUri(
177 "seda: endpoint name must not be empty".to_string(),
178 ));
179 }
180 if name.contains(char::is_whitespace) {
181 return Err(CamelError::InvalidUri(
182 "seda: endpoint name must not contain whitespace".to_string(),
183 ));
184 }
185
186 let size: usize = parts
187 .params
188 .get("size")
189 .map(|v| v.parse::<usize>())
190 .transpose()
191 .map_err(|e: std::num::ParseIntError| {
192 CamelError::InvalidUri(format!("invalid size: {e}"))
193 })?
194 .unwrap_or(1000);
195
196 if size == 0 {
197 return Err(CamelError::InvalidUri(
198 "seda: size must be greater than 0".to_string(),
199 ));
200 }
201
202 let concurrent_consumers: usize = parts
203 .params
204 .get("concurrentConsumers")
205 .map(|v| v.parse::<usize>())
206 .transpose()
207 .map_err(|e: std::num::ParseIntError| {
208 CamelError::InvalidUri(format!("invalid concurrentConsumers: {e}"))
209 })?
210 .unwrap_or(1);
211
212 let multiple_consumers = parts
213 .params
214 .get("multipleConsumers")
215 .map(|v| parse_bool("multipleConsumers", v))
216 .transpose()?
217 .unwrap_or(false);
218
219 let block_when_full = parts
220 .params
221 .get("blockWhenFull")
222 .map(|v| parse_bool("blockWhenFull", v))
223 .transpose()?
224 .unwrap_or(false);
225
226 let discard_if_no_consumers = parts
227 .params
228 .get("discardIfNoConsumers")
229 .map(|v| parse_bool("discardIfNoConsumers", v))
230 .transpose()?
231 .unwrap_or(false);
232
233 let timeout_ms: u64 = parts
234 .params
235 .get("timeout")
236 .map(|v| v.parse::<u64>())
237 .transpose()
238 .map_err(|e: std::num::ParseIntError| {
239 CamelError::InvalidUri(format!("invalid timeout: {e}"))
240 })?
241 .unwrap_or(30_000);
242
243 let wait_for_task_to_complete = parts
244 .params
245 .get("waitForTaskToComplete")
246 .map(|v| parse_wait_for_task(v))
247 .transpose()?
248 .unwrap_or(WaitForTaskToComplete::IfReplyExpected);
249
250 let exchange_pattern = parts
251 .params
252 .get("exchangePattern")
253 .map(|v| parse_exchange_pattern(v))
254 .transpose()?
255 .unwrap_or(ExchangePattern::InOnly);
256
257 let concurrent_consumers = if concurrent_consumers == 0 {
258 warn!(name, "concurrentConsumers=0 clamped to 1");
259 1
260 } else {
261 concurrent_consumers
262 };
263
264 Ok(Self {
265 name,
266 size,
267 concurrent_consumers,
268 multiple_consumers,
269 block_when_full,
270 discard_if_no_consumers,
271 timeout_ms,
272 wait_for_task_to_complete,
273 exchange_pattern,
274 })
275 }
276
277 fn is_compatible_with(&self, other: &SedaConfig) -> Result<(), String> {
278 let mut diffs = Vec::new();
279 if self.size != other.size {
280 diffs.push(format!("size: {} vs {}", self.size, other.size));
281 }
282 if self.multiple_consumers != other.multiple_consumers {
283 diffs.push(format!(
284 "multipleConsumers: {} vs {}",
285 self.multiple_consumers, other.multiple_consumers
286 ));
287 }
288 if self.exchange_pattern != other.exchange_pattern {
289 diffs.push(format!(
290 "exchangePattern: {:?} vs {:?}",
291 self.exchange_pattern, other.exchange_pattern
292 ));
293 }
294 if self.concurrent_consumers != other.concurrent_consumers {
295 diffs.push(format!(
296 "concurrentConsumers: {} vs {}",
297 self.concurrent_consumers, other.concurrent_consumers
298 ));
299 }
300 if diffs.is_empty() {
301 Ok(())
302 } else {
303 Err(format!(
304 "endpoint '{}' already exists with different config: {}",
305 self.name,
306 diffs.join(", ")
307 ))
308 }
309 }
310
311 pub fn metadata() -> ComponentMetadata {
314 SedaUriConfig::metadata()
315 }
316
317 pub fn uri_options() -> Vec<camel_api::component_metadata::UriOption> {
320 SedaUriConfig::uri_options()
321 }
322}
323
324fn parse_bool(name: &str, v: &str) -> Result<bool, CamelError> {
325 match v.to_lowercase().as_str() {
326 "true" | "1" | "yes" => Ok(true),
327 "false" | "0" | "no" => Ok(false),
328 _ => Err(CamelError::InvalidUri(format!(
329 "invalid boolean for {name}: '{v}'"
330 ))),
331 }
332}
333
334fn parse_wait_for_task(v: &str) -> Result<WaitForTaskToComplete, CamelError> {
335 match v.to_lowercase().replace('_', "").as_str() {
336 "never" => Ok(WaitForTaskToComplete::Never),
337 "ifreplyexpected" => Ok(WaitForTaskToComplete::IfReplyExpected),
338 "always" => Ok(WaitForTaskToComplete::Always),
339 _ => Err(CamelError::InvalidUri(format!(
340 "invalid waitForTaskToComplete: '{v}' (expected: Never, IfReplyExpected, Always)"
341 ))),
342 }
343}
344
345fn parse_exchange_pattern(v: &str) -> Result<ExchangePattern, CamelError> {
346 match v.to_lowercase().replace('_', "").as_str() {
347 "inonly" => Ok(ExchangePattern::InOnly),
348 "inout" => Ok(ExchangePattern::InOut),
349 _ => Err(CamelError::InvalidUri(format!(
350 "invalid exchangePattern: '{v}' (expected: InOnly, InOut)"
351 ))),
352 }
353}
354
355static CONSUMER_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
360
361fn next_consumer_id() -> String {
362 format!(
363 "seda-consumer-{}",
364 CONSUMER_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
365 )
366}
367
368type ConsumerId = String;
373
374enum SedaMode {
381 Single {
382 tx: mpsc::Sender<ExchangeEnvelope>,
383 rx: Mutex<Option<mpsc::Receiver<ExchangeEnvelope>>>,
384 active: std::sync::atomic::AtomicBool,
385 },
386 Fanout {
387 subscribers: Mutex<HashMap<ConsumerId, mpsc::Sender<ExchangeEnvelope>>>,
388 },
389}
390
391struct SedaEndpointState {
392 config: SedaConfig,
393 mode: SedaMode,
394 depth: Arc<AtomicUsize>,
414}
415
416impl SedaEndpointState {
417 fn new(config: &SedaConfig) -> Self {
418 let (tx, rx) = mpsc::channel(config.size);
419 let mode = if config.multiple_consumers {
420 SedaMode::Fanout {
421 subscribers: Mutex::new(HashMap::new()),
422 }
423 } else {
424 SedaMode::Single {
425 tx,
426 rx: Mutex::new(Some(rx)),
427 active: std::sync::atomic::AtomicBool::new(false),
428 }
429 };
430 Self {
431 config: config.clone(),
432 mode,
433 depth: Arc::new(AtomicUsize::new(0)),
434 }
435 }
436
437 fn has_active_consumers(&self) -> bool {
442 match &self.mode {
443 SedaMode::Single { active, .. } => active.load(Ordering::SeqCst),
444 SedaMode::Fanout { subscribers } => !subscribers
445 .lock()
446 .unwrap_or_else(|e| e.into_inner())
447 .is_empty(),
448 }
449 }
450}
451
452struct DepthGuard {
466 depth: Arc<AtomicUsize>,
467 count: usize,
468}
469
470impl DepthGuard {
471 fn count_in(depth: &Arc<AtomicUsize>, count: usize) -> Self {
473 depth.fetch_add(count, Ordering::AcqRel);
474 Self {
475 depth: Arc::clone(depth),
476 count,
477 }
478 }
479
480 fn claim(depth: &Arc<AtomicUsize>) -> Self {
483 Self {
484 depth: Arc::clone(depth),
485 count: 1,
486 }
487 }
488
489 fn commit(self) {
492 std::mem::forget(self);
493 }
494}
495
496impl Drop for DepthGuard {
497 fn drop(&mut self) {
498 self.depth.fetch_sub(self.count, Ordering::AcqRel);
499 }
500}
501
502fn spawn_queue_depth_sampler(
514 metrics: Arc<dyn camel_api::MetricsCollector>,
515 label: String,
516 depth: Arc<AtomicUsize>,
517 cancel: CancellationToken,
518) {
519 tokio::spawn(async move {
520 let mut tick = tokio::time::interval(QUEUE_DEPTH_SAMPLE_INTERVAL);
521 loop {
522 tokio::select! {
523 _ = cancel.cancelled() => break,
524 _ = tick.tick() => {
525 metrics.set_queue_depth(&label, depth.load(Ordering::Acquire));
526 }
527 }
528 }
529 });
530}
531
532pub fn is_no_active_consumers_gate(err: &CamelError) -> bool {
547 matches!(err, CamelError::EndpointCreationFailed(msg) if msg.contains("has no active consumers"))
548 || matches!(err, CamelError::EndpointCreationFailed(msg) if msg.contains("has no active subscribers"))
549}
550
551type SedaRegistry = Arc<Mutex<HashMap<String, Arc<SedaEndpointState>>>>;
552
553#[derive(Clone)]
557pub struct SedaComponent {
558 endpoints: SedaRegistry,
559}
560
561impl SedaComponent {
562 pub fn new() -> Self {
563 Self {
564 endpoints: Arc::new(Mutex::new(HashMap::new())),
565 }
566 }
567
568 pub fn has_active_consumer(&self, endpoint_name: &str) -> bool {
577 let endpoints = self.endpoints.lock().unwrap_or_else(|e| e.into_inner());
578 endpoints
579 .get(endpoint_name)
580 .is_some_and(|state| state.has_active_consumers())
581 }
582
583 fn get_or_create_state(
584 &self,
585 config: &SedaConfig,
586 ) -> Result<Arc<SedaEndpointState>, CamelError> {
587 let mut endpoints = self.endpoints.lock().unwrap_or_else(|e| e.into_inner());
588 if let Some(existing) = endpoints.get(&config.name) {
589 existing
590 .config
591 .is_compatible_with(config)
592 .map_err(CamelError::EndpointCreationFailed)?;
593 Ok(Arc::clone(existing))
594 } else {
595 let state = Arc::new(SedaEndpointState::new(config));
596 endpoints.insert(config.name.clone(), Arc::clone(&state));
597 Ok(state)
598 }
599 }
600}
601
602impl Default for SedaComponent {
603 fn default() -> Self {
604 Self::new()
605 }
606}
607
608#[async_trait]
609impl Component for SedaComponent {
610 fn scheme(&self) -> &str {
611 "seda"
612 }
613
614 fn metadata(&self) -> ComponentMetadata {
615 SedaConfig::metadata()
616 }
617
618 fn create_endpoint(
619 &self,
620 uri: &str,
621 _ctx: &dyn ComponentContext,
622 ) -> Result<Box<dyn Endpoint>, CamelError> {
623 let config = SedaConfig::from_uri(uri)?;
624 let state = self.get_or_create_state(&config)?;
625 Ok(Box::new(SedaEndpoint {
626 uri: uri.to_string(),
627 config,
628 state,
629 }))
630 }
631}
632
633struct SedaEndpoint {
638 uri: String,
639 config: SedaConfig,
640 state: Arc<SedaEndpointState>,
641}
642
643impl Endpoint for SedaEndpoint {
644 fn uri(&self) -> &str {
645 &self.uri
646 }
647
648 fn create_consumer(
649 &self,
650 rt: Arc<dyn camel_component_api::RuntimeObservability>,
651 ) -> Result<Box<dyn Consumer>, CamelError> {
652 Ok(Box::new(SedaConsumer::new(
653 Arc::clone(&self.state),
654 next_consumer_id(),
655 rt,
656 )))
657 }
658
659 fn create_producer(
660 &self,
661 rt: Arc<dyn camel_component_api::RuntimeObservability>,
662 _ctx: &ProducerContext,
663 ) -> Result<BoxProcessor, CamelError> {
664 let producer = SedaProducer {
665 state: Arc::clone(&self.state),
666 producer_config: ProducerConfig::from(&self.config),
667 runtime: rt,
668 };
669 Ok(BoxProcessor::from_fn(move |ex| {
670 let mut svc = producer.clone();
671 Box::pin(async move { svc.call(ex).await })
672 }))
673 }
674}
675
676#[derive(Clone)]
680struct ProducerConfig {
681 block_when_full: bool,
682 discard_if_no_consumers: bool,
683 timeout_ms: u64,
684 wait_for_task_to_complete: WaitForTaskToComplete,
685}
686
687impl From<&SedaConfig> for ProducerConfig {
688 fn from(config: &SedaConfig) -> Self {
689 Self {
690 block_when_full: config.block_when_full,
691 discard_if_no_consumers: config.discard_if_no_consumers,
692 timeout_ms: config.timeout_ms,
693 wait_for_task_to_complete: config.wait_for_task_to_complete,
694 }
695 }
696}
697
698struct SedaConsumer {
703 state: Arc<SedaEndpointState>,
704 consumer_id: ConsumerId,
705 started: bool,
706 cancel_token: CancellationToken,
707 forwarder_handles: Vec<JoinHandle<Result<(), CamelError>>>,
708 shared_rx: Option<Arc<AsyncMutex<Option<mpsc::Receiver<ExchangeEnvelope>>>>>,
715 runtime: Arc<dyn camel_component_api::RuntimeObservability>,
718}
719
720impl SedaConsumer {
721 fn new(
722 state: Arc<SedaEndpointState>,
723 consumer_id: ConsumerId,
724 runtime: Arc<dyn camel_component_api::RuntimeObservability>,
725 ) -> Self {
726 Self {
727 state,
728 consumer_id,
729 started: false,
730 cancel_token: CancellationToken::new(),
731 forwarder_handles: Vec::new(),
732 shared_rx: None,
733 runtime,
734 }
735 }
736
737 #[cfg(test)]
738 pub(crate) fn forwarder_count(&self) -> usize {
739 self.forwarder_handles.len()
740 }
741}
742
743#[async_trait]
744impl Consumer for SedaConsumer {
745 async fn start(&mut self, ctx: ConsumerContext) -> Result<(), CamelError> {
746 if self.started {
747 return Err(CamelError::EndpointCreationFailed(
748 "consumer already started".to_string(),
749 ));
750 }
751
752 match &self.state.mode {
753 SedaMode::Single { rx, active, .. } => {
754 let mut rx_guard = rx.lock().unwrap_or_else(|e| e.into_inner());
755 if rx_guard.is_none() {
756 return Err(CamelError::EndpointCreationFailed(format!(
757 "endpoint '{}' already has a registered consumer",
758 self.state.config.name
759 )));
760 }
761 active.store(true, Ordering::SeqCst);
762 let receiver = rx_guard.take().ok_or_else(|| {
763 CamelError::EndpointCreationFailed(format!(
764 "endpoint '{}' receiver already taken",
765 self.state.config.name
766 ))
767 })?;
768 drop(rx_guard);
769
770 let shared_rx = Arc::new(AsyncMutex::new(Some(receiver)));
771 self.shared_rx = Some(Arc::clone(&shared_rx));
772 let concurrent = self.state.config.concurrent_consumers;
773 let queue_metrics = self.runtime.metrics();
774 let queue_label = format!("seda:{}", self.state.config.name);
775 let depth = Arc::clone(&self.state.depth);
776
777 for _ in 0..concurrent {
778 let shared_rx = Arc::clone(&shared_rx);
779 let cancel = self.cancel_token.clone();
780 let ctx = ctx.clone();
781 let depth = Arc::clone(&depth);
782 let component_metrics = self.runtime.component_metrics();
783 let handle = tokio::spawn(async move {
784 loop {
785 let envelope = {
786 let mut guard = shared_rx.lock().await;
787 let Some(rx) = guard.as_mut() else {
788 return Ok(());
790 };
791 let env = tokio::select! {
792 env = rx.recv() => env,
793 _ = cancel.cancelled() => return Ok(()),
794 };
795 env
796 };
797 let Some(envelope) = envelope else {
798 return Ok(());
799 };
800 let _claim = DepthGuard::claim(&depth);
805 forward_envelope(&ctx, &component_metrics, envelope).await;
806 }
807 });
808 self.forwarder_handles.push(handle);
809 }
810
811 spawn_queue_depth_sampler(
819 queue_metrics,
820 queue_label,
821 depth,
822 self.cancel_token.clone(),
823 );
824 }
825 SedaMode::Fanout { subscribers } => {
826 let (tx, rx) = mpsc::channel(self.state.config.size);
827 subscribers
828 .lock()
829 .unwrap_or_else(|e| e.into_inner())
830 .insert(self.consumer_id.clone(), tx);
831
832 let cancel = self.cancel_token.clone();
833 let queue_metrics = self.runtime.metrics();
834 let queue_label = format!("seda:{}", self.state.config.name);
835 let depth = Arc::clone(&self.state.depth);
836 let sampler_depth = Arc::clone(&depth);
837 let shared_rx = Arc::new(AsyncMutex::new(Some(rx)));
838 self.shared_rx = Some(Arc::clone(&shared_rx));
839 let forwarder_rx = Arc::clone(&shared_rx);
840 let forwarder_ctx = ctx.clone();
841 let component_metrics = self.runtime.component_metrics();
842 let handle = tokio::spawn(async move {
843 loop {
844 let envelope = {
845 let mut guard = forwarder_rx.lock().await;
846 let Some(rx) = guard.as_mut() else {
847 return Ok(());
849 };
850 let env = tokio::select! {
851 env = rx.recv() => env,
852 _ = cancel.cancelled() => return Ok(()),
853 };
854 env
855 };
856 let Some(envelope) = envelope else {
857 break;
858 };
859 let _claim = DepthGuard::claim(&depth);
863 forward_envelope(&forwarder_ctx, &component_metrics, envelope).await;
864 }
865 Ok(())
866 });
867 self.forwarder_handles.push(handle);
868
869 spawn_queue_depth_sampler(
874 queue_metrics,
875 queue_label,
876 sampler_depth,
877 self.cancel_token.clone(),
878 );
879 }
880 }
881
882 self.started = true;
883 ctx.mark_ready();
892 info!(
893 name = %self.state.config.name,
894 consumer_id = %self.consumer_id,
895 concurrent = self.state.config.concurrent_consumers,
896 "SEDA consumer started"
897 );
898 Ok(())
899 }
900
901 async fn stop(&mut self) -> Result<(), CamelError> {
902 if !self.started {
903 return Ok(());
904 }
905 self.cancel_token.cancel();
906 for handle in self.forwarder_handles.drain(..) {
907 handle.abort();
908 }
909 match &self.state.mode {
910 SedaMode::Single { rx, active, .. } => {
911 active.store(false, Ordering::SeqCst);
916 if let Some(shared_rx) = self.shared_rx.take() {
917 let receiver = shared_rx.lock().await.take();
918 if let Some(recv) = receiver {
919 *rx.lock().unwrap_or_else(|e| e.into_inner()) = Some(recv);
920 }
921 }
922 }
923 SedaMode::Fanout { subscribers } => {
924 subscribers
925 .lock()
926 .unwrap_or_else(|e| e.into_inner())
927 .remove(&self.consumer_id);
928 if let Some(shared_rx) = self.shared_rx.take()
938 && let Some(mut rx) = shared_rx.lock().await.take()
939 {
940 let mut discarded = 0;
941 while rx.try_recv().is_ok() {
942 discarded += 1;
943 }
944 if discarded > 0 {
945 self.state.depth.fetch_sub(discarded, Ordering::AcqRel);
946 }
947 }
948 }
949 }
950 self.started = false;
951 info!(
952 name = %self.state.config.name,
953 consumer_id = %self.consumer_id,
954 "SEDA consumer stopped"
955 );
956 Ok(())
957 }
958
959 fn concurrency_model(&self) -> ConcurrencyModel {
960 ConcurrencyModel::Concurrent {
961 max: Some(self.state.config.concurrent_consumers),
962 }
963 }
964
965 fn startup_mode(&self) -> ConsumerStartupMode {
966 ConsumerStartupMode::Explicit
971 }
972
973 fn background_task_handle(
974 &mut self,
975 ) -> Option<tokio::task::JoinHandle<Result<(), CamelError>>> {
976 self.forwarder_handles.pop()
979 }
980}
981
982async fn forward_envelope(
994 ctx: &ConsumerContext,
995 component_metrics: &camel_api::ComponentMetrics,
996 envelope: ExchangeEnvelope,
997) {
998 if let Some(reply_tx) = envelope.reply_tx {
999 let result = ctx.send_and_wait(envelope.exchange).await;
1000 component_metrics.observe("seda", "consume", result.is_err());
1001 let _ = reply_tx.send(result);
1002 } else if let Err(e) = ctx.send(envelope.exchange).await {
1003 component_metrics.observe("seda", "consume", true);
1004 warn!(error = %e, "SEDA consumer send failed");
1005 } else {
1006 component_metrics.observe("seda", "consume", false);
1007 }
1008}
1009
1010#[derive(Clone)]
1015struct SedaProducer {
1016 state: Arc<SedaEndpointState>,
1017 producer_config: ProducerConfig,
1018 runtime: Arc<dyn camel_component_api::RuntimeObservability>,
1021}
1022
1023impl Service<Exchange> for SedaProducer {
1024 type Response = Exchange;
1025 type Error = CamelError;
1026 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
1027
1028 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1029 Poll::Ready(Ok(()))
1030 }
1031
1032 fn call(&mut self, exchange: Exchange) -> Self::Future {
1033 let state = Arc::clone(&self.state);
1034 let producer_config = self.producer_config.clone();
1035 let original = exchange.clone();
1036 let component_metrics = self.runtime.component_metrics();
1037 Box::pin(async move {
1038 let result: Result<Exchange, CamelError> = async {
1043 if !state.has_active_consumers() {
1044 if producer_config.discard_if_no_consumers {
1045 return Ok(exchange);
1046 }
1047 return Err(CamelError::EndpointCreationFailed(format!(
1048 "SEDA endpoint '{}' has no active consumers",
1049 state.config.name
1050 )));
1051 }
1052
1053 let should_wait = match producer_config.wait_for_task_to_complete {
1054 WaitForTaskToComplete::Never => false,
1055 WaitForTaskToComplete::Always => true,
1056 WaitForTaskToComplete::IfReplyExpected => {
1057 state.config.exchange_pattern == ExchangePattern::InOut
1058 }
1059 };
1060
1061 if state.config.multiple_consumers && should_wait {
1062 return Err(CamelError::EndpointCreationFailed(
1063 "multipleConsumers=true with waitForTaskToComplete != Never \
1064 is not supported — a single request cannot have N valid \
1065 replies without aggregator semantics"
1066 .to_string(),
1067 ));
1068 }
1069
1070 let (reply_tx, reply_rx) = if should_wait {
1071 let (tx, rx) = oneshot::channel();
1072 (Some(tx), Some(rx))
1073 } else {
1074 (None, None)
1075 };
1076
1077 let envelope = ExchangeEnvelope { exchange, reply_tx };
1078
1079 match &state.mode {
1080 SedaMode::Single { tx, .. } => {
1081 let guard = DepthGuard::count_in(&state.depth, 1);
1086 if producer_config.block_when_full {
1087 let result = tokio::time::timeout(
1088 Duration::from_millis(producer_config.timeout_ms),
1089 tx.send(envelope),
1090 )
1091 .await;
1092 match result {
1093 Ok(Ok(())) => guard.commit(),
1094 Ok(Err(_)) => {
1095 return Err(CamelError::ChannelClosed);
1096 }
1097 Err(_) => {
1098 return Err(CamelError::EndpointCreationFailed(format!(
1099 "SEDA producer timeout enqueueing on '{}' ({}ms)",
1100 state.config.name, producer_config.timeout_ms
1101 )));
1102 }
1103 }
1104 } else {
1105 if let Err(e) = tx.try_send(envelope) {
1106 return Err(match e {
1107 mpsc::error::TrySendError::Full(_) => {
1108 CamelError::EndpointCreationFailed(format!(
1109 "SEDA queue '{}' is full (size={})",
1110 state.config.name, state.config.size
1111 ))
1112 }
1113 _ => CamelError::ChannelClosed,
1114 });
1115 }
1116 guard.commit();
1117 }
1118 }
1119 SedaMode::Fanout { subscribers } => {
1120 let sender_list: Vec<mpsc::Sender<ExchangeEnvelope>> = {
1121 let subs_guard = subscribers.lock().unwrap_or_else(|e| e.into_inner());
1122 if subs_guard.is_empty() {
1123 if producer_config.discard_if_no_consumers {
1124 return Ok(original);
1125 }
1126 return Err(CamelError::EndpointCreationFailed(format!(
1127 "SEDA endpoint '{}' has no active subscribers",
1128 state.config.name
1129 )));
1130 }
1131 subs_guard.values().cloned().collect()
1132 };
1133
1134 if producer_config.block_when_full {
1135 let mut permits: Vec<mpsc::OwnedPermit<ExchangeEnvelope>> =
1136 Vec::with_capacity(sender_list.len());
1137 for sender in &sender_list {
1138 let result = tokio::time::timeout(
1139 Duration::from_millis(producer_config.timeout_ms),
1140 sender.clone().reserve_owned(),
1141 )
1142 .await;
1143 match result {
1144 Ok(Ok(permit)) => permits.push(permit),
1145 Ok(Err(_)) => return Err(CamelError::ChannelClosed),
1146 Err(_) => {
1147 return Err(CamelError::EndpointCreationFailed(format!(
1148 "SEDA fanout timeout on '{}' ({}ms)",
1149 state.config.name, producer_config.timeout_ms
1150 )));
1151 }
1152 }
1153 }
1154 let guard = DepthGuard::count_in(&state.depth, permits.len());
1158 for permit in permits {
1159 permit.send(ExchangeEnvelope {
1160 exchange: original.clone(),
1161 reply_tx: None,
1162 });
1163 }
1164 guard.commit();
1165 } else {
1166 let mut permits: Vec<mpsc::OwnedPermit<ExchangeEnvelope>> =
1167 Vec::with_capacity(sender_list.len());
1168 for sender in &sender_list {
1169 match sender.clone().try_reserve_owned() {
1170 Ok(permit) => permits.push(permit),
1171 Err(e) => {
1172 if matches!(e, mpsc::error::TrySendError::Full(_)) {
1173 return Err(CamelError::EndpointCreationFailed(format!(
1174 "SEDA queue '{}' subscriber full during fanout (size={})",
1175 state.config.name, state.config.size
1176 )));
1177 } else {
1178 return Err(CamelError::ChannelClosed);
1179 }
1180 }
1181 }
1182 }
1183 let guard = DepthGuard::count_in(&state.depth, permits.len());
1184 for permit in permits {
1185 permit.send(ExchangeEnvelope {
1186 exchange: original.clone(),
1187 reply_tx: None,
1188 });
1189 }
1190 guard.commit();
1191 }
1192 }
1193 }
1194
1195 if !should_wait {
1196 return Ok(original);
1197 }
1198
1199 let reply_rx = reply_rx.ok_or(CamelError::ChannelClosed)?;
1200 let result =
1201 tokio::time::timeout(Duration::from_millis(producer_config.timeout_ms), reply_rx)
1202 .await;
1203 match result {
1204 Ok(Ok(reply)) => reply,
1205 Ok(Err(_)) => Err(CamelError::ChannelClosed),
1206 Err(_) => Err(CamelError::EndpointCreationFailed(format!(
1207 "SEDA producer timeout waiting for reply on '{}' ({}ms)",
1208 state.config.name, producer_config.timeout_ms
1209 ))),
1210 }
1211 }
1212 .await;
1213 component_metrics.observe("seda", "produce", result.is_err());
1216 result
1217 })
1218 }
1219}
1220
1221#[cfg(test)]
1226mod config_tests {
1227 use super::*;
1228
1229 #[test]
1230 fn test_seda_config_from_uri_minimal() {
1231 let config = SedaConfig::from_uri("seda:foo").unwrap();
1232 assert_eq!(config.name, "foo");
1233 assert_eq!(config.size, 1000);
1234 assert_eq!(config.concurrent_consumers, 1);
1235 assert!(!config.multiple_consumers);
1236 assert!(!config.block_when_full);
1237 assert!(!config.discard_if_no_consumers);
1238 assert_eq!(config.timeout_ms, 30_000);
1239 assert_eq!(
1240 config.wait_for_task_to_complete,
1241 WaitForTaskToComplete::IfReplyExpected
1242 );
1243 assert_eq!(config.exchange_pattern, ExchangePattern::InOnly);
1244 }
1245
1246 #[test]
1247 fn test_seda_config_from_uri_full() {
1248 let config = SedaConfig::from_uri(
1249 "seda:bar?size=500&concurrentConsumers=4&multipleConsumers=true\
1250 &blockWhenFull=true&discardIfNoConsumers=false&timeout=5000\
1251 &waitForTaskToComplete=Never&exchangePattern=InOut",
1252 )
1253 .unwrap();
1254 assert_eq!(config.name, "bar");
1255 assert_eq!(config.size, 500);
1256 assert_eq!(config.concurrent_consumers, 4);
1257 assert!(config.multiple_consumers);
1258 assert!(config.block_when_full);
1259 assert!(!config.discard_if_no_consumers);
1260 assert_eq!(config.timeout_ms, 5000);
1261 assert_eq!(
1262 config.wait_for_task_to_complete,
1263 WaitForTaskToComplete::Never
1264 );
1265 assert_eq!(config.exchange_pattern, ExchangePattern::InOut);
1266 }
1267
1268 #[test]
1269 fn test_seda_config_invalid_scheme() {
1270 let err = SedaConfig::from_uri("timer:foo").unwrap_err();
1271 assert!(err.to_string().contains("expected 'seda'"));
1272 }
1273
1274 #[test]
1275 fn test_seda_config_empty_name() {
1276 let err = SedaConfig::from_uri("seda:").unwrap_err();
1277 assert!(err.to_string().contains("must not be empty"));
1278 }
1279
1280 #[test]
1281 fn test_seda_size_zero() {
1282 let err = SedaConfig::from_uri("seda:foo?size=0").unwrap_err();
1283 assert!(err.to_string().contains("size must be greater than 0"));
1284 }
1285
1286 #[test]
1287 fn test_seda_config_concurrent_consumers_zero_clamped() {
1288 let config = SedaConfig::from_uri("seda:foo?concurrentConsumers=0").unwrap();
1289 assert_eq!(config.concurrent_consumers, 1);
1290 }
1291
1292 #[test]
1293 fn test_seda_config_case_insensitive_enums() {
1294 let config =
1295 SedaConfig::from_uri("seda:foo?waitForTaskToComplete=never&exchangePattern=inonly")
1296 .unwrap();
1297 assert_eq!(
1298 config.wait_for_task_to_complete,
1299 WaitForTaskToComplete::Never
1300 );
1301 assert_eq!(config.exchange_pattern, ExchangePattern::InOnly);
1302 }
1303
1304 #[test]
1305 fn test_seda_config_invalid_enum() {
1306 let err = SedaConfig::from_uri("seda:foo?exchangePattern=invalid").unwrap_err();
1307 assert!(err.to_string().contains("invalid exchangePattern"));
1308 }
1309
1310 #[test]
1311 fn uri_options_count_parity() {
1312 assert_eq!(
1313 SedaConfig::uri_options().len(),
1314 8,
1315 "SedaUriConfig #[uri_param] count drifted from parser"
1316 );
1317 }
1318}
1319
1320#[cfg(test)]
1321mod consumer_producer_tests {
1322 use super::*;
1323 use camel_api::Value;
1324 use camel_component_api::Message;
1325 use camel_component_api::NoOpComponentContext;
1326 use tokio::time::Duration;
1327 use tower::ServiceExt;
1328
1329 fn test_producer_ctx() -> ProducerContext {
1330 ProducerContext::default()
1331 }
1332
1333 fn create_component() -> SedaComponent {
1334 SedaComponent::new()
1335 }
1336
1337 #[tokio::test]
1338 async fn test_seda_single_consumer_producer_roundtrip() {
1339 let comp = create_component();
1340 let ep = comp
1341 .create_endpoint("seda:test1", &NoOpComponentContext)
1342 .unwrap();
1343
1344 let mut consumer = ep.create_consumer(rt()).unwrap();
1345 let (route_tx, mut route_rx) = mpsc::channel::<ExchangeEnvelope>(16);
1346 let ctx = ConsumerContext::new(
1347 route_tx,
1348 CancellationToken::new(),
1349 "seda-test-route".to_string(),
1350 );
1351 consumer.start(ctx).await.unwrap();
1352
1353 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1354 let exchange = Exchange::new(Message::new("hello seda"));
1355 let result = producer.oneshot(exchange).await;
1356 assert!(result.is_ok());
1357
1358 let received = tokio::time::timeout(Duration::from_millis(500), route_rx.recv())
1359 .await
1360 .unwrap()
1361 .unwrap();
1362 assert_eq!(received.exchange.input.body.as_text(), Some("hello seda"));
1363
1364 consumer.stop().await.unwrap();
1365 }
1366
1367 #[tokio::test]
1368 async fn test_seda_inout_roundtrip() {
1369 let comp = create_component();
1370 let ep = comp
1371 .create_endpoint("seda:io?exchangePattern=InOut", &NoOpComponentContext)
1372 .unwrap();
1373
1374 let mut consumer = ep.create_consumer(rt()).unwrap();
1375 let (route_tx, _) = mpsc::channel::<ExchangeEnvelope>(16);
1376 let ctx = ConsumerContext::new(
1377 route_tx,
1378 CancellationToken::new(),
1379 "seda-test-route".to_string(),
1380 );
1381 consumer.start(ctx).await.unwrap();
1382
1383 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1384 let exchange = Exchange::new(Message::new("io test"));
1385
1386 let result =
1387 tokio::time::timeout(Duration::from_millis(500), producer.oneshot(exchange)).await;
1388 assert!(result.is_err() || result.unwrap().is_err());
1389
1390 consumer.stop().await.unwrap();
1391 }
1392
1393 #[tokio::test]
1394 async fn test_seda_inonly_fire_and_forget() {
1395 let comp = create_component();
1396 let ep = comp
1397 .create_endpoint("seda:ff", &NoOpComponentContext)
1398 .unwrap();
1399
1400 let mut consumer = ep.create_consumer(rt()).unwrap();
1401 let (route_tx, _route_rx) = mpsc::channel::<ExchangeEnvelope>(16);
1402 let ctx = ConsumerContext::new(
1403 route_tx,
1404 CancellationToken::new(),
1405 "seda-test-route".to_string(),
1406 );
1407 consumer.start(ctx).await.unwrap();
1408
1409 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1410 let exchange = Exchange::new(Message::new("fire and forget"));
1411 let result = producer.oneshot(exchange).await;
1412 assert!(result.is_ok());
1413
1414 consumer.stop().await.unwrap();
1415 }
1416
1417 #[tokio::test]
1418 async fn test_seda_queue_full_fail() {
1419 let comp = create_component();
1420 let ep = comp
1421 .create_endpoint("seda:full?size=2", &NoOpComponentContext)
1422 .unwrap();
1423
1424 let mut consumer = ep.create_consumer(rt()).unwrap();
1425 let (route_tx, _route_rx) = mpsc::channel::<ExchangeEnvelope>(16);
1426 let ctx = ConsumerContext::new(
1427 route_tx,
1428 CancellationToken::new(),
1429 "seda-test-route".to_string(),
1430 );
1431 consumer.start(ctx).await.unwrap();
1432
1433 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1434 producer
1435 .clone()
1436 .oneshot(Exchange::new(Message::new("1")))
1437 .await
1438 .unwrap();
1439 producer
1440 .clone()
1441 .oneshot(Exchange::new(Message::new("2")))
1442 .await
1443 .unwrap();
1444
1445 let result = producer.oneshot(Exchange::new(Message::new("3"))).await;
1446 assert!(result.is_err());
1447 assert!(result.unwrap_err().to_string().contains("full"));
1448
1449 consumer.stop().await.unwrap();
1450 }
1451
1452 #[tokio::test]
1453 async fn test_seda_block_when_full_with_timeout() {
1454 let comp = create_component();
1455 let ep = comp
1456 .create_endpoint(
1457 "seda:bwf?size=1&blockWhenFull=true&timeout=50",
1458 &NoOpComponentContext,
1459 )
1460 .unwrap();
1461
1462 let mut consumer = ep.create_consumer(rt()).unwrap();
1463 let (route_tx, _route_rx) = mpsc::channel::<ExchangeEnvelope>(1);
1464 route_tx
1465 .send(ExchangeEnvelope {
1466 exchange: Exchange::new(Message::new("dummy")),
1467 reply_tx: None,
1468 })
1469 .await
1470 .unwrap();
1471 let ctx = ConsumerContext::new(
1472 route_tx,
1473 CancellationToken::new(),
1474 "seda-test-route".to_string(),
1475 );
1476 consumer.start(ctx).await.unwrap();
1477
1478 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1479 producer
1480 .clone()
1481 .oneshot(Exchange::new(Message::new("1")))
1482 .await
1483 .unwrap();
1484
1485 producer
1486 .clone()
1487 .oneshot(Exchange::new(Message::new("2")))
1488 .await
1489 .unwrap();
1490
1491 let result = tokio::time::timeout(
1492 Duration::from_millis(200),
1493 producer.oneshot(Exchange::new(Message::new("3"))),
1494 )
1495 .await;
1496 assert!(result.is_ok());
1497 let inner = result.unwrap();
1498 assert!(inner.is_err());
1499 assert!(inner.unwrap_err().to_string().contains("timeout"));
1500
1501 consumer.stop().await.unwrap();
1502 }
1503
1504 #[tokio::test]
1505 async fn test_seda_no_consumers_fail() {
1506 let comp = create_component();
1507 let ep = comp
1508 .create_endpoint("seda:nocons", &NoOpComponentContext)
1509 .unwrap();
1510
1511 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1512 let result = producer.oneshot(Exchange::new(Message::new("test"))).await;
1513 assert!(result.is_err());
1514 assert!(
1515 result
1516 .unwrap_err()
1517 .to_string()
1518 .contains("no active consumers")
1519 );
1520 }
1521
1522 #[tokio::test]
1523 async fn test_seda_no_consumers_discard() {
1524 let comp = create_component();
1525 let ep = comp
1526 .create_endpoint(
1527 "seda:discard?discardIfNoConsumers=true",
1528 &NoOpComponentContext,
1529 )
1530 .unwrap();
1531
1532 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1533 let result = producer.oneshot(Exchange::new(Message::new("test"))).await;
1534 assert!(result.is_ok());
1535 }
1536
1537 #[tokio::test]
1538 async fn test_seda_duplicate_single_consumer() {
1539 let comp = create_component();
1540 let ep = comp
1541 .create_endpoint("seda:dup", &NoOpComponentContext)
1542 .unwrap();
1543
1544 let mut consumer_a = ep.create_consumer(rt()).unwrap();
1545 let (tx_a, _rx_a) = mpsc::channel::<ExchangeEnvelope>(16);
1546 let ctx_a = ConsumerContext::new(
1547 tx_a,
1548 CancellationToken::new(),
1549 "seda-test-route-a".to_string(),
1550 );
1551 consumer_a.start(ctx_a).await.unwrap();
1552
1553 let mut consumer_b = ep.create_consumer(rt()).unwrap();
1554 let (tx_b, _rx_b) = mpsc::channel::<ExchangeEnvelope>(16);
1555 let ctx_b = ConsumerContext::new(
1556 tx_b,
1557 CancellationToken::new(),
1558 "seda-test-route-b".to_string(),
1559 );
1560 let result = consumer_b.start(ctx_b).await;
1561 assert!(result.is_err());
1562 assert!(
1563 result
1564 .unwrap_err()
1565 .to_string()
1566 .contains("already has a registered consumer")
1567 );
1568
1569 consumer_a.stop().await.unwrap();
1570 }
1571
1572 #[tokio::test]
1573 async fn test_seda_fanout_two_consumers() {
1574 let comp = create_component();
1575 let ep = comp
1576 .create_endpoint("seda:fan?multipleConsumers=true", &NoOpComponentContext)
1577 .unwrap();
1578
1579 let mut consumer_a = ep.create_consumer(rt()).unwrap();
1580 let (tx_a, mut rx_a) = mpsc::channel::<ExchangeEnvelope>(16);
1581 let ctx_a = ConsumerContext::new(
1582 tx_a,
1583 CancellationToken::new(),
1584 "seda-test-route-a".to_string(),
1585 );
1586 consumer_a.start(ctx_a).await.unwrap();
1587
1588 let mut consumer_b = ep.create_consumer(rt()).unwrap();
1589 let (tx_b, mut rx_b) = mpsc::channel::<ExchangeEnvelope>(16);
1590 let ctx_b = ConsumerContext::new(
1591 tx_b,
1592 CancellationToken::new(),
1593 "seda-test-route-b".to_string(),
1594 );
1595 consumer_b.start(ctx_b).await.unwrap();
1596
1597 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1598 producer
1599 .oneshot(Exchange::new(Message::new("fanout msg")))
1600 .await
1601 .unwrap();
1602
1603 let recv_a = tokio::time::timeout(Duration::from_millis(500), rx_a.recv())
1604 .await
1605 .unwrap()
1606 .unwrap();
1607 let recv_b = tokio::time::timeout(Duration::from_millis(500), rx_b.recv())
1608 .await
1609 .unwrap()
1610 .unwrap();
1611
1612 assert_eq!(recv_a.exchange.input.body.as_text(), Some("fanout msg"));
1613 assert_eq!(recv_b.exchange.input.body.as_text(), Some("fanout msg"));
1614
1615 consumer_a.stop().await.unwrap();
1616 consumer_b.stop().await.unwrap();
1617 }
1618
1619 #[tokio::test]
1620 async fn test_seda_fanout_inout_rejected() {
1621 let comp = create_component();
1622 let ep = comp
1623 .create_endpoint(
1624 "seda:fanout?multipleConsumers=true&exchangePattern=InOut",
1625 &NoOpComponentContext,
1626 )
1627 .unwrap();
1628
1629 let mut consumer = ep.create_consumer(rt()).unwrap();
1630 let (tx, _rx) = mpsc::channel::<ExchangeEnvelope>(16);
1631 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
1632 consumer.start(ctx).await.unwrap();
1633
1634 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1635 let result = producer.oneshot(Exchange::new(Message::new("test"))).await;
1636 assert!(result.is_err());
1637 assert!(
1638 result
1639 .unwrap_err()
1640 .to_string()
1641 .contains("multipleConsumers")
1642 );
1643
1644 consumer.stop().await.unwrap();
1645 }
1646
1647 #[tokio::test]
1648 async fn test_seda_consumer_stop_unregisters() {
1649 let comp = create_component();
1650 let ep = comp
1651 .create_endpoint("seda:stop", &NoOpComponentContext)
1652 .unwrap();
1653
1654 let mut consumer = ep.create_consumer(rt()).unwrap();
1655 let (tx, _rx) = mpsc::channel::<ExchangeEnvelope>(16);
1656 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
1657 consumer.start(ctx).await.unwrap();
1658
1659 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1660 producer
1661 .clone()
1662 .oneshot(Exchange::new(Message::new("before stop")))
1663 .await
1664 .unwrap();
1665
1666 consumer.stop().await.unwrap();
1667
1668 tokio::time::sleep(Duration::from_millis(50)).await;
1669
1670 let result = producer
1671 .oneshot(Exchange::new(Message::new("after stop")))
1672 .await;
1673 assert!(result.is_err());
1674 assert!(
1675 result
1676 .unwrap_err()
1677 .to_string()
1678 .contains("no active consumers")
1679 );
1680 }
1681
1682 #[test]
1683 fn test_seda_concurrent_consumers_hint() {
1684 let comp = create_component();
1685 let ep = comp
1686 .create_endpoint("seda:conc?concurrentConsumers=4", &NoOpComponentContext)
1687 .unwrap();
1688 let consumer = ep.create_consumer(rt()).unwrap();
1689 assert_eq!(
1690 consumer.concurrency_model(),
1691 ConcurrencyModel::Concurrent { max: Some(4) }
1692 );
1693 }
1694
1695 fn started_consumer_ctx() -> (
1698 ConsumerContext,
1699 camel_component_api::StartupReceiver,
1700 mpsc::Receiver<ExchangeEnvelope>,
1701 ) {
1702 let (signal, receiver) = camel_component_api::StartupSignal::pair();
1703 let (tx, rx) = mpsc::channel::<ExchangeEnvelope>(16);
1704 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string())
1705 .with_startup(signal);
1706 (ctx, receiver, rx)
1707 }
1708
1709 #[test]
1710 fn test_seda_consumer_startup_mode_is_explicit() {
1711 let comp = create_component();
1712 let ep = comp
1713 .create_endpoint("seda:explicit", &NoOpComponentContext)
1714 .unwrap();
1715 let consumer = ep.create_consumer(rt()).unwrap();
1716 assert_eq!(
1717 consumer.startup_mode(),
1718 ConsumerStartupMode::Explicit,
1719 "seda consumers must gate route startup on activation"
1720 );
1721 }
1722
1723 #[tokio::test]
1724 async fn test_seda_start_signals_readiness_after_activation_single() {
1725 let comp = create_component();
1726 let ep = comp
1727 .create_endpoint("seda:ready", &NoOpComponentContext)
1728 .unwrap();
1729
1730 let mut consumer = ep.create_consumer(rt()).unwrap();
1731 let (ctx, receiver, _route_rx) = started_consumer_ctx();
1732 consumer.start(ctx).await.unwrap();
1733
1734 receiver
1736 .await_ready()
1737 .await
1738 .expect("readiness must be signalled after activation");
1739 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1742 producer
1743 .clone()
1744 .oneshot(Exchange::new(Message::new("activated")))
1745 .await
1746 .expect("send after start must pass the pre-enqueue gate");
1747
1748 consumer.stop().await.unwrap();
1749 }
1750
1751 #[tokio::test]
1752 async fn test_seda_start_signals_readiness_after_registration_fanout() {
1753 let comp = create_component();
1754 let ep = comp
1755 .create_endpoint(
1756 "seda:fanready?multipleConsumers=true",
1757 &NoOpComponentContext,
1758 )
1759 .unwrap();
1760
1761 let mut consumer = ep.create_consumer(rt()).unwrap();
1762 let (ctx, receiver, mut route_rx) = started_consumer_ctx();
1763 consumer.start(ctx).await.unwrap();
1764
1765 receiver
1766 .await_ready()
1767 .await
1768 .expect("readiness must be signalled after activation");
1769 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1772 producer
1773 .oneshot(Exchange::new(Message::new("fan registered")))
1774 .await
1775 .expect("fanout send after start must find the registered subscriber");
1776 let forwarded = tokio::time::timeout(Duration::from_millis(500), route_rx.recv())
1777 .await
1778 .unwrap()
1779 .unwrap();
1780 assert_eq!(
1781 forwarded.exchange.input.body.as_text(),
1782 Some("fan registered")
1783 );
1784
1785 consumer.stop().await.unwrap();
1786 }
1787
1788 #[tokio::test]
1789 async fn test_seda_start_error_does_not_signal_readiness() {
1790 let comp = create_component();
1791 let ep = comp
1792 .create_endpoint("seda:noready", &NoOpComponentContext)
1793 .unwrap();
1794
1795 let mut first = ep.create_consumer(rt()).unwrap();
1797 let (ctx, _receiver, _route_rx) = started_consumer_ctx();
1798 first.start(ctx).await.unwrap();
1799
1800 let mut second = ep.create_consumer(rt()).unwrap();
1802 let (ctx, receiver, _route_rx) = started_consumer_ctx();
1803 let result = second.start(ctx).await;
1804 assert!(result.is_err(), "duplicate Single consumer must fail start");
1805
1806 let outcome = tokio::time::timeout(Duration::from_millis(50), receiver.await_ready()).await;
1810 assert!(
1811 !matches!(&outcome, Ok(Ok(()))),
1812 "error path must not signal readiness, got {outcome:?}"
1813 );
1814
1815 first.stop().await.unwrap();
1816 }
1817
1818 #[tokio::test]
1819 async fn test_seda_restart_signals_readiness_and_flows_buffered() {
1820 let comp = create_component();
1821 let ep = comp
1822 .create_endpoint("seda:restart", &NoOpComponentContext)
1823 .unwrap();
1824
1825 let mut first = ep.create_consumer(rt()).unwrap();
1826 let (ctx, _receiver, mut rx1) = started_consumer_ctx();
1827 first.start(ctx).await.unwrap();
1828
1829 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1830 producer
1831 .clone()
1832 .oneshot(Exchange::new(Message::new("survivor")))
1833 .await
1834 .unwrap();
1835
1836 first.stop().await.unwrap();
1837
1838 let mut second = ep.create_consumer(rt()).unwrap();
1842 let (ctx, receiver, mut rx2) = started_consumer_ctx();
1843 second.start(ctx).await.unwrap();
1844 receiver
1845 .await_ready()
1846 .await
1847 .expect("restart readiness must be signalled");
1848 producer
1851 .clone()
1852 .oneshot(Exchange::new(Message::new("after restart")))
1853 .await
1854 .expect("send after restart must pass the pre-enqueue gate");
1855
1856 let delivered = tokio::time::timeout(Duration::from_millis(500), async {
1860 loop {
1861 tokio::select! {
1862 env = rx1.recv() => {
1863 if let Some(env) = env
1864 && env.exchange.input.body.as_text() == Some("survivor")
1865 {
1866 return true;
1867 }
1868 }
1869 env = rx2.recv() => {
1870 if let Some(env) = env
1871 && env.exchange.input.body.as_text() == Some("survivor")
1872 {
1873 return true;
1874 }
1875 }
1876 }
1877 }
1878 })
1879 .await
1880 .expect("buffered envelope must survive the restart");
1881 assert!(delivered);
1882
1883 second.stop().await.unwrap();
1884 }
1885
1886 #[tokio::test]
1887 async fn test_seda_config_mismatch() {
1888 let comp = create_component();
1889 let _ep1 = comp
1890 .create_endpoint("seda:mm?size=100", &NoOpComponentContext)
1891 .unwrap();
1892 let result = comp.create_endpoint("seda:mm?size=200", &NoOpComponentContext);
1893 let err = match result {
1894 Err(e) => e,
1895 Ok(_) => panic!("expected config mismatch error"),
1896 };
1897 assert!(err.to_string().contains("size"));
1898 }
1899
1900 #[tokio::test]
1901 async fn test_seda_wait_always_inonly() {
1902 let comp = create_component();
1903 let ep = comp
1904 .create_endpoint(
1905 "seda:waitalways?waitForTaskToComplete=Always",
1906 &NoOpComponentContext,
1907 )
1908 .unwrap();
1909
1910 let mut consumer = ep.create_consumer(rt()).unwrap();
1911 let (route_tx, _) = mpsc::channel::<ExchangeEnvelope>(16);
1912 let ctx = ConsumerContext::new(
1913 route_tx,
1914 CancellationToken::new(),
1915 "seda-test-route".to_string(),
1916 );
1917 consumer.start(ctx).await.unwrap();
1918
1919 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1920 let result = tokio::time::timeout(
1921 Duration::from_millis(500),
1922 producer.oneshot(Exchange::new(Message::new("always wait"))),
1923 )
1924 .await;
1925 assert!(result.is_err() || result.unwrap().is_err());
1926
1927 consumer.stop().await.unwrap();
1928 }
1929
1930 #[tokio::test]
1931 async fn test_seda_fanout_all_or_nothing() {
1932 let comp = create_component();
1933 let ep = comp
1934 .create_endpoint(
1935 "seda:aon?multipleConsumers=true&size=2",
1936 &NoOpComponentContext,
1937 )
1938 .unwrap();
1939
1940 let mut consumer_a = ep.create_consumer(rt()).unwrap();
1941 let (tx_a, _rx_a) = mpsc::channel::<ExchangeEnvelope>(1);
1942 let ctx_a = ConsumerContext::new(
1943 tx_a,
1944 CancellationToken::new(),
1945 "seda-test-route-a".to_string(),
1946 );
1947 consumer_a.start(ctx_a).await.unwrap();
1948
1949 let mut consumer_b = ep.create_consumer(rt()).unwrap();
1950 let (tx_b, _rx_b) = mpsc::channel::<ExchangeEnvelope>(1);
1951 let ctx_b = ConsumerContext::new(
1952 tx_b,
1953 CancellationToken::new(),
1954 "seda-test-route-b".to_string(),
1955 );
1956 consumer_b.start(ctx_b).await.unwrap();
1957
1958 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
1959 producer
1960 .clone()
1961 .oneshot(Exchange::new(Message::new("1")))
1962 .await
1963 .unwrap();
1964 producer
1965 .clone()
1966 .oneshot(Exchange::new(Message::new("2")))
1967 .await
1968 .unwrap();
1969
1970 let result = producer.oneshot(Exchange::new(Message::new("3"))).await;
1971 assert!(result.is_err());
1972 let err_msg = result.unwrap_err().to_string();
1973 assert!(err_msg.contains("full") || err_msg.contains("subscriber"));
1974
1975 consumer_a.stop().await.unwrap();
1976 consumer_b.stop().await.unwrap();
1977 }
1978
1979 #[tokio::test]
1980 async fn test_seda_fanout_block_when_full_rejects_closed_subscriber_without_partial_delivery() {
1981 let comp = create_component();
1982 let ep = comp
1983 .create_endpoint(
1984 "seda:aonblock?multipleConsumers=true&size=2&blockWhenFull=true&timeout=100",
1985 &NoOpComponentContext,
1986 )
1987 .unwrap();
1988
1989 let mut consumer_a = ep.create_consumer(rt()).unwrap();
1990 let (tx_a, mut rx_a) = mpsc::channel::<ExchangeEnvelope>(1);
1991 let ctx_a = ConsumerContext::new(
1992 tx_a,
1993 CancellationToken::new(),
1994 "seda-test-route-a".to_string(),
1995 );
1996 consumer_a.start(ctx_a).await.unwrap();
1997
1998 let state = comp
1999 .endpoints
2000 .lock()
2001 .unwrap_or_else(|e| e.into_inner())
2002 .get("aonblock")
2003 .cloned()
2004 .unwrap();
2005 let (closed_tx, closed_rx) = mpsc::channel::<ExchangeEnvelope>(1);
2006 drop(closed_rx);
2007 match &state.mode {
2008 SedaMode::Fanout { subscribers } => {
2009 subscribers
2010 .lock()
2011 .unwrap_or_else(|e| e.into_inner())
2012 .insert("closed-subscriber".to_string(), closed_tx);
2013 }
2014 SedaMode::Single { .. } => panic!("expected fanout mode"),
2015 }
2016
2017 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2018 let result = producer
2019 .oneshot(Exchange::new(Message::new("partial")))
2020 .await;
2021
2022 assert!(matches!(result, Err(CamelError::ChannelClosed)));
2023 let delivered = tokio::time::timeout(Duration::from_millis(50), rx_a.recv()).await;
2024 assert!(
2025 delivered.is_err(),
2026 "fanout delivered to only one subscriber"
2027 );
2028
2029 consumer_a.stop().await.unwrap();
2030 }
2031
2032 #[tokio::test]
2033 async fn test_seda_discard_if_no_consumers_fanout() {
2034 let comp = create_component();
2035 let ep = comp
2036 .create_endpoint(
2037 "seda:discardfan?multipleConsumers=true&discardIfNoConsumers=true",
2038 &NoOpComponentContext,
2039 )
2040 .unwrap();
2041
2042 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2043 let result = producer
2044 .oneshot(Exchange::new(Message::new("discard")))
2045 .await;
2046 assert!(result.is_ok());
2047 }
2048
2049 #[tokio::test]
2050 async fn test_seda_multiple_producers_single_consumer() {
2051 let comp = create_component();
2052 let ep = comp
2053 .create_endpoint("seda:mpsc", &NoOpComponentContext)
2054 .unwrap();
2055
2056 let mut consumer = ep.create_consumer(rt()).unwrap();
2057 let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(16);
2058 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
2059 consumer.start(ctx).await.unwrap();
2060
2061 let producer_a = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2062 let producer_b = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2063
2064 producer_a
2065 .oneshot(Exchange::new(Message::new("A")))
2066 .await
2067 .unwrap();
2068 producer_b
2069 .oneshot(Exchange::new(Message::new("B")))
2070 .await
2071 .unwrap();
2072
2073 let mut bodies = Vec::new();
2074 for _ in 0..2 {
2075 let received = tokio::time::timeout(Duration::from_millis(500), rx.recv())
2076 .await
2077 .unwrap()
2078 .unwrap();
2079 bodies.push(received.exchange.input.body.as_text().unwrap().to_string());
2080 }
2081 bodies.sort();
2082 assert_eq!(bodies, vec!["A", "B"]);
2083
2084 consumer.stop().await.unwrap();
2085 }
2086
2087 #[tokio::test]
2088 async fn test_seda_inout_timeout_no_reply() {
2089 let comp = create_component();
2090 let ep = comp
2091 .create_endpoint(
2092 "seda:iotimeout?exchangePattern=InOut&timeout=100",
2093 &NoOpComponentContext,
2094 )
2095 .unwrap();
2096
2097 let mut consumer = ep.create_consumer(rt()).unwrap();
2098 let (tx, _rx) = mpsc::channel::<ExchangeEnvelope>(16);
2099 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
2100 consumer.start(ctx).await.unwrap();
2101
2102 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2103 let result = tokio::time::timeout(
2104 Duration::from_millis(500),
2105 producer.oneshot(Exchange::new(Message::new("no reply"))),
2106 )
2107 .await
2108 .unwrap();
2109
2110 assert!(result.is_err());
2111 assert!(result.unwrap_err().to_string().contains("timeout"));
2112
2113 consumer.stop().await.unwrap();
2114 }
2115
2116 #[tokio::test]
2117 async fn test_seda_producer_preserves_headers() {
2118 let comp = create_component();
2119 let ep = comp
2120 .create_endpoint("seda:hdr", &NoOpComponentContext)
2121 .unwrap();
2122
2123 let mut consumer = ep.create_consumer(rt()).unwrap();
2124 let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(16);
2125 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
2126 consumer.start(ctx).await.unwrap();
2127
2128 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2129 let mut msg = Message::new("with headers");
2130 msg.set_header("X-Custom", Value::String("test-value".into()));
2131 msg.set_header("X-Count", Value::Number(42.into()));
2132 producer.oneshot(Exchange::new(msg)).await.unwrap();
2133
2134 let received = tokio::time::timeout(Duration::from_millis(500), rx.recv())
2135 .await
2136 .unwrap()
2137 .unwrap();
2138
2139 assert_eq!(
2140 received.exchange.input.header("X-Custom"),
2141 Some(&Value::String("test-value".into()))
2142 );
2143 assert_eq!(
2144 received.exchange.input.header("X-Count"),
2145 Some(&Value::Number(42.into()))
2146 );
2147
2148 consumer.stop().await.unwrap();
2149 }
2150
2151 #[tokio::test]
2152 async fn test_seda_concurrent_send_receive() {
2153 use std::sync::atomic::AtomicU64;
2154
2155 let comp = create_component();
2156 let ep = comp
2157 .create_endpoint("seda:concsend?size=1000", &NoOpComponentContext)
2158 .unwrap();
2159
2160 let mut consumer = ep.create_consumer(rt()).unwrap();
2161 let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(1000);
2162 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
2163 consumer.start(ctx).await.unwrap();
2164
2165 let counter = Arc::new(AtomicU64::new(0));
2166 let counter_clone = counter.clone();
2167 let recv_handle = tokio::spawn(async move {
2168 while let Some(envelope) = rx.recv().await {
2169 counter_clone.fetch_add(1, Ordering::SeqCst);
2170 let _ = envelope;
2171 }
2172 });
2173
2174 let mut handles = Vec::new();
2175 for i in 0..10u64 {
2176 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2177 handles.push(tokio::spawn(async move {
2178 for j in 0..10u64 {
2179 producer
2180 .clone()
2181 .oneshot(Exchange::new(Message::new(format!("{}-{}", i, j))))
2182 .await
2183 .unwrap();
2184 }
2185 }));
2186 }
2187
2188 for h in handles {
2189 h.await.unwrap();
2190 }
2191
2192 tokio::time::timeout(Duration::from_secs(2), async {
2193 loop {
2194 if counter.load(Ordering::SeqCst) == 100 {
2195 break;
2196 }
2197 tokio::time::sleep(Duration::from_millis(10)).await;
2198 }
2199 })
2200 .await
2201 .unwrap();
2202
2203 recv_handle.abort();
2204 assert_eq!(counter.load(Ordering::SeqCst), 100);
2205
2206 consumer.stop().await.unwrap();
2207 }
2208
2209 #[tokio::test]
2210 async fn test_seda_size_one_queue() {
2211 let comp = create_component();
2212 let ep = comp
2213 .create_endpoint("seda:sz1?size=1", &NoOpComponentContext)
2214 .unwrap();
2215
2216 let mut consumer = ep.create_consumer(rt()).unwrap();
2217 let (tx, mut rx) = mpsc::channel::<ExchangeEnvelope>(16);
2218 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
2219 consumer.start(ctx).await.unwrap();
2220
2221 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2222 producer
2223 .clone()
2224 .oneshot(Exchange::new(Message::new("1")))
2225 .await
2226 .unwrap();
2227
2228 let result = producer
2229 .clone()
2230 .oneshot(Exchange::new(Message::new("2")))
2231 .await;
2232 assert!(result.is_err());
2233 assert!(result.unwrap_err().to_string().contains("full"));
2234
2235 let _dropped = tokio::time::timeout(Duration::from_millis(500), rx.recv())
2236 .await
2237 .unwrap()
2238 .unwrap();
2239
2240 producer
2241 .oneshot(Exchange::new(Message::new("3")))
2242 .await
2243 .unwrap();
2244
2245 consumer.stop().await.unwrap();
2246 }
2247
2248 #[tokio::test]
2249 async fn test_seda_concurrent_forwarders_count() {
2250 let comp = create_component();
2251 let _ep = comp
2252 .create_endpoint("seda:cfc?concurrentConsumers=4", &NoOpComponentContext)
2253 .unwrap();
2254
2255 let state = comp
2256 .endpoints
2257 .lock()
2258 .unwrap_or_else(|e| e.into_inner())
2259 .get("cfc")
2260 .cloned()
2261 .unwrap();
2262 let mut consumer = SedaConsumer::new(state, next_consumer_id(), rt());
2263 let (tx, _rx) = mpsc::channel::<ExchangeEnvelope>(16);
2264 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
2265 consumer.start(ctx).await.unwrap();
2266
2267 assert_eq!(consumer.forwarder_count(), 4);
2268
2269 consumer.stop().await.unwrap();
2270 }
2271
2272 #[tokio::test]
2273 async fn test_seda_concurrent_parallel_processing() {
2274 let comp = create_component();
2275 let ep = comp
2276 .create_endpoint(
2277 "seda:cpp?concurrentConsumers=2&size=10",
2278 &NoOpComponentContext,
2279 )
2280 .unwrap();
2281
2282 let (route_tx, mut route_rx) = mpsc::channel::<ExchangeEnvelope>(16);
2284 let mut consumer = ep.create_consumer(rt()).unwrap();
2285 let ctx = ConsumerContext::new(
2286 route_tx,
2287 CancellationToken::new(),
2288 "seda-test-route".to_string(),
2289 );
2290 consumer.start(ctx).await.unwrap();
2291
2292 tokio::spawn(async move {
2295 while let Some(envelope) = route_rx.recv().await {
2296 tokio::spawn(async move {
2297 tokio::time::sleep(Duration::from_millis(200)).await;
2298 if let Some(reply_tx) = envelope.reply_tx {
2299 let _ = reply_tx.send(Ok(envelope.exchange));
2300 }
2301 });
2302 }
2303 });
2304
2305 let state = comp
2307 .endpoints
2308 .lock()
2309 .unwrap_or_else(|e| e.into_inner())
2310 .get("cpp")
2311 .cloned()
2312 .unwrap();
2313 let mut reply_rxs = Vec::new();
2314 match &state.mode {
2315 SedaMode::Single { tx, .. } => {
2316 for i in 0..2u32 {
2317 let (reply_tx, reply_rx) = oneshot::channel();
2318 tx.send(ExchangeEnvelope {
2319 exchange: Exchange::new(Message::new(format!("msg-{}", i))),
2320 reply_tx: Some(reply_tx),
2321 })
2322 .await
2323 .unwrap();
2324 reply_rxs.push(reply_rx);
2325 }
2326 }
2327 SedaMode::Fanout { .. } => panic!("expected single mode"),
2328 }
2329
2330 let result = tokio::time::timeout(Duration::from_millis(300), async {
2332 for reply_rx in reply_rxs {
2333 let _ = reply_rx.await.unwrap().unwrap();
2334 }
2335 })
2336 .await;
2337 assert!(
2338 result.is_ok(),
2339 "parallel processing timed out — must complete within 300ms"
2340 );
2341
2342 consumer.stop().await.unwrap();
2343 }
2344
2345 #[tokio::test]
2346 async fn test_seda_concurrent_consumers_one_still_single() {
2347 let comp = create_component();
2348 let _ep = comp
2349 .create_endpoint("seda:cco?concurrentConsumers=1", &NoOpComponentContext)
2350 .unwrap();
2351
2352 let state = comp
2353 .endpoints
2354 .lock()
2355 .unwrap_or_else(|e| e.into_inner())
2356 .get("cco")
2357 .cloned()
2358 .unwrap();
2359 let mut consumer = SedaConsumer::new(state, next_consumer_id(), rt());
2360 let (tx, _rx) = mpsc::channel::<ExchangeEnvelope>(16);
2361 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
2362 consumer.start(ctx).await.unwrap();
2363
2364 assert_eq!(consumer.forwarder_count(), 1);
2365
2366 consumer.stop().await.unwrap();
2367 }
2368
2369 #[test]
2373 fn gate_predicate_matches_both_modes_only() {
2374 assert!(is_no_active_consumers_gate(
2375 &CamelError::EndpointCreationFailed(
2376 "SEDA endpoint 'x' has no active consumers".to_string()
2377 )
2378 ));
2379 assert!(is_no_active_consumers_gate(
2380 &CamelError::EndpointCreationFailed(
2381 "SEDA endpoint 'x' has no active subscribers".to_string()
2382 )
2383 ));
2384 assert!(!is_no_active_consumers_gate(
2385 &CamelError::EndpointCreationFailed(
2386 "endpoint 'x' already has a registered consumer".to_string()
2387 )
2388 ));
2389 assert!(!is_no_active_consumers_gate(&CamelError::Config(
2390 "unrelated".to_string()
2391 )));
2392 }
2393
2394 #[tokio::test]
2399 async fn has_active_consumer_tracks_consumer_lifecycle() {
2400 let comp = create_component();
2401 let _ep = comp
2402 .create_endpoint("seda:probe1", &NoOpComponentContext)
2403 .unwrap();
2404
2405 assert!(
2406 !comp.has_active_consumer("probe1"),
2407 "endpoint without consumer must report inactive"
2408 );
2409 assert!(
2410 !comp.has_active_consumer("never-created"),
2411 "unknown endpoint name must report inactive"
2412 );
2413
2414 let state = comp
2415 .endpoints
2416 .lock()
2417 .unwrap_or_else(|e| e.into_inner())
2418 .get("probe1")
2419 .cloned()
2420 .unwrap();
2421 let mut consumer = SedaConsumer::new(state, next_consumer_id(), rt());
2422 let (tx, _rx) = mpsc::channel::<ExchangeEnvelope>(16);
2423 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "seda-test-route".to_string());
2424 consumer.start(ctx).await.unwrap();
2425
2426 let cloned = comp.clone();
2427 assert!(
2428 cloned.has_active_consumer("probe1"),
2429 "started consumer must report active through a clone"
2430 );
2431
2432 consumer.stop().await.unwrap();
2433 assert!(
2434 !comp.has_active_consumer("probe1"),
2435 "stopped consumer must report inactive"
2436 );
2437 }
2438
2439 #[tokio::test]
2440 async fn single_consumer_restart_restores_receiver() {
2441 let state = Arc::new(SedaEndpointState::new(
2442 &SedaConfig::from_uri("seda:restart1").unwrap(),
2443 ));
2444
2445 let mut a = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2447 let (tx_a, _rx_a) = mpsc::channel::<ExchangeEnvelope>(16);
2448 let ctx_a = ConsumerContext::new(
2449 tx_a,
2450 CancellationToken::new(),
2451 "seda-test-route".to_string(),
2452 );
2453 a.start(ctx_a).await.unwrap();
2454 a.stop().await.unwrap();
2455
2456 let mut b = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2457 let (tx_b, _rx_b) = mpsc::channel::<ExchangeEnvelope>(16);
2458 let ctx_b = ConsumerContext::new(
2459 tx_b,
2460 CancellationToken::new(),
2461 "seda-test-route".to_string(),
2462 );
2463 b.start(ctx_b).await.unwrap();
2464 assert!(state.has_active_consumers());
2465 b.stop().await.unwrap();
2466
2467 for _ in 0..3 {
2469 let mut c = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2470 let (tx_c, _rx_c) = mpsc::channel::<ExchangeEnvelope>(16);
2471 let ctx_c = ConsumerContext::new(
2472 tx_c,
2473 CancellationToken::new(),
2474 "seda-test-route".to_string(),
2475 );
2476 c.start(ctx_c).await.unwrap();
2477 assert!(state.has_active_consumers());
2478 c.stop().await.unwrap();
2479 }
2480
2481 let mut d = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2483 let (tx_d, _rx_d) = mpsc::channel::<ExchangeEnvelope>(16);
2484 let ctx_d = ConsumerContext::new(
2485 tx_d,
2486 CancellationToken::new(),
2487 "seda-test-route".to_string(),
2488 );
2489 d.start(ctx_d).await.unwrap();
2490
2491 let ep = SedaEndpoint {
2492 uri: "seda:restart1".to_string(),
2493 config: SedaConfig::from_uri("seda:restart1").unwrap(),
2494 state: Arc::clone(&state),
2495 };
2496 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2497 let result = producer
2498 .oneshot(Exchange::new(Message::new("post-restart")))
2499 .await;
2500 assert!(result.is_ok());
2501
2502 d.stop().await.unwrap();
2503 }
2504
2505 #[tokio::test]
2506 async fn single_consumer_restart_preserves_buffered_envelopes() {
2507 let state = Arc::new(SedaEndpointState::new(
2508 &SedaConfig::from_uri("seda:restart2").unwrap(),
2509 ));
2510
2511 let (ctx_tx, mut retained_rx) = mpsc::channel::<ExchangeEnvelope>(1);
2513 let ctx = ConsumerContext::new(
2514 ctx_tx.clone(),
2515 CancellationToken::new(),
2516 "seda-test-route".to_string(),
2517 );
2518
2519 let mut a = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2520 a.start(ctx).await.unwrap();
2521
2522 let tx = match &state.mode {
2524 SedaMode::Single { tx, .. } => tx.clone(),
2525 SedaMode::Fanout { .. } => panic!("expected single mode"),
2526 };
2527 for body in ["e1"] {
2532 tx.send(ExchangeEnvelope {
2533 exchange: Exchange::new(Message::new(body)),
2534 reply_tx: None,
2535 })
2536 .await
2537 .unwrap();
2538 }
2539 let deadline = tokio::time::Instant::now() + Duration::from_millis(2_000);
2540 while retained_rx.len() != 1 {
2541 assert!(
2542 tokio::time::Instant::now() < deadline,
2543 "forwarder never delivered e1; retained_rx.len() = {}",
2544 retained_rx.len()
2545 );
2546 tokio::time::sleep(Duration::from_millis(5)).await;
2547 }
2548
2549 tx.send(ExchangeEnvelope {
2554 exchange: Exchange::new(Message::new("e2")),
2555 reply_tx: None,
2556 })
2557 .await
2558 .unwrap();
2559 for _ in 0..3 {
2560 tokio::task::yield_now().await;
2561 tokio::time::sleep(Duration::from_millis(1)).await;
2562 }
2563
2564 tx.send(ExchangeEnvelope {
2568 exchange: Exchange::new(Message::new("e3")),
2569 reply_tx: None,
2570 })
2571 .await
2572 .unwrap();
2573 tokio::task::yield_now().await;
2574
2575 a.stop().await.unwrap();
2576
2577 let mut b = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2579 let ctx_b = ConsumerContext::new(
2580 ctx_tx,
2581 CancellationToken::new(),
2582 "seda-test-route".to_string(),
2583 );
2584 b.start(ctx_b).await.unwrap();
2585
2586 let mut bodies = Vec::new();
2588 let drained = tokio::time::timeout(Duration::from_millis(500), async {
2589 while let Some(env) = retained_rx.recv().await {
2590 bodies.push(env.exchange.input.body.as_text().unwrap().to_string());
2591 if bodies.len() >= 2 {
2592 break;
2593 }
2594 }
2595 })
2596 .await;
2597 assert!(drained.is_ok(), "timed out draining retained receiver");
2598 assert!(bodies.contains(&"e1".to_string()));
2599 assert!(bodies.contains(&"e3".to_string()));
2600
2601 b.stop().await.unwrap();
2602 }
2603
2604 #[tokio::test]
2605 async fn single_consumer_concurrent_restart() {
2606 let state = Arc::new(SedaEndpointState::new(
2607 &SedaConfig::from_uri("seda:restart3?concurrentConsumers=4").unwrap(),
2608 ));
2609
2610 let mut a = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2611 let (tx_a, _rx_a) = mpsc::channel::<ExchangeEnvelope>(16);
2612 let ctx_a = ConsumerContext::new(
2613 tx_a,
2614 CancellationToken::new(),
2615 "seda-test-route".to_string(),
2616 );
2617 a.start(ctx_a).await.unwrap();
2618 assert_eq!(a.forwarder_count(), 4);
2619 a.stop().await.unwrap();
2620
2621 let mut b = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2622 let (tx_b, mut rx_b) = mpsc::channel::<ExchangeEnvelope>(16);
2623 let ctx_b = ConsumerContext::new(
2624 tx_b,
2625 CancellationToken::new(),
2626 "seda-test-route".to_string(),
2627 );
2628 b.start(ctx_b).await.unwrap();
2629 assert_eq!(b.forwarder_count(), 4);
2630
2631 let ep = SedaEndpoint {
2633 uri: "seda:restart3?concurrentConsumers=4".to_string(),
2634 config: SedaConfig::from_uri("seda:restart3?concurrentConsumers=4").unwrap(),
2635 state: Arc::clone(&state),
2636 };
2637 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2638 producer
2639 .oneshot(Exchange::new(Message::new("post-restart")))
2640 .await
2641 .unwrap();
2642
2643 let received = tokio::time::timeout(Duration::from_millis(500), rx_b.recv())
2644 .await
2645 .unwrap()
2646 .unwrap();
2647 assert_eq!(received.exchange.input.body.as_text(), Some("post-restart"));
2648
2649 b.stop().await.unwrap();
2650 }
2651
2652 #[tokio::test]
2653 async fn single_second_start_while_active_still_errors() {
2654 let state = Arc::new(SedaEndpointState::new(
2655 &SedaConfig::from_uri("seda:restart4").unwrap(),
2656 ));
2657
2658 let mut a = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2659 let (tx_a, _rx_a) = mpsc::channel::<ExchangeEnvelope>(16);
2660 let ctx_a = ConsumerContext::new(
2661 tx_a,
2662 CancellationToken::new(),
2663 "seda-test-route".to_string(),
2664 );
2665 a.start(ctx_a).await.unwrap();
2666
2667 let mut c = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2668 let (tx_c, _rx_c) = mpsc::channel::<ExchangeEnvelope>(16);
2669 let ctx_c = ConsumerContext::new(
2670 tx_c,
2671 CancellationToken::new(),
2672 "seda-test-route".to_string(),
2673 );
2674 let result = c.start(ctx_c).await;
2675 assert!(result.is_err());
2676 assert!(
2677 result
2678 .unwrap_err()
2679 .to_string()
2680 .contains("already has a registered consumer")
2681 );
2682
2683 a.stop().await.unwrap();
2684 }
2685
2686 #[tokio::test]
2687 async fn fanout_consumer_restart_cycle() {
2688 let state = Arc::new(SedaEndpointState::new(
2689 &SedaConfig::from_uri("seda:fanrestart?multipleConsumers=true").unwrap(),
2690 ));
2691
2692 let mut a = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2693 let (tx_a, _rx_a) = mpsc::channel::<ExchangeEnvelope>(16);
2694 let ctx_a = ConsumerContext::new(
2695 tx_a,
2696 CancellationToken::new(),
2697 "seda-test-route".to_string(),
2698 );
2699 a.start(ctx_a).await.unwrap();
2700 a.stop().await.unwrap();
2701
2702 let mut b = SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt());
2703 let (tx_b, mut rx_b) = mpsc::channel::<ExchangeEnvelope>(16);
2704 let ctx_b = ConsumerContext::new(
2705 tx_b,
2706 CancellationToken::new(),
2707 "seda-test-route".to_string(),
2708 );
2709 b.start(ctx_b).await.unwrap();
2710
2711 let ep = SedaEndpoint {
2712 uri: "seda:fanrestart?multipleConsumers=true".to_string(),
2713 config: SedaConfig::from_uri("seda:fanrestart?multipleConsumers=true").unwrap(),
2714 state: Arc::clone(&state),
2715 };
2716 let producer = ep.create_producer(rt(), &test_producer_ctx()).unwrap();
2717 producer
2718 .oneshot(Exchange::new(Message::new("fanout restart")))
2719 .await
2720 .unwrap();
2721
2722 let received = tokio::time::timeout(Duration::from_millis(500), rx_b.recv())
2723 .await
2724 .unwrap()
2725 .unwrap();
2726 assert_eq!(
2727 received.exchange.input.body.as_text(),
2728 Some("fanout restart")
2729 );
2730
2731 b.stop().await.unwrap();
2732 }
2733}
2734
2735#[cfg(test)]
2736mod queue_depth_tests {
2737 use super::*;
2738 use camel_api::MetricsCollector;
2739 use camel_component_api::{
2740 HealthCheckRegistry, Message, NoOpComponentContext, RuntimeObservability,
2741 };
2742 use tokio::time::Duration;
2743 use tower::ServiceExt;
2744
2745 #[derive(Clone, Default)]
2748 struct QueueDepthRecorder(Arc<Mutex<Vec<(String, usize)>>>);
2749
2750 impl MetricsCollector for QueueDepthRecorder {
2751 fn record_exchange_duration(&self, _: &str, _: std::time::Duration) {}
2752 fn increment_errors(&self, _: &str, _: &str) {}
2753 fn increment_exchanges(&self, _: &str) {}
2754 fn set_queue_depth(&self, queue: &str, depth: usize) {
2755 self.0
2756 .lock()
2757 .unwrap_or_else(|e| e.into_inner())
2758 .push((queue.to_string(), depth));
2759 }
2760 fn record_circuit_breaker_change(&self, _: &str, _: &str, _: &str) {}
2761 }
2762
2763 struct RecordingObservability(QueueDepthRecorder);
2764
2765 impl RuntimeObservability for RecordingObservability {
2766 fn metrics(&self) -> Arc<dyn MetricsCollector> {
2767 Arc::new(self.0.clone())
2768 }
2769 fn health(&self) -> Arc<dyn HealthCheckRegistry> {
2770 Arc::new(NoopRuntimeObservability)
2771 }
2772 }
2773
2774 fn recording_rt() -> (Arc<RecordingObservability>, QueueDepthRecorder) {
2775 let rec = QueueDepthRecorder::default();
2776 (Arc::new(RecordingObservability(rec.clone())), rec)
2777 }
2778
2779 fn rt_handle(obs: &Arc<RecordingObservability>) -> Arc<dyn RuntimeObservability> {
2780 Arc::clone(obs) as Arc<dyn RuntimeObservability>
2781 }
2782
2783 #[tokio::test]
2791 async fn fanout_gauge_stays_positive_while_blocked_subscriber_has_backlog() {
2792 let comp = SedaComponent::new();
2793 let ep = comp
2794 .create_endpoint("seda:fq?multipleConsumers=true", &NoOpComponentContext)
2795 .unwrap();
2796 let (obs, recorder) = recording_rt();
2797
2798 let mut consumer_a = ep.create_consumer(rt_handle(&obs)).unwrap();
2803 let (tx_a, blocked_rx_a) = mpsc::channel::<ExchangeEnvelope>(1);
2804 let ctx_a = ConsumerContext::new(tx_a, CancellationToken::new(), "route-a".to_string());
2805 consumer_a.start(ctx_a).await.unwrap();
2806
2807 let mut consumer_b = ep.create_consumer(rt_handle(&obs)).unwrap();
2809 let (tx_b, mut rx_b) = mpsc::channel::<ExchangeEnvelope>(16);
2810 let ctx_b = ConsumerContext::new(tx_b, CancellationToken::new(), "route-b".to_string());
2811 consumer_b.start(ctx_b).await.unwrap();
2812 let b_drained = Arc::new(AtomicUsize::new(0));
2813 let drained_clone = Arc::clone(&b_drained);
2814 tokio::spawn(async move {
2815 while let Some(env) = rx_b.recv().await {
2816 drained_clone.fetch_add(1, Ordering::SeqCst);
2817 let _ = env;
2818 }
2819 });
2820
2821 let producer = ep
2822 .create_producer(rt_handle(&obs), &ProducerContext::default())
2823 .unwrap();
2824 for i in 0..4u32 {
2825 producer
2826 .clone()
2827 .oneshot(Exchange::new(Message::new(format!("m{i}"))))
2828 .await
2829 .unwrap();
2830 }
2831
2832 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
2834 while b_drained.load(Ordering::SeqCst) < 4 {
2835 assert!(
2836 tokio::time::Instant::now() < deadline,
2837 "subscriber B never drained its copies"
2838 );
2839 tokio::time::sleep(Duration::from_millis(10)).await;
2840 }
2841
2842 let settled = recorder.0.lock().unwrap_or_else(|e| e.into_inner()).len();
2849 let deadline = tokio::time::Instant::now() + Duration::from_millis(900);
2850 loop {
2851 let post: Vec<usize> = {
2852 let log = recorder.0.lock().unwrap_or_else(|e| e.into_inner());
2853 log[settled..]
2854 .iter()
2855 .filter(|(q, _)| q == "seda:fq")
2856 .map(|(_, d)| *d)
2857 .collect()
2858 };
2859 if post.len() >= 3 {
2860 assert!(
2861 post.iter().all(|d| *d > 0),
2862 "false-zero gauge samples while backlog exists: {post:?}"
2863 );
2864 break;
2865 }
2866 assert!(
2867 tokio::time::Instant::now() < deadline,
2868 "sampler produced too few samples in 900ms: {post:?}"
2869 );
2870 tokio::time::sleep(Duration::from_millis(50)).await;
2871 }
2872
2873 drop(blocked_rx_a);
2874 consumer_a.stop().await.unwrap();
2875 consumer_b.stop().await.unwrap();
2876 }
2877
2878 #[test]
2880 fn depth_guard_count_in_rolls_back_on_drop() {
2881 let depth = Arc::new(AtomicUsize::new(0));
2882 let guard = DepthGuard::count_in(&depth, 2);
2883 assert_eq!(depth.load(Ordering::Acquire), 2);
2884 drop(guard);
2885 assert_eq!(depth.load(Ordering::Acquire), 0);
2886 }
2887
2888 #[test]
2891 fn depth_guard_commit_keeps_count() {
2892 let depth = Arc::new(AtomicUsize::new(0));
2893 DepthGuard::count_in(&depth, 3).commit();
2894 assert_eq!(depth.load(Ordering::Acquire), 3);
2895 }
2896
2897 #[test]
2900 fn depth_guard_claim_decrements_on_panic_unwind() {
2901 let depth = Arc::new(AtomicUsize::new(1)); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2903 let _claim = DepthGuard::claim(&depth);
2904 assert_eq!(depth.load(Ordering::Acquire), 1);
2905 panic!("simulated forward_envelope panic");
2906 }));
2907 assert!(result.is_err());
2908 assert_eq!(
2909 depth.load(Ordering::Acquire),
2910 0,
2911 "unwind must count the claimed envelope out"
2912 );
2913 }
2914
2915 #[tokio::test]
2919 async fn fanout_stop_with_backlog_returns_depth_counts() {
2920 let state = Arc::new(SedaEndpointState::new(
2921 &SedaConfig::from_uri("seda:fqstop?multipleConsumers=true").unwrap(),
2922 ));
2923 let (obs, _recorder) = recording_rt();
2924 let mut consumer =
2925 SedaConsumer::new(Arc::clone(&state), next_consumer_id(), rt_handle(&obs));
2926 let (tx, blocked_rx) = mpsc::channel::<ExchangeEnvelope>(1);
2928 let ctx = ConsumerContext::new(tx, CancellationToken::new(), "route-blk".to_string());
2929 consumer.start(ctx).await.unwrap();
2930
2931 let ep = SedaEndpoint {
2933 uri: "seda:fqstop?multipleConsumers=true".to_string(),
2934 config: SedaConfig::from_uri("seda:fqstop?multipleConsumers=true").unwrap(),
2935 state: Arc::clone(&state),
2936 };
2937 let producer = ep
2938 .create_producer(rt_handle(&obs), &ProducerContext::default())
2939 .unwrap();
2940 for i in 0..4u32 {
2941 producer
2942 .clone()
2943 .oneshot(Exchange::new(Message::new(format!("m{i}"))))
2944 .await
2945 .unwrap();
2946 }
2947
2948 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
2952 while state.depth.load(Ordering::Acquire) != 3 {
2953 assert!(
2954 tokio::time::Instant::now() < deadline,
2955 "depth never settled at 3 (got {})",
2956 state.depth.load(Ordering::Acquire)
2957 );
2958 tokio::time::sleep(Duration::from_millis(10)).await;
2959 }
2960
2961 consumer.stop().await.unwrap();
2962
2963 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
2966 while state.depth.load(Ordering::Acquire) != 0 {
2967 assert!(
2968 tokio::time::Instant::now() < deadline,
2969 "stop must return the discarded backlog's counts (got {})",
2970 state.depth.load(Ordering::Acquire)
2971 );
2972 tokio::time::sleep(Duration::from_millis(10)).await;
2973 }
2974
2975 drop(blocked_rx);
2976 }
2977}