1use std::collections::HashMap;
2use std::future::Future;
3use std::path::PathBuf;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::task::{Context, Poll};
8use std::time::{Duration, Instant};
9
10use camel_bridge::{
11 download::ensure_binary,
12 health::wait_for_health,
13 process::{BridgeProcess, BridgeProcessConfig, BrokerType},
14};
15use camel_component_api::{
16 BoxProcessor, CamelError, Component, Consumer, Endpoint, Exchange, NetworkRetryPolicy,
17 ProducerContext,
18};
19use dashmap::DashMap;
20use tokio::sync::{Mutex, watch};
21use tonic::transport::Channel;
22use tower::Service;
23use tracing::{info, warn};
24
25use crate::config::{BrokerConfig, JmsEndpointConfig, JmsPoolConfig};
26use crate::consumer::JmsConsumer;
27use crate::health::JmsHealthCheck;
28use crate::producer::JmsProducer;
29use crate::proto::{HealthRequest, bridge_service_client::BridgeServiceClient};
30
31pub const BRIDGE_TRANSPORT_ERROR_PREFIX: &str = "JMS gRPC ";
39const MAX_RESTART_ATTEMPTS: u32 = 10;
40
41#[derive(Debug, Clone)]
44pub enum BridgeState {
45 Starting,
46 Ready { channel: Channel },
47 Degraded(String),
48 Restarting { attempt: u32, next_at: Instant },
49 Stopped,
50}
51
52pub struct BridgeSlot {
55 pub name: String,
56 pub broker_url: String,
57 pub broker_type: BrokerType,
58 pub credentials: Option<(String, String)>,
59 pub state_rx: watch::Receiver<BridgeState>,
60 pub(crate) state_tx: watch::Sender<BridgeState>,
61 pub process: Arc<tokio::sync::Mutex<Option<BridgeProcess>>>,
63 pub(crate) health_monitor_handle: Arc<tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>>,
66}
67
68impl std::fmt::Debug for BridgeSlot {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("BridgeSlot")
71 .field("name", &self.name)
72 .field("broker_url", &self.broker_url)
73 .field("broker_type", &self.broker_type)
74 .finish()
75 }
76}
77
78pub struct JmsBridgePool {
81 pub(crate) slots: DashMap<String, Arc<BridgeSlot>>,
82 pub(crate) config: HashMap<String, BrokerConfig>,
83 pub(crate) bridge_start_timeout_ms: u64,
84 pub(crate) reconnect: NetworkRetryPolicy,
85 pub(crate) health_check_interval_ms: u64,
86 pub(crate) bridge_version: String,
87 pub(crate) bridge_cache_dir: PathBuf,
88 pub(crate) max_bridges: usize,
90 bridge_create_lock: Mutex<()>,
92 pub(crate) shutting_down: Arc<AtomicBool>,
93}
94
95impl JmsBridgePool {
96 pub fn from_config(pool_config: JmsPoolConfig) -> Result<Self, CamelError> {
97 pool_config.validate()?;
98 let mut reconnect = pool_config.reconnect;
101 if pool_config.broker_reconnect_interval_ms
102 != crate::config::default_broker_reconnect_interval_ms()
103 {
104 reconnect.initial_delay =
105 Duration::from_millis(pool_config.broker_reconnect_interval_ms);
106 }
107 Ok(Self {
108 slots: DashMap::new(),
109 config: pool_config.brokers,
110 bridge_start_timeout_ms: pool_config.bridge_start_timeout_ms,
111 reconnect,
112 health_check_interval_ms: pool_config.health_check_interval_ms,
113 bridge_version: crate::BRIDGE_VERSION.to_string(),
114 bridge_cache_dir: pool_config.bridge_cache_dir,
115 max_bridges: pool_config.max_bridges,
116 bridge_create_lock: Mutex::new(()),
117 shutting_down: Arc::new(AtomicBool::new(false)),
118 })
119 }
120
121 pub fn resolve_broker_name(&self, name: Option<&str>) -> Result<String, CamelError> {
128 match name {
129 Some(n) => {
130 if self.config.contains_key(n) {
131 Ok(n.to_string())
132 } else {
133 Err(CamelError::ProcessorError(format!(
134 "Unknown JMS broker '{n}' — declare it in [components.jms.brokers] in Camel.toml",
135 )))
136 }
137 }
138 None => match self.config.len() {
139 0 => Err(CamelError::ProcessorError(
140 "No JMS brokers configured — declare at least one in [components.jms.brokers] in Camel.toml".to_string(),
141 )),
142 1 => Ok(self.config.keys().next().unwrap().clone()), _ => Err(CamelError::ProcessorError(format!(
144 "Multiple JMS brokers configured ({}); specify one with ?broker=<name> in the URI",
145 self.config.keys().cloned().collect::<Vec<_>>().join(", ")
146 ))),
147 },
148 }
149 }
150
151 pub fn resolve_broker_type(&self, scheme: &str, broker_name: &str) -> BrokerType {
153 let config_type = self
154 .config
155 .get(broker_name)
156 .map(|c| c.broker_type.clone())
157 .unwrap_or(BrokerType::Generic);
158
159 match scheme {
160 "activemq" => {
161 if config_type != BrokerType::ActiveMq && config_type != BrokerType::Generic {
162 warn!(
163 "Scheme 'activemq' overrides configured broker_type '{:?}' for broker '{}'",
164 config_type, broker_name
165 );
166 }
167 BrokerType::ActiveMq
168 }
169 "artemis" => {
170 if config_type != BrokerType::Artemis && config_type != BrokerType::Generic {
171 warn!(
172 "Scheme 'artemis' overrides configured broker_type '{:?}' for broker '{}'",
173 config_type, broker_name
174 );
175 }
176 BrokerType::Artemis
177 }
178 _ => config_type,
179 }
180 }
181
182 pub async fn get_or_create_slot(
185 &self,
186 broker_name: &str,
187 ) -> Result<Arc<BridgeSlot>, CamelError> {
188 if let Some(slot) = self.slots.get(broker_name) {
189 return Ok(Arc::clone(&*slot));
190 }
191
192 let _guard = self.bridge_create_lock.lock().await;
195
196 if let Some(slot) = self.slots.get(broker_name) {
198 return Ok(Arc::clone(&*slot));
199 }
200
201 let total_count = self.slots.len();
208 if total_count >= self.max_bridges {
209 return Err(CamelError::Config(format!(
210 "JMS bridge limit reached: {total_count} bridge(s) >= max_bridges ({})",
211 self.max_bridges
212 )));
213 }
214
215 let broker_config = self.config.get(broker_name).ok_or_else(|| {
216 CamelError::ProcessorError(format!("Unknown JMS broker '{}'", broker_name))
217 })?;
218
219 let broker_url = broker_config.broker_url.clone();
221 let broker_type = broker_config.broker_type.clone();
222 let credentials = match (&broker_config.username, &broker_config.password) {
223 (Some(u), Some(p)) => Some((u.clone(), p.clone())),
224 _ => None,
225 };
226
227 let slot = match self.slots.entry(broker_name.to_string()) {
228 dashmap::Entry::Occupied(existing) => {
229 return Ok(Arc::clone(existing.get()));
230 }
231 dashmap::Entry::Vacant(entry) => {
232 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
233 let slot = Arc::new(BridgeSlot {
234 name: broker_name.to_string(),
235 broker_url: broker_url.clone(),
236 broker_type: broker_type.clone(),
237 credentials: credentials.clone(),
238 state_rx,
239 state_tx,
240 process: Arc::new(tokio::sync::Mutex::new(None)),
241 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
242 });
243 entry.insert(Arc::clone(&slot));
244 slot
245 }
246 };
247
248 let start_result = Self::start_bridge_process(
249 &self.bridge_version,
250 &self.bridge_cache_dir,
251 self.bridge_start_timeout_ms,
252 &broker_url,
253 &broker_type,
254 &credentials,
255 )
256 .await;
257
258 match start_result {
259 Ok((process, channel)) => {
260 {
261 let mut guard = slot.process.lock().await;
262 *guard = Some(process);
263 }
264 let _ = slot.state_tx.send(BridgeState::Ready { channel });
265 }
266 Err(e) => {
267 let _ = slot
268 .state_tx
269 .send(BridgeState::Degraded(format!("Initial start failed: {e}")));
270 }
271 }
272
273 self.spawn_health_monitor(Arc::clone(&slot)).await;
274
275 Ok(slot)
276 }
277
278 pub fn restart_slot(&self, broker_name: &str) {
280 if let Some(slot) = self.slots.get(broker_name) {
281 let _ = slot.state_tx.send(BridgeState::Restarting {
282 attempt: 0,
283 next_at: Instant::now(),
284 });
285 }
286 }
287
288 pub async fn refresh_slot_channel(&self, broker_name: &str) -> Result<(), CamelError> {
293 let slot = self
294 .slots
295 .get(broker_name)
296 .map(|s| Arc::clone(&*s))
297 .ok_or_else(|| {
298 CamelError::ProcessorError(format!("Unknown JMS broker '{}'", broker_name))
299 })?;
300
301 let channel = {
306 let guard = slot.process.lock().await;
307 let process = guard.as_ref().ok_or_else(|| {
308 CamelError::ProcessorError(format!(
309 "JMS broker '{}' has no running bridge process",
310 broker_name
311 ))
312 })?;
313 process.connect().await.map_err(|e| {
314 CamelError::ProcessorError(format!(
315 "JMS broker '{}' channel refresh failed: {}",
316 broker_name, e
317 ))
318 })?
319 };
320
321 let _ = slot.state_tx.send(BridgeState::Ready { channel });
322 Ok(())
323 }
324
325 pub fn begin_shutdown(&self) {
326 self.shutting_down.store(true, Ordering::SeqCst);
327 }
328
329 pub async fn shutdown(&self) -> Result<(), CamelError> {
331 self.begin_shutdown();
332 let names: Vec<String> = self.slots.iter().map(|e| e.key().clone()).collect();
333 let mut errors: Vec<String> = Vec::new();
334
335 for name in names {
336 if let Some((_, slot)) = self.slots.remove(&name) {
337 let _ = slot.state_tx.send(BridgeState::Stopped);
339
340 let process = {
342 let mut guard = slot.process.lock().await;
343 guard.take()
344 };
345 if let Some(p) = process
346 && let Err(e) = p.stop().await
347 {
348 errors.push(format!("broker '{}': process stop failed: {e}", slot.name));
349 }
350
351 let monitor_handle = {
353 let mut guard = slot.health_monitor_handle.lock().await;
354 guard.take()
355 };
356 if let Some(mut h) = monitor_handle
357 && tokio::time::timeout(Duration::from_secs(5), &mut h)
358 .await
359 .is_err()
360 {
361 h.abort();
362 let _ = h.await;
363 warn!(
364 "health monitor for '{}' did not stop in 5s; aborted",
365 slot.name
366 );
367 }
368 }
369 }
370
371 if errors.is_empty() {
372 Ok(())
373 } else {
374 Err(CamelError::ProcessorError(format!(
375 "JMS pool shutdown completed with {} error(s): {}",
376 errors.len(),
377 errors.join("; ")
378 )))
379 }
380 }
381
382 async fn spawn_health_monitor(&self, slot: Arc<BridgeSlot>) {
383 let health_interval = self.health_check_interval_ms;
384 let bridge_version = self.bridge_version.clone();
385 let bridge_cache_dir = self.bridge_cache_dir.clone();
386 let start_timeout_ms = self.bridge_start_timeout_ms;
387 let handle_ref = Arc::clone(&slot.health_monitor_handle);
388 let shutting_down = Arc::clone(&self.shutting_down);
389 let broker_name = slot.name.clone();
390
391 let handle = tokio::spawn(async move {
392 loop {
393 let state = slot.state_rx.borrow().clone();
394 match state {
395 BridgeState::Stopped => {
396 info!("Health monitor for '{}' exiting (Stopped)", slot.name);
397 break;
398 }
399 BridgeState::Ready { ref channel } => {
400 tokio::time::sleep(Duration::from_millis(health_interval)).await;
401 let mut client = BridgeServiceClient::new(channel.clone());
402 let health_timeout = Duration::from_secs(3);
403 match tokio::time::timeout(health_timeout, client.health(HealthRequest {}))
404 .await
405 {
406 Ok(Ok(_)) => {}
407 Ok(Err(e)) => {
408 warn!(
409 "Health check failed for broker '{}': {e}. Marking Degraded.",
410 slot.name
411 );
412 let _ = slot.state_tx.send(BridgeState::Degraded(e.to_string()));
413 }
414 Err(_) => {
415 let msg = format!(
416 "health RPC timed out after {}ms",
417 health_timeout.as_millis()
418 );
419 warn!(
420 "Health check timed out for broker '{}': {}. Marking Degraded.",
421 slot.name, msg
422 );
423 let _ = slot.state_tx.send(BridgeState::Degraded(msg));
424 }
425 }
426 }
427 BridgeState::Degraded(_) | BridgeState::Starting => {
428 if matches!(*slot.state_rx.borrow(), BridgeState::Stopped) {
429 break;
430 }
431 if shutting_down.load(Ordering::SeqCst) {
432 tracing::info!(
433 "Pool shutting down — not restarting bridge for broker '{}'",
434 broker_name
435 );
436 break;
437 }
438 let _ = slot.state_tx.send(BridgeState::Restarting {
439 attempt: 0,
440 next_at: Instant::now(),
441 });
442 }
443 BridgeState::Restarting { attempt, next_at } => {
444 if shutting_down.load(Ordering::SeqCst) {
445 tracing::info!(
446 "Pool shutting down — aborting restart for broker '{}'",
447 broker_name
448 );
449 break;
450 }
451
452 let now = Instant::now();
453 if now < next_at {
454 tokio::time::sleep(next_at - now).await;
455 }
456
457 info!(
458 "Restarting bridge for broker '{}' (attempt {})",
459 slot.name,
460 attempt + 1
461 );
462
463 if attempt >= MAX_RESTART_ATTEMPTS {
464 tracing::error!(
466 "Max restart attempts ({}) reached for broker '{}' — staying degraded",
467 attempt,
468 broker_name
469 );
470 let _ = slot.state_tx.send(BridgeState::Degraded(format!(
471 "max restart attempts ({}) exceeded",
472 attempt
473 )));
474 break;
475 }
476
477 let old_process = {
478 let mut guard = slot.process.lock().await;
479 guard.take()
480 };
481 if let Some(p) = old_process {
482 let _ = p.stop().await;
483 }
484
485 let start_result = Self::start_bridge_process(
486 &bridge_version,
487 &bridge_cache_dir,
488 start_timeout_ms,
489 &slot.broker_url,
490 &slot.broker_type,
491 &slot.credentials,
492 )
493 .await;
494
495 match start_result {
496 Ok((process, channel)) => {
497 if matches!(*slot.state_rx.borrow(), BridgeState::Stopped) {
500 let _ = process.stop().await;
501 break;
502 }
503 {
504 let mut guard = slot.process.lock().await;
505 *guard = Some(process);
506 }
507 let _ = slot.state_tx.send(BridgeState::Ready { channel });
508 info!("Broker '{}' bridge restarted successfully", slot.name);
509 }
510 Err(e) => {
511 if matches!(*slot.state_rx.borrow(), BridgeState::Stopped) {
513 break;
514 }
515 let delay_secs = std::cmp::min(5 * 2u64.pow(attempt), 120);
516 let next = Instant::now() + Duration::from_secs(delay_secs);
517 warn!(
518 "Failed to restart bridge for '{}' (attempt {}): {e}. Retry in {delay_secs}s",
519 slot.name,
520 attempt + 1
521 );
522 let _ = slot.state_tx.send(BridgeState::Restarting {
523 attempt: attempt + 1,
524 next_at: next,
525 });
526 }
527 }
528 }
529 }
530 }
531 });
532
533 let mut guard = handle_ref.lock().await;
535 *guard = Some(handle);
536 }
537
538 async fn start_bridge_process(
539 bridge_version: &str,
540 bridge_cache_dir: &std::path::Path,
541 start_timeout_ms: u64,
542 broker_url: &str,
543 broker_type: &BrokerType,
544 credentials: &Option<(String, String)>,
545 ) -> Result<(BridgeProcess, Channel), CamelError> {
546 info!(
547 "Starting JMS bridge process for {}...",
548 redact_url(broker_url)
549 );
550 let binary_path = ensure_binary(bridge_version, bridge_cache_dir)
551 .await
552 .map_err(|e| {
553 CamelError::ProcessorError(format!("JMS bridge binary unavailable: {e}"))
554 })?;
555
556 let process_config = BridgeProcessConfig::jms(
557 binary_path,
558 broker_url.to_string(),
559 broker_type.clone(),
560 credentials.as_ref().map(|(u, _)| u.clone()),
561 credentials
562 .as_ref()
563 .map(|(_, p)| camel_bridge::process::Redacted::new(p.clone())),
564 start_timeout_ms,
565 );
566
567 let total_timeout = Duration::from_millis(start_timeout_ms);
568 let result = tokio::time::timeout(total_timeout, async {
569 let (process, channel) = BridgeProcess::start_and_connect(&process_config)
570 .await
571 .map_err(|e| CamelError::ProcessorError(format!("JMS bridge start failed: {e}")))?;
572
573 wait_for_health(&channel, Duration::from_secs(10), |ch| {
574 let mut client = BridgeServiceClient::new(ch);
575 async move {
576 let resp = client.health(HealthRequest {}).await?;
577 Ok(resp.into_inner().healthy)
578 }
579 })
580 .await
581 .map_err(|e| {
582 CamelError::ProcessorError(format!("JMS bridge health check failed: {e}"))
583 })?;
584
585 Ok::<(BridgeProcess, Channel), CamelError>((process, channel))
586 })
587 .await
588 .map_err(|_| {
589 CamelError::ProcessorError(format!(
590 "JMS bridge start timed out after {}ms",
591 start_timeout_ms
592 ))
593 })??;
594
595 Ok(result)
596 }
597}
598
599#[derive(Clone)]
602pub struct JmsComponent {
603 scheme: String,
604 pool: Arc<JmsBridgePool>,
605}
606
607impl JmsComponent {
608 pub fn with_scheme(scheme: impl Into<String>, pool: Arc<JmsBridgePool>) -> Self {
609 Self {
610 scheme: scheme.into(),
611 pool,
612 }
613 }
614
615 pub fn scheme(&self) -> &str {
616 &self.scheme
617 }
618
619 #[cfg(test)]
621 pub async fn send_for_test(
622 &self,
623 destination: &str,
624 body: &[u8],
625 content_type: &str,
626 ) -> Result<String, CamelError> {
627 let broker_name = self.pool.resolve_broker_name(None)?;
628 let slot = self.pool.get_or_create_slot(&broker_name).await?;
629 let channel = match &*slot.state_rx.borrow() {
630 BridgeState::Ready { channel } => channel.clone(),
631 other => {
632 return Err(CamelError::ProcessorError(format!(
633 "Bridge not ready: {:?}",
634 other
635 )));
636 }
637 };
638 let mut client = BridgeServiceClient::new(channel);
639 let r = client
640 .send(crate::proto::SendRequest {
641 destination: destination.to_string(),
642 body: body.to_vec(),
643 headers: Default::default(),
644 content_type: content_type.to_string(),
645 })
646 .await
647 .map_err(|e| CamelError::ProcessorError(format!("test send error: {e}")))?;
648 Ok(r.into_inner().message_id)
649 }
650}
651
652impl Component for JmsComponent {
653 fn scheme(&self) -> &str {
654 &self.scheme
655 }
656
657 fn create_endpoint(
658 &self,
659 uri: &str,
660 ctx: &dyn camel_component_api::ComponentContext,
661 ) -> Result<Box<dyn Endpoint>, CamelError> {
662 let endpoint_config = JmsEndpointConfig::from_uri(uri)?;
663 let broker_name = self
664 .pool
665 .resolve_broker_name(endpoint_config.broker_name.as_deref())?;
666 let resolved_broker_type = self.pool.resolve_broker_type(&self.scheme, &broker_name);
667
668 let health_check = JmsHealthCheck::new(Arc::clone(&self.pool), broker_name.clone());
669 ctx.register_current_route_health_check(Arc::new(health_check));
670
671 Ok(Box::new(JmsEndpoint {
672 pool: Arc::clone(&self.pool),
673 uri: uri.to_string(),
674 broker_name,
675 resolved_broker_type,
676 endpoint_config,
677 }))
678 }
679}
680
681struct JmsEndpoint {
684 pool: Arc<JmsBridgePool>,
685 uri: String,
686 broker_name: String,
687 resolved_broker_type: BrokerType,
688 endpoint_config: JmsEndpointConfig,
689}
690
691impl Endpoint for JmsEndpoint {
692 fn uri(&self) -> &str {
693 &self.uri
694 }
695
696 fn create_producer(
697 &self,
698 rt: Arc<dyn camel_component_api::RuntimeObservability>,
699 _ctx: &ProducerContext,
700 ) -> Result<BoxProcessor, CamelError> {
701 Ok(BoxProcessor::new(LazyJmsProducer {
702 pool: Arc::clone(&self.pool),
703 broker_name: self.broker_name.clone(),
704 endpoint_config: self.endpoint_config.clone(),
705 resolved_broker_type: self.resolved_broker_type.clone(),
706 runtime: rt,
707 }))
708 }
709
710 fn create_consumer(
711 &self,
712 rt: Arc<dyn camel_component_api::RuntimeObservability>,
713 ) -> Result<Box<dyn Consumer>, CamelError> {
714 Ok(Box::new(JmsConsumer::new(
715 Arc::clone(&self.pool),
716 self.broker_name.clone(),
717 self.endpoint_config.clone(),
718 self.pool.reconnect.clone(),
719 rt,
720 )))
721 }
722}
723
724#[derive(Clone)]
725struct LazyJmsProducer {
726 pool: Arc<JmsBridgePool>,
727 broker_name: String,
728 endpoint_config: JmsEndpointConfig,
729 #[allow(dead_code)]
730 resolved_broker_type: BrokerType,
731 #[allow(dead_code)]
734 runtime: Arc<dyn camel_component_api::RuntimeObservability>,
735}
736
737impl Service<Exchange> for LazyJmsProducer {
738 type Response = Exchange;
739 type Error = CamelError;
740 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
741
742 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
743 if let Some(slot) = self.pool.slots.get(&self.broker_name) {
746 match &*slot.state_rx.borrow() {
747 BridgeState::Ready { .. } => return Poll::Ready(Ok(())),
748 BridgeState::Starting | BridgeState::Restarting { .. } => {
749 let waker = cx.waker().clone();
755 let mut rx = slot.state_rx.clone();
756 if let Ok(handle) = tokio::runtime::Handle::try_current() {
757 handle.spawn(async move {
758 let _ = rx.changed().await;
759 waker.wake();
760 });
761 } else {
762 waker.wake_by_ref();
763 }
764 return Poll::Pending;
765 }
766 BridgeState::Degraded(reason) => {
767 return Poll::Ready(Err(CamelError::ProcessorError(format!(
768 "JMS broker '{}' is degraded: {}",
769 self.broker_name, reason
770 ))));
771 }
772 BridgeState::Stopped => {
773 return Poll::Ready(Err(CamelError::ProcessorError(format!(
774 "JMS broker '{}' is stopped",
775 self.broker_name
776 ))));
777 }
778 }
779 }
780 Poll::Ready(Ok(()))
781 }
782
783 fn call(&mut self, exchange: Exchange) -> Self::Future {
784 let pool = Arc::clone(&self.pool);
785 let broker_name = self.broker_name.clone();
786 let endpoint_config = self.endpoint_config.clone();
787
788 Box::pin(async move {
789 let slot = pool.get_or_create_slot(&broker_name).await?;
790 let mut rx = slot.state_rx.clone();
791
792 loop {
793 let state = rx.borrow().clone();
794 match state {
795 BridgeState::Ready { channel } => {
796 let mut producer = JmsProducer::new(channel, endpoint_config.clone());
797 match producer.call(exchange).await {
798 Ok(done) => return Ok(done),
799 Err(first_err) if is_bridge_transport_error(&first_err) => {
800 warn!(
801 broker = %broker_name,
802 error = %first_err,
803 "JMS send transport error; refreshing channel (no automatic resend)"
804 );
805
806 if let Err(refresh_err) =
807 pool.refresh_slot_channel(&broker_name).await
808 {
809 warn!(
810 broker = %broker_name,
811 error = %refresh_err,
812 "JMS channel refresh failed; requesting bridge restart"
813 );
814 pool.restart_slot(&broker_name);
815 }
816
817 return Err(first_err);
822 }
823 Err(other_err) => return Err(other_err),
824 }
825 }
826 BridgeState::Degraded(reason) => {
827 return Err(CamelError::ProcessorError(format!(
828 "JMS broker '{}' is degraded: {}",
829 broker_name, reason
830 )));
831 }
832 BridgeState::Stopped => {
833 return Err(CamelError::ProcessorError(format!(
834 "JMS broker '{}' is stopped",
835 broker_name
836 )));
837 }
838 BridgeState::Starting | BridgeState::Restarting { .. } => {
839 if rx.changed().await.is_err() {
840 return Err(CamelError::ProcessorError(format!(
841 "JMS broker '{}' state channel closed",
842 broker_name
843 )));
844 }
845 }
846 }
847 }
848 })
849 }
850}
851
852fn redact_url(url: &str) -> String {
857 if let Some(pos) = url.find("://") {
859 let scheme = &url[..pos + 3]; let rest = &url[pos + 3..];
861 if let Some(at_pos) = rest.find('@') {
863 return format!("{}***@{}", scheme, &rest[at_pos + 1..]);
864 }
865 }
866 url.to_string()
867}
868
869pub fn is_bridge_transport_error(err: &CamelError) -> bool {
870 match err {
875 CamelError::ProcessorError(msg) => msg.starts_with(BRIDGE_TRANSPORT_ERROR_PREFIX),
876 _ => false,
877 }
878}
879
880#[cfg(test)]
883mod tests {
884 use camel_component_api::test_support::PanicRuntimeObservability;
885 fn test_rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
886 std::sync::Arc::new(PanicRuntimeObservability)
887 }
888 fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
889 std::sync::Arc::new(PanicRuntimeObservability)
890 }
891
892 use super::*;
893 use crate::config::{BrokerConfig, JmsPoolConfig};
894 use std::collections::HashMap;
895
896 #[test]
897 fn from_config_accepts_empty_brokers() {
898 let pool_config = JmsPoolConfig::default();
899 let result = JmsBridgePool::from_config(pool_config);
900 assert!(result.is_ok());
901 }
902
903 #[test]
904 fn resolve_broker_name_with_explicit_name() {
905 let pool = JmsBridgePool::from_config(JmsPoolConfig::single_broker(
906 "tcp://localhost:61616",
907 BrokerType::ActiveMq,
908 ))
909 .unwrap();
910 assert_eq!(
911 pool.resolve_broker_name(Some("default")).unwrap(),
912 "default"
913 );
914 }
915
916 #[test]
917 fn resolve_broker_name_default() {
918 let pool = JmsBridgePool::from_config(JmsPoolConfig::single_broker(
919 "tcp://localhost:61616",
920 BrokerType::ActiveMq,
921 ))
922 .unwrap();
923 assert_eq!(pool.resolve_broker_name(None).unwrap(), "default");
924 }
925
926 #[test]
927 fn resolve_broker_name_unknown_returns_error() {
928 let pool = JmsBridgePool::from_config(JmsPoolConfig::single_broker(
929 "tcp://localhost:61616",
930 BrokerType::ActiveMq,
931 ))
932 .unwrap();
933 let err = pool.resolve_broker_name(Some("unknown")).unwrap_err();
934 assert!(
935 err.to_string().contains("Unknown JMS broker 'unknown'"),
936 "got: {}",
937 err
938 );
939 }
940
941 #[test]
942 fn resolve_broker_type_scheme_overrides() {
943 let pool = JmsBridgePool::from_config(JmsPoolConfig::single_broker(
944 "tcp://localhost:61616",
945 BrokerType::Generic,
946 ))
947 .unwrap();
948 assert_eq!(
949 pool.resolve_broker_type("activemq", "default"),
950 BrokerType::ActiveMq
951 );
952 assert_eq!(
953 pool.resolve_broker_type("artemis", "default"),
954 BrokerType::Artemis
955 );
956 assert_eq!(
957 pool.resolve_broker_type("jms", "default"),
958 BrokerType::Generic
959 );
960 }
961
962 #[test]
963 fn resolve_broker_type_activemq_scheme_overrides_artemis_config() {
964 let pool = JmsBridgePool::from_config(JmsPoolConfig {
965 brokers: HashMap::from([(
966 "main".to_string(),
967 BrokerConfig {
968 broker_url: "tcp://localhost:61616".to_string(),
969 broker_type: BrokerType::Artemis,
970 username: None,
971 password: None,
972 },
973 )]),
974 ..JmsPoolConfig::default()
975 })
976 .unwrap();
977 assert_eq!(
978 pool.resolve_broker_type("activemq", "main"),
979 BrokerType::ActiveMq
980 );
981 assert_eq!(pool.resolve_broker_type("jms", "main"), BrokerType::Artemis);
982 }
983
984 #[test]
985 fn create_endpoint_resolves_broker() {
986 let pool = Arc::new(
987 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
988 "tcp://localhost:61616",
989 BrokerType::ActiveMq,
990 ))
991 .unwrap(),
992 );
993 let component = JmsComponent::with_scheme("jms", pool);
994 let endpoint = component.create_endpoint(
995 "jms:queue:orders",
996 &camel_component_api::NoOpComponentContext,
997 );
998 assert!(endpoint.is_ok(), "got: {:?}", endpoint.err());
999 }
1000
1001 #[test]
1002 fn create_endpoint_rejects_wrong_scheme() {
1003 let pool = Arc::new(
1004 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1005 "tcp://localhost:61616",
1006 BrokerType::ActiveMq,
1007 ))
1008 .unwrap(),
1009 );
1010 let component = JmsComponent::with_scheme("jms", pool);
1011 let err = component
1012 .create_endpoint("kafka:orders", &camel_component_api::NoOpComponentContext)
1013 .err()
1014 .unwrap();
1015 assert!(
1016 err.to_string()
1017 .contains("expected scheme 'jms', 'activemq', or 'artemis'"),
1018 "got: {}",
1019 err
1020 );
1021 }
1022
1023 #[test]
1024 fn create_endpoint_with_explicit_broker_param() {
1025 let pool = Arc::new(
1026 JmsBridgePool::from_config(JmsPoolConfig {
1027 brokers: HashMap::from([
1028 (
1029 "primary".to_string(),
1030 BrokerConfig {
1031 broker_url: "tcp://primary:61616".to_string(),
1032 broker_type: BrokerType::ActiveMq,
1033 username: None,
1034 password: None,
1035 },
1036 ),
1037 (
1038 "secondary".to_string(),
1039 BrokerConfig {
1040 broker_url: "tcp://secondary:61616".to_string(),
1041 broker_type: BrokerType::Artemis,
1042 username: None,
1043 password: None,
1044 },
1045 ),
1046 ]),
1047 ..JmsPoolConfig::default()
1048 })
1049 .unwrap(),
1050 );
1051 let component = JmsComponent::with_scheme("jms", Arc::clone(&pool));
1052 let endpoint = component.create_endpoint(
1053 "jms:queue:orders?broker=secondary",
1054 &camel_component_api::NoOpComponentContext,
1055 );
1056 assert!(endpoint.is_ok(), "got: {:?}", endpoint.err());
1057 }
1058
1059 #[tokio::test]
1060 async fn concurrent_get_or_create_slot_no_deadlock() {
1061 use tokio::time::timeout;
1062
1063 struct EnvGuard {
1064 key: &'static str,
1065 prev: Option<std::ffi::OsString>,
1066 }
1067 impl Drop for EnvGuard {
1068 fn drop(&mut self) {
1069 if let Some(v) = &self.prev {
1070 unsafe { std::env::set_var(self.key, v) };
1072 } else {
1073 unsafe { std::env::remove_var(self.key) };
1075 }
1076 }
1077 }
1078
1079 let env_key = "CAMEL_JMS_BRIDGE_BINARY_PATH";
1080 let _guard = EnvGuard {
1081 key: env_key,
1082 prev: std::env::var_os(env_key),
1083 };
1084 unsafe { std::env::set_var(env_key, "/bin/false") };
1086
1087 let pool = Arc::new(
1088 JmsBridgePool::from_config(JmsPoolConfig {
1089 brokers: HashMap::from([(
1090 "test".to_string(),
1091 BrokerConfig {
1092 broker_url: "tcp://localhost:61616".to_string(),
1093 broker_type: BrokerType::ActiveMq,
1094 username: None,
1095 password: None,
1096 },
1097 )]),
1098 bridge_start_timeout_ms: 100,
1099 ..JmsPoolConfig::default()
1100 })
1101 .unwrap(),
1102 );
1103
1104 let handles: Vec<_> = (0..5)
1105 .map(|_| {
1106 let pool = Arc::clone(&pool);
1107 tokio::spawn(async move {
1108 let _ = pool.get_or_create_slot("test").await;
1109 })
1110 })
1111 .collect();
1112
1113 let result = timeout(Duration::from_secs(5), async {
1114 for h in handles {
1115 let _ = h.await;
1116 }
1117 })
1118 .await;
1119
1120 assert!(result.is_ok(), "Concurrent get_or_create_slot deadlocked!");
1121 }
1122
1123 #[tokio::test]
1124 async fn lazy_producer_reports_degraded_when_bridge_start_fails() {
1125 use tower::Service;
1126
1127 struct EnvGuard {
1128 key: &'static str,
1129 prev: Option<std::ffi::OsString>,
1130 }
1131 impl Drop for EnvGuard {
1132 fn drop(&mut self) {
1133 if let Some(v) = &self.prev {
1134 unsafe { std::env::set_var(self.key, v) };
1136 } else {
1137 unsafe { std::env::remove_var(self.key) };
1139 }
1140 }
1141 }
1142
1143 let env_key = "CAMEL_JMS_BRIDGE_BINARY_PATH";
1144 let _guard = EnvGuard {
1145 key: env_key,
1146 prev: std::env::var_os(env_key),
1147 };
1148 unsafe { std::env::set_var(env_key, "/bin/false") };
1150
1151 let pool = Arc::new(
1152 JmsBridgePool::from_config(JmsPoolConfig {
1153 brokers: HashMap::from([(
1154 "default".to_string(),
1155 BrokerConfig {
1156 broker_url: "tcp://localhost:61616".to_string(),
1157 broker_type: BrokerType::ActiveMq,
1158 username: None,
1159 password: None,
1160 },
1161 )]),
1162 bridge_start_timeout_ms: 100,
1163 ..JmsPoolConfig::default()
1164 })
1165 .unwrap(),
1166 );
1167
1168 let component = JmsComponent::with_scheme("jms", pool);
1169 let endpoint = component
1170 .create_endpoint(
1171 "jms:queue:orders",
1172 &camel_component_api::NoOpComponentContext,
1173 )
1174 .unwrap();
1175 let mut producer = endpoint
1176 .create_producer(rt(), &camel_component_api::ProducerContext::default())
1177 .unwrap();
1178
1179 let mut exchange = Exchange::default();
1180 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1181
1182 let err = producer.call(exchange).await.unwrap_err();
1183 assert!(err.to_string().contains("is degraded"), "got: {}", err);
1184 }
1185
1186 #[tokio::test]
1190 async fn lazy_producer_requests_restart_when_refresh_unavailable() {
1191 use tokio::sync::watch;
1192 use tonic::transport::Endpoint as TonicEndpoint;
1193 use tower::Service;
1194
1195 let dead_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1198
1199 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1200 channel: dead_channel.clone(),
1201 });
1202
1203 let pool = Arc::new(
1204 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1205 "tcp://localhost:61616",
1206 BrokerType::ActiveMq,
1207 ))
1208 .unwrap(),
1209 );
1210
1211 let slot = Arc::new(BridgeSlot {
1213 name: "default".to_string(),
1214 broker_url: "tcp://localhost:61616".to_string(),
1215 broker_type: BrokerType::ActiveMq,
1216 credentials: None,
1217 state_rx: state_rx.clone(),
1218 state_tx: state_tx.clone(),
1219 process: Arc::new(tokio::sync::Mutex::new(None)),
1220 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1221 });
1222 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1223
1224 let endpoint_config =
1225 crate::config::JmsEndpointConfig::from_uri("jms:queue:test-retry").unwrap();
1226
1227 let mut producer = LazyJmsProducer {
1228 pool: Arc::clone(&pool),
1229 broker_name: "default".to_string(),
1230 endpoint_config,
1231 resolved_broker_type: BrokerType::ActiveMq,
1232 runtime: test_rt(),
1233 };
1234
1235 let mut exchange = Exchange::default();
1236 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1237
1238 let result = producer.call(exchange).await;
1240 assert!(result.is_err(), "expected send to fail");
1241
1242 let state_after = state_rx.borrow().clone();
1245 assert!(
1246 matches!(state_after, BridgeState::Restarting { .. }),
1247 "slot must enter Restarting when refresh is unavailable; got: {:?}",
1248 state_after
1249 );
1250 }
1251
1252 #[test]
1255 fn transport_error_detects_send_error() {
1256 let err = CamelError::ProcessorError(format!(
1257 "{}send error: connection refused",
1258 BRIDGE_TRANSPORT_ERROR_PREFIX
1259 ));
1260 assert!(
1261 is_bridge_transport_error(&err),
1262 "send error must be classified as transport"
1263 );
1264 }
1265
1266 #[test]
1267 fn transport_error_detects_subscribe_error() {
1268 let err = CamelError::ProcessorError(format!(
1269 "{}subscribe error: stream reset",
1270 BRIDGE_TRANSPORT_ERROR_PREFIX
1271 ));
1272 assert!(
1273 is_bridge_transport_error(&err),
1274 "subscribe error must be classified as transport"
1275 );
1276 }
1277
1278 #[test]
1279 fn transport_error_rejects_business_errors() {
1280 let err = CamelError::ProcessorError("JMS broker 'main' is degraded: timeout".to_string());
1281 assert!(
1282 !is_bridge_transport_error(&err),
1283 "degraded state error must NOT be transport"
1284 );
1285 }
1286
1287 #[test]
1288 fn transport_error_rejects_config_errors() {
1289 let err = CamelError::Config("bridge_start_timeout_ms must be > 0".to_string());
1290 assert!(
1291 !is_bridge_transport_error(&err),
1292 "config error must NOT be transport"
1293 );
1294 }
1295
1296 #[test]
1297 fn transport_error_prefix_is_used_by_producer_and_consumer() {
1298 assert!(
1301 BRIDGE_TRANSPORT_ERROR_PREFIX.starts_with("JMS gRPC "),
1302 "prefix must start with 'JMS gRPC '"
1303 );
1304 }
1305
1306 #[tokio::test]
1309 async fn pool_enforces_max_bridges_limit() {
1310 use tokio::sync::watch;
1311
1312 let pool = Arc::new(
1313 JmsBridgePool::from_config(JmsPoolConfig {
1314 brokers: HashMap::from([
1315 (
1316 "b1".to_string(),
1317 BrokerConfig {
1318 broker_url: "tcp://b1:61616".to_string(),
1319 broker_type: BrokerType::ActiveMq,
1320 username: None,
1321 password: None,
1322 },
1323 ),
1324 (
1325 "b2".to_string(),
1326 BrokerConfig {
1327 broker_url: "tcp://b2:61616".to_string(),
1328 broker_type: BrokerType::ActiveMq,
1329 username: None,
1330 password: None,
1331 },
1332 ),
1333 (
1334 "b3".to_string(),
1335 BrokerConfig {
1336 broker_url: "tcp://b3:61616".to_string(),
1337 broker_type: BrokerType::ActiveMq,
1338 username: None,
1339 password: None,
1340 },
1341 ),
1342 ]),
1343 max_bridges: 2,
1344 ..JmsPoolConfig::default()
1345 })
1346 .unwrap(),
1347 );
1348
1349 for name in &["b1", "b2"] {
1352 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1353 let slot = Arc::new(BridgeSlot {
1354 name: name.to_string(),
1355 broker_url: format!("tcp://{name}:61616"),
1356 broker_type: BrokerType::ActiveMq,
1357 credentials: None,
1358 state_rx,
1359 state_tx,
1360 process: Arc::new(tokio::sync::Mutex::new(None)),
1361 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1362 });
1363 pool.slots.insert(name.to_string(), slot);
1364 }
1365
1366 let err = pool.get_or_create_slot("b3").await.unwrap_err();
1368 assert!(
1369 err.to_string().contains("max_bridges"),
1370 "expected max_bridges error, got: {}",
1371 err
1372 );
1373 }
1374
1375 #[tokio::test]
1376 async fn pool_allows_slot_when_below_max_bridges() {
1377 use tokio::sync::watch;
1378
1379 let pool = Arc::new(
1380 JmsBridgePool::from_config(JmsPoolConfig {
1381 brokers: HashMap::from([(
1382 "b1".to_string(),
1383 BrokerConfig {
1384 broker_url: "tcp://b1:61616".to_string(),
1385 broker_type: BrokerType::ActiveMq,
1386 username: None,
1387 password: None,
1388 },
1389 )]),
1390 max_bridges: 2,
1391 bridge_start_timeout_ms: 100,
1392 ..JmsPoolConfig::default()
1393 })
1394 .unwrap(),
1395 );
1396
1397 let (state_tx, state_rx) = watch::channel(BridgeState::Degraded("test".to_string()));
1399 let slot = Arc::new(BridgeSlot {
1400 name: "b1".to_string(),
1401 broker_url: "tcp://b1:61616".to_string(),
1402 broker_type: BrokerType::ActiveMq,
1403 credentials: None,
1404 state_rx,
1405 state_tx,
1406 process: Arc::new(tokio::sync::Mutex::new(None)),
1407 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1408 });
1409 pool.slots.insert("b1".to_string(), slot);
1410
1411 let result = pool.get_or_create_slot("b1").await;
1414 assert!(result.is_ok(), "existing slot must be returned");
1415 }
1416
1417 #[tokio::test]
1420 async fn poll_ready_returns_pending_when_starting() {
1421 use tokio::sync::watch;
1422 use tower::Service;
1423
1424 let pool = Arc::new(
1425 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1426 "tcp://localhost:61616",
1427 BrokerType::ActiveMq,
1428 ))
1429 .unwrap(),
1430 );
1431
1432 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1433 let slot = Arc::new(BridgeSlot {
1434 name: "default".to_string(),
1435 broker_url: "tcp://localhost:61616".to_string(),
1436 broker_type: BrokerType::ActiveMq,
1437 credentials: None,
1438 state_rx,
1439 state_tx,
1440 process: Arc::new(tokio::sync::Mutex::new(None)),
1441 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1442 });
1443 pool.slots.insert("default".to_string(), slot);
1444
1445 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1446 let mut producer = LazyJmsProducer {
1447 pool: Arc::clone(&pool),
1448 broker_name: "default".to_string(),
1449 endpoint_config,
1450 resolved_broker_type: BrokerType::ActiveMq,
1451 runtime: test_rt(),
1452 };
1453
1454 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1455 assert!(
1456 matches!(result, Poll::Pending),
1457 "poll_ready must be Pending when Starting; got: {:?}",
1458 result
1459 );
1460 }
1461
1462 #[tokio::test]
1463 async fn poll_ready_returns_error_when_degraded() {
1464 use tokio::sync::watch;
1465 use tower::Service;
1466
1467 let pool = Arc::new(
1468 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1469 "tcp://localhost:61616",
1470 BrokerType::ActiveMq,
1471 ))
1472 .unwrap(),
1473 );
1474
1475 let (state_tx, state_rx) =
1476 watch::channel(BridgeState::Degraded("health check failed".to_string()));
1477 let slot = Arc::new(BridgeSlot {
1478 name: "default".to_string(),
1479 broker_url: "tcp://localhost:61616".to_string(),
1480 broker_type: BrokerType::ActiveMq,
1481 credentials: None,
1482 state_rx,
1483 state_tx,
1484 process: Arc::new(tokio::sync::Mutex::new(None)),
1485 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1486 });
1487 pool.slots.insert("default".to_string(), slot);
1488
1489 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1490 let mut producer = LazyJmsProducer {
1491 pool: Arc::clone(&pool),
1492 broker_name: "default".to_string(),
1493 endpoint_config,
1494 resolved_broker_type: BrokerType::ActiveMq,
1495 runtime: test_rt(),
1496 };
1497
1498 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1499 assert!(
1500 matches!(result, Poll::Ready(Err(_))),
1501 "poll_ready must be Err when Degraded; got: {:?}",
1502 result
1503 );
1504 let err_msg = match result {
1505 Poll::Ready(Err(e)) => e.to_string(),
1506 _ => unreachable!(),
1507 };
1508 assert!(
1509 err_msg.contains("degraded"),
1510 "error must mention degraded: {}",
1511 err_msg
1512 );
1513 }
1514
1515 #[tokio::test]
1516 async fn poll_ready_returns_error_when_stopped() {
1517 use tokio::sync::watch;
1518 use tower::Service;
1519
1520 let pool = Arc::new(
1521 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1522 "tcp://localhost:61616",
1523 BrokerType::ActiveMq,
1524 ))
1525 .unwrap(),
1526 );
1527
1528 let (state_tx, state_rx) = watch::channel(BridgeState::Stopped);
1529 let slot = Arc::new(BridgeSlot {
1530 name: "default".to_string(),
1531 broker_url: "tcp://localhost:61616".to_string(),
1532 broker_type: BrokerType::ActiveMq,
1533 credentials: None,
1534 state_rx,
1535 state_tx,
1536 process: Arc::new(tokio::sync::Mutex::new(None)),
1537 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1538 });
1539 pool.slots.insert("default".to_string(), slot);
1540
1541 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1542 let mut producer = LazyJmsProducer {
1543 pool: Arc::clone(&pool),
1544 broker_name: "default".to_string(),
1545 endpoint_config,
1546 resolved_broker_type: BrokerType::ActiveMq,
1547 runtime: test_rt(),
1548 };
1549
1550 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1551 assert!(
1552 matches!(result, Poll::Ready(Err(_))),
1553 "poll_ready must be Err when Stopped; got: {:?}",
1554 result
1555 );
1556 }
1557
1558 #[tokio::test]
1559 async fn poll_ready_returns_ready_when_slot_ready() {
1560 use tokio::sync::watch;
1561 use tonic::transport::Endpoint as TonicEndpoint;
1562 use tower::Service;
1563
1564 let pool = Arc::new(
1565 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1566 "tcp://localhost:61616",
1567 BrokerType::ActiveMq,
1568 ))
1569 .unwrap(),
1570 );
1571
1572 let lazy_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1573 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1574 channel: lazy_channel,
1575 });
1576 let slot = Arc::new(BridgeSlot {
1577 name: "default".to_string(),
1578 broker_url: "tcp://localhost:61616".to_string(),
1579 broker_type: BrokerType::ActiveMq,
1580 credentials: None,
1581 state_rx,
1582 state_tx,
1583 process: Arc::new(tokio::sync::Mutex::new(None)),
1584 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1585 });
1586 pool.slots.insert("default".to_string(), slot);
1587
1588 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1589 let mut producer = LazyJmsProducer {
1590 pool: Arc::clone(&pool),
1591 broker_name: "default".to_string(),
1592 endpoint_config,
1593 resolved_broker_type: BrokerType::ActiveMq,
1594 runtime: test_rt(),
1595 };
1596
1597 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1598 assert!(
1599 matches!(result, Poll::Ready(Ok(()))),
1600 "poll_ready must be Ready(Ok) when bridge is Ready; got: {:?}",
1601 result
1602 );
1603 }
1604
1605 #[tokio::test]
1606 async fn poll_ready_returns_ready_when_no_slot_exists() {
1607 use tower::Service;
1608
1609 let pool = Arc::new(
1610 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1611 "tcp://localhost:61616",
1612 BrokerType::ActiveMq,
1613 ))
1614 .unwrap(),
1615 );
1616
1617 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1618 let mut producer = LazyJmsProducer {
1619 pool: Arc::clone(&pool),
1620 broker_name: "default".to_string(),
1621 endpoint_config,
1622 resolved_broker_type: BrokerType::ActiveMq,
1623 runtime: test_rt(),
1624 };
1625
1626 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1628 assert!(
1629 matches!(result, Poll::Ready(Ok(()))),
1630 "poll_ready must be Ready(Ok) when no slot exists; got: {:?}",
1631 result
1632 );
1633 }
1634
1635 #[tokio::test]
1638 async fn pool_shutdown_awaits_health_monitor() {
1639 use tokio::sync::watch;
1640
1641 let pool = Arc::new(
1642 JmsBridgePool::from_config(JmsPoolConfig {
1643 brokers: HashMap::from([(
1644 "default".to_string(),
1645 BrokerConfig {
1646 broker_url: "tcp://localhost:61616".to_string(),
1647 broker_type: BrokerType::ActiveMq,
1648 username: None,
1649 password: None,
1650 },
1651 )]),
1652 health_check_interval_ms: 100,
1653 ..JmsPoolConfig::default()
1654 })
1655 .unwrap(),
1656 );
1657
1658 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1660 channel: tonic::transport::Endpoint::from_static("http://127.0.0.1:1").connect_lazy(),
1661 });
1662 let monitor_handle_ref: Arc<tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>> =
1663 Arc::new(tokio::sync::Mutex::new(None));
1664
1665 let state_rx_clone = state_rx.clone();
1667 let handle = tokio::spawn(async move {
1668 loop {
1669 if matches!(*state_rx_clone.borrow(), BridgeState::Stopped) {
1670 break;
1671 }
1672 tokio::time::sleep(Duration::from_millis(50)).await;
1673 }
1674 });
1675 *monitor_handle_ref.lock().await = Some(handle);
1676
1677 let slot = Arc::new(BridgeSlot {
1678 name: "default".to_string(),
1679 broker_url: "tcp://localhost:61616".to_string(),
1680 broker_type: BrokerType::ActiveMq,
1681 credentials: None,
1682 state_rx,
1683 state_tx,
1684 process: Arc::new(tokio::sync::Mutex::new(None)),
1685 health_monitor_handle: monitor_handle_ref,
1686 });
1687 pool.slots.insert("default".to_string(), slot);
1688
1689 let result = pool.shutdown().await;
1691 let _ = result;
1693 }
1694
1695 #[tokio::test]
1696 async fn health_monitor_handle_stored_after_spawn() {
1697 use tokio::sync::watch;
1698
1699 let pool = Arc::new(
1700 JmsBridgePool::from_config(JmsPoolConfig {
1701 brokers: HashMap::from([(
1702 "default".to_string(),
1703 BrokerConfig {
1704 broker_url: "tcp://localhost:61616".to_string(),
1705 broker_type: BrokerType::ActiveMq,
1706 username: None,
1707 password: None,
1708 },
1709 )]),
1710 health_check_interval_ms: 100,
1711 bridge_start_timeout_ms: 100,
1712 ..JmsPoolConfig::default()
1713 })
1714 .unwrap(),
1715 );
1716
1717 let (state_tx, state_rx) = watch::channel(BridgeState::Stopped);
1718 let slot = Arc::new(BridgeSlot {
1719 name: "default".to_string(),
1720 broker_url: "tcp://localhost:61616".to_string(),
1721 broker_type: BrokerType::ActiveMq,
1722 credentials: None,
1723 state_rx,
1724 state_tx,
1725 process: Arc::new(tokio::sync::Mutex::new(None)),
1726 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1727 });
1728 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1729
1730 pool.spawn_health_monitor(Arc::clone(&slot)).await;
1731
1732 tokio::time::sleep(Duration::from_millis(50)).await;
1733 let guard = slot.health_monitor_handle.lock().await;
1734 assert!(
1735 guard.is_some(),
1736 "health monitor handle must be stored after spawn_health_monitor"
1737 );
1738 }
1739
1740 #[test]
1743 fn redact_url_strips_userinfo_with_password() {
1744 assert_eq!(
1745 redact_url("tcp://admin:s3cret@broker:61616"),
1746 "tcp://***@broker:61616"
1747 );
1748 }
1749
1750 #[test]
1751 fn redact_url_strips_userinfo_without_password() {
1752 assert_eq!(
1753 redact_url("tcp://admin@broker:61616"),
1754 "tcp://***@broker:61616"
1755 );
1756 }
1757
1758 #[test]
1759 fn redact_url_passes_clean_url_unchanged() {
1760 assert_eq!(redact_url("tcp://localhost:61616"), "tcp://localhost:61616");
1761 }
1762
1763 #[test]
1764 fn redact_url_handles_ssl_scheme() {
1765 assert_eq!(
1766 redact_url("ssl://user:pass@secure-broker:61617"),
1767 "ssl://***@secure-broker:61617"
1768 );
1769 }
1770
1771 #[tokio::test]
1774 async fn concurrent_slot_creation_respects_max_bridges() {
1775 let pool = Arc::new(
1776 JmsBridgePool::from_config(JmsPoolConfig {
1777 brokers: HashMap::from([
1778 (
1779 "b1".to_string(),
1780 BrokerConfig {
1781 broker_url: "tcp://b1:61616".to_string(),
1782 broker_type: BrokerType::ActiveMq,
1783 username: None,
1784 password: None,
1785 },
1786 ),
1787 (
1788 "b2".to_string(),
1789 BrokerConfig {
1790 broker_url: "tcp://b2:61616".to_string(),
1791 broker_type: BrokerType::ActiveMq,
1792 username: None,
1793 password: None,
1794 },
1795 ),
1796 (
1797 "b3".to_string(),
1798 BrokerConfig {
1799 broker_url: "tcp://b3:61616".to_string(),
1800 broker_type: BrokerType::ActiveMq,
1801 username: None,
1802 password: None,
1803 },
1804 ),
1805 ]),
1806 max_bridges: 2,
1807 bridge_start_timeout_ms: 100,
1808 ..JmsPoolConfig::default()
1809 })
1810 .unwrap(),
1811 );
1812
1813 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1814 for name in &["b1", "b2"] {
1815 let slot = Arc::new(BridgeSlot {
1816 name: name.to_string(),
1817 broker_url: format!("tcp://{name}:61616"),
1818 broker_type: BrokerType::ActiveMq,
1819 credentials: None,
1820 state_rx: state_rx.clone(),
1821 state_tx: state_tx.clone(),
1822 process: Arc::new(tokio::sync::Mutex::new(None)),
1823 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1824 });
1825 pool.slots.insert(name.to_string(), slot);
1826 }
1827
1828 assert_eq!(pool.slots.len(), 2);
1829
1830 let guard = pool.bridge_create_lock.lock().await;
1831 let total_count = pool.slots.len();
1832 assert!(total_count >= pool.max_bridges);
1833 let result = if total_count >= pool.max_bridges {
1834 Err(CamelError::Config(format!(
1835 "JMS bridge limit reached: {total_count} bridge(s) >= max_bridges ({})",
1836 pool.max_bridges
1837 )))
1838 } else {
1839 Ok(())
1840 };
1841 drop(guard);
1842
1843 assert!(result.is_err(), "3rd broker should be rejected");
1844 let err_msg = result.unwrap_err().to_string();
1845 assert!(
1846 err_msg.contains("max_bridges"),
1847 "error must mention max_bridges, got: {err_msg}"
1848 );
1849 }
1850
1851 #[tokio::test]
1854 async fn transport_error_refreshes_channel_but_does_not_resend() {
1855 use tokio::sync::watch;
1856 use tonic::transport::Endpoint as TonicEndpoint;
1857 use tower::Service;
1858
1859 let dead_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1860
1861 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1862 channel: dead_channel.clone(),
1863 });
1864
1865 let pool = Arc::new(
1866 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1867 "tcp://localhost:61616",
1868 BrokerType::ActiveMq,
1869 ))
1870 .unwrap(),
1871 );
1872
1873 let slot = Arc::new(BridgeSlot {
1874 name: "default".to_string(),
1875 broker_url: "tcp://localhost:61616".to_string(),
1876 broker_type: BrokerType::ActiveMq,
1877 credentials: None,
1878 state_rx: state_rx.clone(),
1879 state_tx: state_tx.clone(),
1880 process: Arc::new(tokio::sync::Mutex::new(None)),
1881 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1882 });
1883 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1884
1885 let endpoint_config =
1886 crate::config::JmsEndpointConfig::from_uri("jms:queue:test-no-resend").unwrap();
1887
1888 let mut producer = LazyJmsProducer {
1889 pool: Arc::clone(&pool),
1890 broker_name: "default".to_string(),
1891 endpoint_config,
1892 resolved_broker_type: BrokerType::ActiveMq,
1893 runtime: test_rt(),
1894 };
1895
1896 let mut exchange = Exchange::default();
1897 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1898
1899 let result = producer.call(exchange).await;
1900 assert!(result.is_err(), "expected send to fail");
1901
1902 let state_after = state_rx.borrow().clone();
1903 assert!(
1904 matches!(state_after, BridgeState::Restarting { .. }),
1905 "slot must enter Restarting; got: {:?}",
1906 state_after
1907 );
1908
1909 let err_msg = result.unwrap_err().to_string();
1910 assert!(
1911 err_msg.contains(BRIDGE_TRANSPORT_ERROR_PREFIX),
1912 "error must be original transport error, got: {}",
1913 err_msg
1914 );
1915 }
1916}