Skip to main content

camel_component_seda/
lib.rs

1//! In-memory SEDA component for rust-camel — asynchronous staging channel
2//! between routes sharing the same context via bounded queues.
3//!
4//! Main types: `SedaComponent`, `SedaEndpoint`, `SedaConsumer`, `SedaProducer`.
5
6use 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    // The consumer reports queue depth through `rt.metrics()` on its
19    // forwarder/sampler loop, so tests need the permissive double (the
20    // panicking double is for components that must not touch observability).
21    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
40/// Queue-depth sampling cadence for the per-endpoint
41/// `camel_queue_depth{queue="seda:<name>"}` gauge (dashboard-observability
42/// T3.3). Short enough that a scrape between ticks never misses a backlog,
43/// long enough that the len() read is negligible.
44const QUEUE_DEPTH_SAMPLE_INTERVAL: Duration = Duration::from_millis(250);
45
46// ---------------------------------------------------------------------------
47// Enums
48// ---------------------------------------------------------------------------
49
50#[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// ---------------------------------------------------------------------------
64// SedaConfig
65// ---------------------------------------------------------------------------
66
67/// Configuration parsed from a SEDA URI.
68///
69/// URI format: `seda:name[?options]`
70///
71/// Options are split into two groups:
72/// - **shared**: validated for consistency when multiple endpoints reference
73///   the same endpoint name (`size`, `multiple_consumers`, `exchange_pattern`,
74///   `concurrent_consumers`).
75/// - **producer only**: stored per-endpoint, used only by the producer
76///   (`block_when_full`, `discard_if_no_consumers`, `timeout_ms`,
77///   `wait_for_task_to_complete`).
78#[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/// Private container for macro-derived `uri_options()` and `metadata()`.
92///
93/// Mirrors `SedaConfig`'s URI-parsed fields with `String` for enum types.
94/// `SedaConfig` holds the typed enum variants; metadata delegation targets
95/// this inner type.
96#[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    /// Component metadata for the seda scheme, derived from `#[uri_param]`
312    /// annotations on `SedaUriConfig`.
313    pub fn metadata() -> ComponentMetadata {
314        SedaUriConfig::metadata()
315    }
316
317    /// Generated URI option definitions for the seda scheme, derived from
318    /// `#[uri_param]` annotations on `SedaUriConfig`.
319    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
355// ---------------------------------------------------------------------------
356// ConsumerId generator (no uuid dependency needed)
357// ---------------------------------------------------------------------------
358
359static 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
368// ---------------------------------------------------------------------------
369// SedaMode + SedaEndpointState
370// ---------------------------------------------------------------------------
371
372type ConsumerId = String;
373
374/// Transport mode for a SEDA endpoint.
375///
376/// - `Single`: one bounded mpsc channel, one consumer allowed.
377///   `active` tracks whether a consumer has started (separate from receiver
378///   ownership, which is taken by the forwarder task on start).
379/// - `Fanout`: one bounded mpsc per subscriber, multiple consumers allowed.
380enum 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    /// Lock-free queue-depth counter backing the per-endpoint
395    /// `camel_queue_depth{queue="seda:<name>"}` gauge (dashboard-observability
396    /// T3.3). ONE counter per endpoint, shared by producers and every
397    /// forwarder (hoisted out of `SedaMode` so both modes use it).
398    ///
399    /// Semantics: Single counts each envelope once from producer send until
400    /// the forwarder finishes forwarding it. Fanout is broadcast — each
401    /// produced exchange is cloned to every subscriber — so the honest
402    /// shared-label metric counts each *undelivered copy*: the producer adds
403    /// one per reserved subscriber and each forwarder subtracts one when its
404    /// copy leaves the endpoint. (Per-subscriber `rx.len()` publishes under
405    /// the shared label — the scheme this replaces — let an idle subscriber
406    /// clobber a busy subscriber's backlog with intermittent false zeros.)
407    ///
408    /// The forwarder holds the shared-receiver mutex while parked in
409    /// `recv()` (Single), so a sampler cannot take that lock; producers
410    /// count an envelope in before sending and forwarders count it out via
411    /// the RAII [`DepthGuard`]. Reads are exact once sends settle (transient
412    /// over-count only, never negative).
413    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    /// Returns true if at least one consumer has started and not yet stopped.
438    /// For Single mode: checks the `active` flag (not the receiver, which is
439    /// moved into the forwarder task on start).
440    /// For Fanout mode: checks if subscribers map is non-empty.
441    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
452/// RAII pairing for the per-endpoint [`SedaEndpointState::depth`] counter:
453/// every counted-in envelope (or fanout copy) must be counted out exactly
454/// once, including on panic unwind and forwarder task abort (review F4).
455///
456/// - Producers create the guard with [`DepthGuard::count_in`] after reserving
457///   channel capacity and [`DepthGuard::commit`] it once the envelope(s) are
458///   handed to the channel — any early return, panic, or abort before the
459///   commit drops the guard and rolls the count back.
460/// - Forwarders create it with [`DepthGuard::claim`] immediately after
461///   receiving an envelope; normal completion of `forward_envelope`, a panic
462///   inside it, and a task abort at one of its await points all drop the
463///   guard, counting the envelope out. Until then the envelope counts as
464///   in-flight through the endpoint (queued or being forwarded).
465struct DepthGuard {
466    depth: Arc<AtomicUsize>,
467    count: usize,
468}
469
470impl DepthGuard {
471    /// Producer side: count `count` envelopes in; dropping rolls back.
472    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    /// Forwarder side: take ownership of one already-counted-in envelope
481    /// (no increment); dropping counts it out.
482    fn claim(depth: &Arc<AtomicUsize>) -> Self {
483        Self {
484            depth: Arc::clone(depth),
485            count: 1,
486        }
487    }
488
489    /// Producer commit: the count now belongs to envelopes inside the
490    /// channel; abandon the rollback.
491    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
502/// Spawn the detached per-endpoint queue-depth sampler (T3.3). A forwarder
503/// parked inside a blocked pipeline (or parked in `recv()` holding the
504/// shared-receiver mutex) cannot publish loop-edge samples, so a fixed-tick
505/// task reads the lock-free `depth` counter instead. Detached by design —
506/// it exits on the consumer's cancel token (stop()) and is not a forwarder,
507/// so it stays out of `forwarder_handles`.
508///
509/// Fanout consumers each spawn one; every sampler publishes the SAME shared
510/// atomic, so concurrent ticks are idempotent and an idle subscriber can
511/// never clobber a busy subscriber's backlog with a false zero (the
512/// per-subscriber `rx.len()` publish this replaces did exactly that).
513fn 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
532// ---------------------------------------------------------------------------
533// SedaComponent
534// ---------------------------------------------------------------------------
535
536/// True for the SEDA producer's startup-race rejections: the pre-enqueue
537/// gate that fires when no consumer has started yet — Single mode's "has
538/// no active consumers" and Fanout mode's "has no active subscribers" (the
539/// error variant is shared `EndpointCreationFailed`, so the wording is the
540/// discriminator; this crate owns the message text). Both reject BEFORE
541/// enqueue but INSIDE the caller's pipeline: steps that already ran (e.g.
542/// route-interception divert copies) have executed, so RETRYING the send
543/// duplicates their side effects. Senders that must not retry use this
544/// predicate to fail fast instead (rc-zjrx); readiness probing with
545/// [`SedaComponent::has_active_consumer`] avoids the error up front.
546pub 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/// Cloning shares the endpoint registry, so a clone registered into a
554/// `CamelContext` and the original handle observe the same per-endpoint
555/// state (the `MockComponent` pattern).
556#[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    /// True when the named endpoint has at least one consumer that has
569    /// started and not yet stopped — the same signal the producer-side
570    /// no-active-consumers gate checks, so polling this predicate is a
571    /// side-effect-free readiness probe for senders that must not retry
572    /// (a retried pipeline re-executes steps with side effects, such as
573    /// route-interception divert copies). Singular: one endpoint name,
574    /// unlike the per-endpoint-state `has_active_consumers` check.
575    /// Unknown endpoint names report `false`.
576    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
633// ---------------------------------------------------------------------------
634// SedaEndpoint
635// ---------------------------------------------------------------------------
636
637struct 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/// Per-endpoint producer options. These are NOT shared at the SedaEndpointState
677/// level because two endpoints referencing the same seda name may have different
678/// producer-only options (e.g. different blockWhenFull settings).
679#[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
698// ---------------------------------------------------------------------------
699// SedaConsumer
700// ---------------------------------------------------------------------------
701
702struct SedaConsumer {
703    state: Arc<SedaEndpointState>,
704    consumer_id: ConsumerId,
705    started: bool,
706    cancel_token: CancellationToken,
707    forwarder_handles: Vec<JoinHandle<Result<(), CamelError>>>,
708    /// Handle to the forwarder-shared receiver. Set on start for BOTH
709    /// modes (Single: shared by the concurrent forwarders; Fanout: owned by
710    /// the single forwarder). Used by `stop()` — Single restores the
711    /// receiver into the endpoint state so a later consumer can start
712    /// again; Fanout takes it back to return the discarded backlog's
713    /// queue-depth counts.
714    shared_rx: Option<Arc<AsyncMutex<Option<mpsc::Receiver<ExchangeEnvelope>>>>>,
715    /// Runtime observability handle: `metrics()` powers the per-endpoint
716    /// `camel_queue_depth{queue="seda:<name>"}` gauge on the forwarder loop.
717    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                                    // Stop took the receiver back; exit cleanly.
789                                    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                            // Own the counted-in envelope until it leaves the
801                            // endpoint: the claim's Drop counts it out when
802                            // forwarding completes, panics, or the task is
803                            // aborted at an await point.
804                            let _claim = DepthGuard::claim(&depth);
805                            forward_envelope(&ctx, &component_metrics, envelope).await;
806                        }
807                    });
808                    self.forwarder_handles.push(handle);
809                }
810
811                // Periodic queue-depth sampler: a forwarder parked inside a
812                // blocked pipeline (or parked in `recv()` holding the
813                // shared-receiver mutex) cannot publish loop-edge samples,
814                // so the consumer reports the lock-free depth counter on a
815                // fixed tick instead. Detached by design — it exits on the
816                // consumer's cancel token (stop()) and is not a forwarder,
817                // so it stays out of `forwarder_handles`.
818                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                                // Stop took the receiver back; exit cleanly.
848                                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                        // Fanout copy: counted in by the producer (one per
860                        // reserved subscriber); the claim counts it out when
861                        // forwarding finishes, panics, or the task is aborted.
862                        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                // Shared-atomic sampler (see `spawn_queue_depth_sampler`):
870                // replaces per-subscriber `rx.len()` publishes under the
871                // same label, which let an idle subscriber clobber a busy
872                // subscriber's backlog with false zeros.
873                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        // Explicit startup contract (rc-dbrkr): signal readiness only now —
884        // both mode arms have published the consumer-activation state
885        // (Single: `active` stored + receiver taken + forwarders spawned;
886        // Fanout: subscriber registered + forwarder spawned), so route
887        // startup resolves only against a fully activated consumer set and
888        // a producer send after `start()` passes the pre-enqueue gate on
889        // the first attempt. The error paths above return before this point
890        // and signal nothing.
891        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                // Flag-first: clear `active` before publishing the restored
912                // receiver so a concurrent start that acquires the receiver
913                // does so only after `active` is false, making its own
914                // `active.store(true)` the final write (race closure).
915                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                // The aborted forwarder leaves its backlog in the
929                // subscriber channel; those copies are discarded with the
930                // subscription, so return their queue-depth counts —
931                // otherwise the shared gauge stays inflated forever. The
932                // mutex is free once the aborted task is dropped, so this
933                // is deterministic even when the abort raced the cancel
934                // branch. (Narrow residual race, accepted: a producer that
935                // reserved a permit on this subscriber just before removal
936                // still sends into the discarded channel, leaking +1.)
937                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        // Explicit (rc-dbrkr): readiness is signalled via
967        // `ConsumerContext::mark_ready()` at the end of `start()`, after the
968        // endpoint's consumer-activation state is published, so
969        // `ctx.start()` never returns ahead of an active consumer set.
970        ConsumerStartupMode::Explicit
971    }
972
973    fn background_task_handle(
974        &mut self,
975    ) -> Option<tokio::task::JoinHandle<Result<(), CamelError>>> {
976        // SEDA may have multiple forwarder handles; return the first one.
977        // The remaining handles are cancelled in stop().
978        self.forwarder_handles.pop()
979    }
980}
981
982/// Forward an envelope from the SEDA queue into the route pipeline.
983///
984/// Key rule: if the envelope carries a `reply_tx`, the forwarder MUST use
985/// `send_and_wait()` to route the pipeline result back to the producer.
986/// This handles both InOut and `waitForTaskToComplete=Always` cases.
987/// If no `reply_tx`, use fire-and-forget `send()`.
988///
989/// The consume operation is observed through the uniform
990/// component-operations facade (dashboard-observability Task 4.2):
991/// failures ALWAYS reach the error family as `e:seda:consume`, the
992/// component series only with the lever on.
993async 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// ---------------------------------------------------------------------------
1011// SedaProducer
1012// ---------------------------------------------------------------------------
1013
1014#[derive(Clone)]
1015struct SedaProducer {
1016    state: Arc<SedaEndpointState>,
1017    producer_config: ProducerConfig,
1018    /// Observability handle: `component_metrics()` powers the uniform
1019    /// `seda:produce` emission (dashboard-observability Task 4.2).
1020    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            // The produce operation covers the whole enqueue outcome
1039            // (no-consumers rejection, queue-full, timeout, reply wait):
1040            // failures ALWAYS reach the error family as `e:seda:produce`,
1041            // the component series only with the lever on.
1042            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                        // Count the envelope into the queue depth before it
1082                        // enters the channel; the guard rolls the count back on
1083                        // send failure, panic, or abort before the commit, so
1084                        // the lock-free counter never under-reads or leaks.
1085                        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                            // All copies reserved: count one in per subscriber
1155                            // copy. The guard rolls back if anything between
1156                            // here and the sends panics; commit afterwards.
1157                            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            // Uniform component-operations emission (Task 4.2): failures
1214            // always reach the error family, component series lever-gated.
1215            component_metrics.observe("seda", "produce", result.is_err());
1216            result
1217        })
1218    }
1219}
1220
1221// ---------------------------------------------------------------------------
1222// Tests
1223// ---------------------------------------------------------------------------
1224
1225#[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    // --- Explicit startup handshake (rc-dbrkr) ---
1696
1697    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        // Readiness must be resolvable the moment start() returned Ok.
1735        receiver
1736            .await_ready()
1737            .await
1738            .expect("readiness must be signalled after activation");
1739        // Behavioral activation proof: the pre-enqueue gate passes on the
1740        // first attempt after start() returned Ok.
1741        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        // Behavioral registration proof: the fanout producer finds the
1770        // subscriber immediately after start() returned Ok.
1771        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        // First consumer holds the Single-mode receiver.
1796        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        // Second consumer's start() fails before readiness.
1801        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        // The failure surfaced as an Err without signalling readiness: the
1807        // receiver must stay Pending or resolve as Err (drop semantics) —
1808        // never Ok.
1809        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        // Fresh consumer instance for the same endpoint: readiness must be
1839        // signalled only after the new active flag was stored, and the
1840        // buffered envelope must flow without any test-side probe.
1841        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        // Behavioral reactivation proof: the fresh consumer's activation
1849        // publishes the gate again — the first send after the restart passes.
1850        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        // The envelope survived the restart: it arrives on either the old
1857        // consumer's route channel (already forwarded) or the new one
1858        // (still queued across stop).
1859        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        // Set up route pipeline: receives envelope, sleeps 100ms, sends reply
2283        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        // Spawn a concurrent pipeline: each envelope gets its own task so
2293        // parallel processing is measurable even with InOut exchanges.
2294        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        // Enqueue 2 InOut envelopes to the SEDA channel (both at once, not awaiting replies)
2306        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        // Await both replies; with 2 concurrent forwarders this completes in ~200ms
2331        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    /// `is_no_active_consumers_gate` recognizes exactly both gate wordings
2370    /// (Single and Fanout) and no other `EndpointCreationFailed` — other
2371    /// messages must stay eligible for callers' startup-race retries.
2372    #[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    /// `has_active_consumer` is the readiness-probe signal for senders that
2395    /// must not retry (rc-zjrx): unknown names and known-but-consumerless
2396    /// endpoints report false; a started consumer flips it true; stop flips
2397    /// it back; clones share the registry.
2398    #[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        // First cycle: A starts, stops; fresh B starts -> Ok, active.
2446        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        // Repeat full stop/start cycle on fresh instances 3x — every start Ok.
2468        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        // After a restart, producer send succeeds (unfenced).
2482        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        // Capacity-1 context channel; receiver retained but NOT read (blocked context).
2512        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        // Push 3 identifiable envelopes directly through the Single-mode tx.
2523        let tx = match &state.mode {
2524            SedaMode::Single { tx, .. } => tx.clone(),
2525            SedaMode::Fanout { .. } => panic!("expected single mode"),
2526        };
2527        // Establish the steady state observably instead of with a fixed sleep.
2528        // Phase 1: e1 alone. Wait until the forwarder delivered e1 into the
2529        // (unread) context channel: retained receiver length == 1 proves the
2530        // forwarder parked its send and went back to recv.
2531        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        // Phase 2: e2 only. The forwarder dequeues e2 (FIFO) and parks on
2550        // the send into the still-full context channel — that send cannot
2551        // progress because nothing reads the retained receiver before stop.
2552        // Yield a few slots so the forwarder reaches that parked send.
2553        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        // Phase 3: e3 last. The forwarder is parked on the e2 send, so e3
2565        // CANNOT be dequeued before stop — it is deterministically the
2566        // still-queued envelope the restore path must preserve.
2567        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        // Start fresh B on a clone of the SAME sender wired to the retained receiver.
2578        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        // Drain the retained receiver with a timeout; assert e1 and e3 arrive.
2587        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        // Envelope sent post-restart (via producer after B active) is delivered on B's context receiver.
2632        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    /// Shared queue-depth recorder: `metrics()` hands out clones that all
2746    /// append to the same log.
2747    #[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    /// F1: fanout subscribers must not clobber the shared
2784    /// `camel_queue_depth{queue="seda:<name>"}` gauge. Two subscribers; one
2785    /// is blocked (route channel capacity 1, never read) with a backlog
2786    /// behind it, the other drains freely. While the blocked subscriber's
2787    /// backlog exists, every gauge sample must stay > 0 — the old
2788    /// per-subscriber `rx.len()` publishes let the idle subscriber write 0
2789    /// over the busy subscriber's backlog.
2790    #[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        // Subscriber A: blocked. Route channel capacity 1 and never read —
2799        // the first copy is delivered, the forwarder then parks forwarding
2800        // the second, and the rest queue behind it. Keeping the receiver
2801        // alive (never read) makes this state permanent.
2802        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        // Subscriber B: free-draining. A reader task consumes every copy.
2808        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        // B consumes all 4 copies (proves the endpoint delivers normally).
2833        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        // From here on, the shared depth is deterministically >= 3 (A's
2843        // one in-forward claim + two queued copies; the first copy's claim
2844        // already dropped on delivery into A's route channel; A's claims
2845        // cannot drop further because its route channel is never read).
2846        // Any 0 sample after this settle point is a false zero — the F1
2847        // bug.
2848        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    /// F4: the producer-side guard rolls its count back on drop.
2879    #[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    /// F4: `commit` keeps the count — it now belongs to envelopes inside
2889    /// the channel.
2890    #[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    /// F4: the forwarder-side claim counts its envelope out even when the
2898    /// code it guards panics — the unwind drops the guard.
2899    #[test]
2900    fn depth_guard_claim_decrements_on_panic_unwind() {
2901        let depth = Arc::new(AtomicUsize::new(1)); // one envelope counted in
2902        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    /// Stopping a fanout consumer with a backlog discards the queued
2916    /// copies with the subscription — their queue-depth counts must be
2917    /// returned so the shared gauge does not stay inflated forever.
2918    #[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        // Blocked subscriber: route channel capacity 1, never read.
2927        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        // Produce 4 copies through the real producer (counted in per copy).
2932        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        // Steady state: copy 1 delivered into the blocked route channel
2949        // (claim dropped), copy 2 parked in the blocked forward (claim
2950        // held), copies 3-4 queued in the subscriber channel.
2951        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        // The abort drops the parked claim and the stop drain returns the
2964        // two queued copies; both land within milliseconds, poll for it.
2965        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}