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
599impl Drop for JmsBridgePool {
602 fn drop(&mut self) {
603 self.shutting_down.store(true, Ordering::SeqCst);
604
605 let slots: Vec<(String, Arc<BridgeSlot>)> = self
607 .slots
608 .iter()
609 .map(|e| (e.key().clone(), e.value().clone()))
610 .collect();
611
612 if slots.is_empty() {
613 return;
614 }
615
616 match tokio::runtime::Handle::try_current() {
617 Ok(handle) => {
618 drop(handle.spawn(async move {
621 for (_name, slot) in slots {
622 let _ = slot.state_tx.send(BridgeState::Stopped);
624
625 let process = {
627 let mut guard = slot.process.lock().await;
628 guard.take()
629 };
630 if let Some(p) = process {
631 let _ = p.stop().await;
632 }
633
634 let monitor_handle = {
636 let mut guard = slot.health_monitor_handle.lock().await;
637 guard.take()
638 };
639 if let Some(mut h) = monitor_handle
640 && tokio::time::timeout(Duration::from_secs(5), &mut h)
641 .await
642 .is_err()
643 {
644 h.abort();
645 let _ = h.await;
646 warn!(
647 "health monitor for '{}' did not stop in 5s; aborted",
648 slot.name
649 );
650 }
651 }
652 }));
653 }
654 Err(_) => {
655 warn!("JmsBridgePool dropped outside tokio runtime; bridges not cleaned up");
656 }
657 }
658 }
659}
660
661#[derive(Clone)]
664pub struct JmsComponent {
665 scheme: String,
666 pool: Arc<JmsBridgePool>,
667}
668
669impl JmsComponent {
670 pub fn with_scheme(scheme: impl Into<String>, pool: Arc<JmsBridgePool>) -> Self {
671 Self {
672 scheme: scheme.into(),
673 pool,
674 }
675 }
676
677 pub fn scheme(&self) -> &str {
678 &self.scheme
679 }
680
681 #[cfg(test)]
683 pub async fn send_for_test(
684 &self,
685 destination: &str,
686 body: &[u8],
687 content_type: &str,
688 ) -> Result<String, CamelError> {
689 let broker_name = self.pool.resolve_broker_name(None)?;
690 let slot = self.pool.get_or_create_slot(&broker_name).await?;
691 let channel = match &*slot.state_rx.borrow() {
692 BridgeState::Ready { channel } => channel.clone(),
693 other => {
694 return Err(CamelError::ProcessorError(format!(
695 "Bridge not ready: {:?}",
696 other
697 )));
698 }
699 };
700 let mut client = BridgeServiceClient::new(channel);
701 let r = client
702 .send(crate::proto::SendRequest {
703 destination: destination.to_string(),
704 body: body.to_vec(),
705 headers: Default::default(),
706 content_type: content_type.to_string(),
707 })
708 .await
709 .map_err(|e| CamelError::ProcessorError(format!("test send error: {e}")))?;
710 Ok(r.into_inner().message_id)
711 }
712}
713
714impl Component for JmsComponent {
715 fn scheme(&self) -> &str {
716 &self.scheme
717 }
718
719 fn create_endpoint(
720 &self,
721 uri: &str,
722 ctx: &dyn camel_component_api::ComponentContext,
723 ) -> Result<Box<dyn Endpoint>, CamelError> {
724 let endpoint_config = JmsEndpointConfig::from_uri(uri)?;
725 let broker_name = self
726 .pool
727 .resolve_broker_name(endpoint_config.broker_name.as_deref())?;
728 let resolved_broker_type = self.pool.resolve_broker_type(&self.scheme, &broker_name);
729
730 let health_check = JmsHealthCheck::new(Arc::clone(&self.pool), broker_name.clone());
731 ctx.register_current_route_health_check(Arc::new(health_check));
732
733 Ok(Box::new(JmsEndpoint {
734 pool: Arc::clone(&self.pool),
735 uri: uri.to_string(),
736 broker_name,
737 resolved_broker_type,
738 endpoint_config,
739 }))
740 }
741}
742
743struct JmsEndpoint {
746 pool: Arc<JmsBridgePool>,
747 uri: String,
748 broker_name: String,
749 resolved_broker_type: BrokerType,
750 endpoint_config: JmsEndpointConfig,
751}
752
753impl Endpoint for JmsEndpoint {
754 fn uri(&self) -> &str {
755 &self.uri
756 }
757
758 fn create_producer(
759 &self,
760 rt: Arc<dyn camel_component_api::RuntimeObservability>,
761 _ctx: &ProducerContext,
762 ) -> Result<BoxProcessor, CamelError> {
763 Ok(BoxProcessor::new(LazyJmsProducer {
764 pool: Arc::clone(&self.pool),
765 broker_name: self.broker_name.clone(),
766 endpoint_config: self.endpoint_config.clone(),
767 resolved_broker_type: self.resolved_broker_type.clone(),
768 runtime: rt,
769 }))
770 }
771
772 fn create_consumer(
773 &self,
774 rt: Arc<dyn camel_component_api::RuntimeObservability>,
775 ) -> Result<Box<dyn Consumer>, CamelError> {
776 Ok(Box::new(JmsConsumer::new(
777 Arc::clone(&self.pool),
778 self.broker_name.clone(),
779 self.endpoint_config.clone(),
780 self.pool.reconnect.clone(),
781 rt,
782 )))
783 }
784}
785
786#[derive(Clone)]
787struct LazyJmsProducer {
788 pool: Arc<JmsBridgePool>,
789 broker_name: String,
790 endpoint_config: JmsEndpointConfig,
791 #[allow(dead_code)]
792 resolved_broker_type: BrokerType,
793 #[allow(dead_code)]
796 runtime: Arc<dyn camel_component_api::RuntimeObservability>,
797}
798
799impl Service<Exchange> for LazyJmsProducer {
800 type Response = Exchange;
801 type Error = CamelError;
802 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
803
804 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
805 if let Some(slot) = self.pool.slots.get(&self.broker_name) {
808 match &*slot.state_rx.borrow() {
809 BridgeState::Ready { .. } => return Poll::Ready(Ok(())),
810 BridgeState::Starting | BridgeState::Restarting { .. } => {
811 let waker = cx.waker().clone();
817 let mut rx = slot.state_rx.clone();
818 if let Ok(handle) = tokio::runtime::Handle::try_current() {
819 handle.spawn(async move {
820 let _ = rx.changed().await;
821 waker.wake();
822 });
823 } else {
824 waker.wake_by_ref();
825 }
826 return Poll::Pending;
827 }
828 BridgeState::Degraded(reason) => {
829 return Poll::Ready(Err(CamelError::ProcessorError(format!(
830 "JMS broker '{}' is degraded: {}",
831 self.broker_name, reason
832 ))));
833 }
834 BridgeState::Stopped => {
835 return Poll::Ready(Err(CamelError::ProcessorError(format!(
836 "JMS broker '{}' is stopped",
837 self.broker_name
838 ))));
839 }
840 }
841 }
842 Poll::Ready(Ok(()))
843 }
844
845 fn call(&mut self, exchange: Exchange) -> Self::Future {
846 let pool = Arc::clone(&self.pool);
847 let broker_name = self.broker_name.clone();
848 let endpoint_config = self.endpoint_config.clone();
849
850 Box::pin(async move {
851 let slot = pool.get_or_create_slot(&broker_name).await?;
852 let mut rx = slot.state_rx.clone();
853
854 loop {
855 let state = rx.borrow().clone();
856 match state {
857 BridgeState::Ready { channel } => {
858 let mut producer = JmsProducer::new(channel, endpoint_config.clone());
859 match producer.call(exchange).await {
860 Ok(done) => return Ok(done),
861 Err(first_err) if is_bridge_transport_error(&first_err) => {
862 warn!(
863 broker = %broker_name,
864 error = %first_err,
865 "JMS send transport error; refreshing channel (no automatic resend)"
866 );
867
868 if let Err(refresh_err) =
869 pool.refresh_slot_channel(&broker_name).await
870 {
871 warn!(
872 broker = %broker_name,
873 error = %refresh_err,
874 "JMS channel refresh failed; requesting bridge restart"
875 );
876 pool.restart_slot(&broker_name);
877 }
878
879 return Err(first_err);
884 }
885 Err(other_err) => return Err(other_err),
886 }
887 }
888 BridgeState::Degraded(reason) => {
889 return Err(CamelError::ProcessorError(format!(
890 "JMS broker '{}' is degraded: {}",
891 broker_name, reason
892 )));
893 }
894 BridgeState::Stopped => {
895 return Err(CamelError::ProcessorError(format!(
896 "JMS broker '{}' is stopped",
897 broker_name
898 )));
899 }
900 BridgeState::Starting | BridgeState::Restarting { .. } => {
901 if rx.changed().await.is_err() {
902 return Err(CamelError::ProcessorError(format!(
903 "JMS broker '{}' state channel closed",
904 broker_name
905 )));
906 }
907 }
908 }
909 }
910 })
911 }
912}
913
914fn redact_url(url: &str) -> String {
919 if let Some(pos) = url.find("://") {
921 let scheme = &url[..pos + 3]; let rest = &url[pos + 3..];
923 if let Some(at_pos) = rest.find('@') {
925 return format!("{}***@{}", scheme, &rest[at_pos + 1..]);
926 }
927 }
928 url.to_string()
929}
930
931pub fn is_bridge_transport_error(err: &CamelError) -> bool {
932 match err {
937 CamelError::ProcessorError(msg) => msg.starts_with(BRIDGE_TRANSPORT_ERROR_PREFIX),
938 _ => false,
939 }
940}
941
942#[cfg(test)]
945mod tests {
946 use camel_component_api::test_support::PanicRuntimeObservability;
947 fn test_rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
948 std::sync::Arc::new(PanicRuntimeObservability)
949 }
950 fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
951 std::sync::Arc::new(PanicRuntimeObservability)
952 }
953
954 use super::*;
955 use crate::config::{BrokerConfig, JmsPoolConfig};
956 use std::collections::HashMap;
957
958 #[test]
959 fn from_config_accepts_empty_brokers() {
960 let pool_config = JmsPoolConfig::default();
961 let result = JmsBridgePool::from_config(pool_config);
962 assert!(result.is_ok());
963 }
964
965 #[test]
966 fn resolve_broker_name_with_explicit_name() {
967 let pool = JmsBridgePool::from_config(JmsPoolConfig::single_broker(
968 "tcp://localhost:61616",
969 BrokerType::ActiveMq,
970 ))
971 .unwrap();
972 assert_eq!(
973 pool.resolve_broker_name(Some("default")).unwrap(),
974 "default"
975 );
976 }
977
978 #[test]
979 fn resolve_broker_name_default() {
980 let pool = JmsBridgePool::from_config(JmsPoolConfig::single_broker(
981 "tcp://localhost:61616",
982 BrokerType::ActiveMq,
983 ))
984 .unwrap();
985 assert_eq!(pool.resolve_broker_name(None).unwrap(), "default");
986 }
987
988 #[test]
989 fn resolve_broker_name_unknown_returns_error() {
990 let pool = JmsBridgePool::from_config(JmsPoolConfig::single_broker(
991 "tcp://localhost:61616",
992 BrokerType::ActiveMq,
993 ))
994 .unwrap();
995 let err = pool.resolve_broker_name(Some("unknown")).unwrap_err();
996 assert!(
997 err.to_string().contains("Unknown JMS broker 'unknown'"),
998 "got: {}",
999 err
1000 );
1001 }
1002
1003 #[test]
1004 fn resolve_broker_type_scheme_overrides() {
1005 let pool = JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1006 "tcp://localhost:61616",
1007 BrokerType::Generic,
1008 ))
1009 .unwrap();
1010 assert_eq!(
1011 pool.resolve_broker_type("activemq", "default"),
1012 BrokerType::ActiveMq
1013 );
1014 assert_eq!(
1015 pool.resolve_broker_type("artemis", "default"),
1016 BrokerType::Artemis
1017 );
1018 assert_eq!(
1019 pool.resolve_broker_type("jms", "default"),
1020 BrokerType::Generic
1021 );
1022 }
1023
1024 #[test]
1025 fn resolve_broker_type_activemq_scheme_overrides_artemis_config() {
1026 let pool = JmsBridgePool::from_config(JmsPoolConfig {
1027 brokers: HashMap::from([(
1028 "main".to_string(),
1029 BrokerConfig {
1030 broker_url: "tcp://localhost:61616".to_string(),
1031 broker_type: BrokerType::Artemis,
1032 username: None,
1033 password: None,
1034 },
1035 )]),
1036 ..JmsPoolConfig::default()
1037 })
1038 .unwrap();
1039 assert_eq!(
1040 pool.resolve_broker_type("activemq", "main"),
1041 BrokerType::ActiveMq
1042 );
1043 assert_eq!(pool.resolve_broker_type("jms", "main"), BrokerType::Artemis);
1044 }
1045
1046 #[test]
1047 fn create_endpoint_resolves_broker() {
1048 let pool = Arc::new(
1049 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1050 "tcp://localhost:61616",
1051 BrokerType::ActiveMq,
1052 ))
1053 .unwrap(),
1054 );
1055 let component = JmsComponent::with_scheme("jms", pool);
1056 let endpoint = component.create_endpoint(
1057 "jms:queue:orders",
1058 &camel_component_api::NoOpComponentContext,
1059 );
1060 assert!(endpoint.is_ok(), "got: {:?}", endpoint.err());
1061 }
1062
1063 #[test]
1064 fn create_endpoint_rejects_wrong_scheme() {
1065 let pool = Arc::new(
1066 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1067 "tcp://localhost:61616",
1068 BrokerType::ActiveMq,
1069 ))
1070 .unwrap(),
1071 );
1072 let component = JmsComponent::with_scheme("jms", pool);
1073 let err = component
1074 .create_endpoint("kafka:orders", &camel_component_api::NoOpComponentContext)
1075 .err()
1076 .unwrap();
1077 assert!(
1078 err.to_string()
1079 .contains("expected scheme 'jms', 'activemq', or 'artemis'"),
1080 "got: {}",
1081 err
1082 );
1083 }
1084
1085 #[test]
1086 fn create_endpoint_with_explicit_broker_param() {
1087 let pool = Arc::new(
1088 JmsBridgePool::from_config(JmsPoolConfig {
1089 brokers: HashMap::from([
1090 (
1091 "primary".to_string(),
1092 BrokerConfig {
1093 broker_url: "tcp://primary:61616".to_string(),
1094 broker_type: BrokerType::ActiveMq,
1095 username: None,
1096 password: None,
1097 },
1098 ),
1099 (
1100 "secondary".to_string(),
1101 BrokerConfig {
1102 broker_url: "tcp://secondary:61616".to_string(),
1103 broker_type: BrokerType::Artemis,
1104 username: None,
1105 password: None,
1106 },
1107 ),
1108 ]),
1109 ..JmsPoolConfig::default()
1110 })
1111 .unwrap(),
1112 );
1113 let component = JmsComponent::with_scheme("jms", Arc::clone(&pool));
1114 let endpoint = component.create_endpoint(
1115 "jms:queue:orders?broker=secondary",
1116 &camel_component_api::NoOpComponentContext,
1117 );
1118 assert!(endpoint.is_ok(), "got: {:?}", endpoint.err());
1119 }
1120
1121 #[tokio::test]
1122 #[allow(clippy::await_holding_lock)]
1123 async fn concurrent_get_or_create_slot_no_deadlock() {
1124 use tokio::time::timeout;
1125
1126 let _env_lock_guard = crate::BRIDGE_ENV_LOCK.lock().unwrap();
1128
1129 struct EnvGuard {
1130 key: &'static str,
1131 prev: Option<std::ffi::OsString>,
1132 }
1133 impl Drop for EnvGuard {
1134 fn drop(&mut self) {
1135 if let Some(v) = &self.prev {
1136 unsafe { std::env::set_var(self.key, v) };
1138 } else {
1139 unsafe { std::env::remove_var(self.key) };
1141 }
1142 }
1143 }
1144
1145 let env_key = "CAMEL_JMS_BRIDGE_BINARY_PATH";
1146 let _guard = EnvGuard {
1147 key: env_key,
1148 prev: std::env::var_os(env_key),
1149 };
1150 unsafe { std::env::set_var(env_key, "/bin/false") };
1152
1153 let pool = Arc::new(
1154 JmsBridgePool::from_config(JmsPoolConfig {
1155 brokers: HashMap::from([(
1156 "test".to_string(),
1157 BrokerConfig {
1158 broker_url: "tcp://localhost:61616".to_string(),
1159 broker_type: BrokerType::ActiveMq,
1160 username: None,
1161 password: None,
1162 },
1163 )]),
1164 bridge_start_timeout_ms: 100,
1165 ..JmsPoolConfig::default()
1166 })
1167 .unwrap(),
1168 );
1169
1170 let handles: Vec<_> = (0..5)
1171 .map(|_| {
1172 let pool = Arc::clone(&pool);
1173 tokio::spawn(async move {
1174 let _ = pool.get_or_create_slot("test").await;
1175 })
1176 })
1177 .collect();
1178
1179 let result = timeout(Duration::from_secs(5), async {
1180 for h in handles {
1181 let _ = h.await;
1182 }
1183 })
1184 .await;
1185
1186 assert!(result.is_ok(), "Concurrent get_or_create_slot deadlocked!");
1187 }
1188
1189 #[tokio::test]
1190 #[allow(clippy::await_holding_lock)]
1191 async fn lazy_producer_reports_degraded_when_bridge_start_fails() {
1192 use tower::Service;
1193
1194 let _env_lock_guard = crate::BRIDGE_ENV_LOCK.lock().unwrap();
1196
1197 struct EnvGuard {
1198 key: &'static str,
1199 prev: Option<std::ffi::OsString>,
1200 }
1201 impl Drop for EnvGuard {
1202 fn drop(&mut self) {
1203 if let Some(v) = &self.prev {
1204 unsafe { std::env::set_var(self.key, v) };
1206 } else {
1207 unsafe { std::env::remove_var(self.key) };
1209 }
1210 }
1211 }
1212
1213 let env_key = "CAMEL_JMS_BRIDGE_BINARY_PATH";
1214 let _guard = EnvGuard {
1215 key: env_key,
1216 prev: std::env::var_os(env_key),
1217 };
1218 unsafe { std::env::set_var(env_key, "/bin/false") };
1220
1221 let pool = Arc::new(
1222 JmsBridgePool::from_config(JmsPoolConfig {
1223 brokers: HashMap::from([(
1224 "default".to_string(),
1225 BrokerConfig {
1226 broker_url: "tcp://localhost:61616".to_string(),
1227 broker_type: BrokerType::ActiveMq,
1228 username: None,
1229 password: None,
1230 },
1231 )]),
1232 bridge_start_timeout_ms: 100,
1233 ..JmsPoolConfig::default()
1234 })
1235 .unwrap(),
1236 );
1237
1238 let component = JmsComponent::with_scheme("jms", pool);
1239 let endpoint = component
1240 .create_endpoint(
1241 "jms:queue:orders",
1242 &camel_component_api::NoOpComponentContext,
1243 )
1244 .unwrap();
1245 let mut producer = endpoint
1246 .create_producer(rt(), &camel_component_api::ProducerContext::default())
1247 .unwrap();
1248
1249 let mut exchange = Exchange::default();
1250 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1251
1252 let err = producer.call(exchange).await.unwrap_err();
1253 assert!(err.to_string().contains("is degraded"), "got: {}", err);
1254 }
1255
1256 #[tokio::test]
1260 async fn lazy_producer_requests_restart_when_refresh_unavailable() {
1261 use tokio::sync::watch;
1262 use tonic::transport::Endpoint as TonicEndpoint;
1263 use tower::Service;
1264
1265 let dead_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1268
1269 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1270 channel: dead_channel.clone(),
1271 });
1272
1273 let pool = Arc::new(
1274 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1275 "tcp://localhost:61616",
1276 BrokerType::ActiveMq,
1277 ))
1278 .unwrap(),
1279 );
1280
1281 let slot = Arc::new(BridgeSlot {
1283 name: "default".to_string(),
1284 broker_url: "tcp://localhost:61616".to_string(),
1285 broker_type: BrokerType::ActiveMq,
1286 credentials: None,
1287 state_rx: state_rx.clone(),
1288 state_tx: state_tx.clone(),
1289 process: Arc::new(tokio::sync::Mutex::new(None)),
1290 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1291 });
1292 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1293
1294 let endpoint_config =
1295 crate::config::JmsEndpointConfig::from_uri("jms:queue:test-retry").unwrap();
1296
1297 let mut producer = LazyJmsProducer {
1298 pool: Arc::clone(&pool),
1299 broker_name: "default".to_string(),
1300 endpoint_config,
1301 resolved_broker_type: BrokerType::ActiveMq,
1302 runtime: test_rt(),
1303 };
1304
1305 let mut exchange = Exchange::default();
1306 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1307
1308 let result = producer.call(exchange).await;
1310 assert!(result.is_err(), "expected send to fail");
1311
1312 let state_after = state_rx.borrow().clone();
1315 assert!(
1316 matches!(state_after, BridgeState::Restarting { .. }),
1317 "slot must enter Restarting when refresh is unavailable; got: {:?}",
1318 state_after
1319 );
1320 }
1321
1322 #[test]
1325 fn transport_error_detects_send_error() {
1326 let err = CamelError::ProcessorError(format!(
1327 "{}send error: connection refused",
1328 BRIDGE_TRANSPORT_ERROR_PREFIX
1329 ));
1330 assert!(
1331 is_bridge_transport_error(&err),
1332 "send error must be classified as transport"
1333 );
1334 }
1335
1336 #[test]
1337 fn transport_error_detects_subscribe_error() {
1338 let err = CamelError::ProcessorError(format!(
1339 "{}subscribe error: stream reset",
1340 BRIDGE_TRANSPORT_ERROR_PREFIX
1341 ));
1342 assert!(
1343 is_bridge_transport_error(&err),
1344 "subscribe error must be classified as transport"
1345 );
1346 }
1347
1348 #[test]
1349 fn transport_error_rejects_business_errors() {
1350 let err = CamelError::ProcessorError("JMS broker 'main' is degraded: timeout".to_string());
1351 assert!(
1352 !is_bridge_transport_error(&err),
1353 "degraded state error must NOT be transport"
1354 );
1355 }
1356
1357 #[test]
1358 fn transport_error_rejects_config_errors() {
1359 let err = CamelError::Config("bridge_start_timeout_ms must be > 0".to_string());
1360 assert!(
1361 !is_bridge_transport_error(&err),
1362 "config error must NOT be transport"
1363 );
1364 }
1365
1366 #[test]
1367 fn transport_error_prefix_is_used_by_producer_and_consumer() {
1368 assert!(
1371 BRIDGE_TRANSPORT_ERROR_PREFIX.starts_with("JMS gRPC "),
1372 "prefix must start with 'JMS gRPC '"
1373 );
1374 }
1375
1376 #[tokio::test]
1379 async fn pool_enforces_max_bridges_limit() {
1380 use tokio::sync::watch;
1381
1382 let pool = Arc::new(
1383 JmsBridgePool::from_config(JmsPoolConfig {
1384 brokers: HashMap::from([
1385 (
1386 "b1".to_string(),
1387 BrokerConfig {
1388 broker_url: "tcp://b1:61616".to_string(),
1389 broker_type: BrokerType::ActiveMq,
1390 username: None,
1391 password: None,
1392 },
1393 ),
1394 (
1395 "b2".to_string(),
1396 BrokerConfig {
1397 broker_url: "tcp://b2:61616".to_string(),
1398 broker_type: BrokerType::ActiveMq,
1399 username: None,
1400 password: None,
1401 },
1402 ),
1403 (
1404 "b3".to_string(),
1405 BrokerConfig {
1406 broker_url: "tcp://b3:61616".to_string(),
1407 broker_type: BrokerType::ActiveMq,
1408 username: None,
1409 password: None,
1410 },
1411 ),
1412 ]),
1413 max_bridges: 2,
1414 ..JmsPoolConfig::default()
1415 })
1416 .unwrap(),
1417 );
1418
1419 for name in &["b1", "b2"] {
1422 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1423 let slot = Arc::new(BridgeSlot {
1424 name: name.to_string(),
1425 broker_url: format!("tcp://{name}:61616"),
1426 broker_type: BrokerType::ActiveMq,
1427 credentials: None,
1428 state_rx,
1429 state_tx,
1430 process: Arc::new(tokio::sync::Mutex::new(None)),
1431 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1432 });
1433 pool.slots.insert(name.to_string(), slot);
1434 }
1435
1436 let err = pool.get_or_create_slot("b3").await.unwrap_err();
1438 assert!(
1439 err.to_string().contains("max_bridges"),
1440 "expected max_bridges error, got: {}",
1441 err
1442 );
1443 }
1444
1445 #[tokio::test]
1446 async fn pool_allows_slot_when_below_max_bridges() {
1447 use tokio::sync::watch;
1448
1449 let pool = Arc::new(
1450 JmsBridgePool::from_config(JmsPoolConfig {
1451 brokers: HashMap::from([(
1452 "b1".to_string(),
1453 BrokerConfig {
1454 broker_url: "tcp://b1:61616".to_string(),
1455 broker_type: BrokerType::ActiveMq,
1456 username: None,
1457 password: None,
1458 },
1459 )]),
1460 max_bridges: 2,
1461 bridge_start_timeout_ms: 100,
1462 ..JmsPoolConfig::default()
1463 })
1464 .unwrap(),
1465 );
1466
1467 let (state_tx, state_rx) = watch::channel(BridgeState::Degraded("test".to_string()));
1469 let slot = Arc::new(BridgeSlot {
1470 name: "b1".to_string(),
1471 broker_url: "tcp://b1:61616".to_string(),
1472 broker_type: BrokerType::ActiveMq,
1473 credentials: None,
1474 state_rx,
1475 state_tx,
1476 process: Arc::new(tokio::sync::Mutex::new(None)),
1477 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1478 });
1479 pool.slots.insert("b1".to_string(), slot);
1480
1481 let result = pool.get_or_create_slot("b1").await;
1484 assert!(result.is_ok(), "existing slot must be returned");
1485 }
1486
1487 #[tokio::test]
1490 async fn poll_ready_returns_pending_when_starting() {
1491 use tokio::sync::watch;
1492 use tower::Service;
1493
1494 let pool = Arc::new(
1495 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1496 "tcp://localhost:61616",
1497 BrokerType::ActiveMq,
1498 ))
1499 .unwrap(),
1500 );
1501
1502 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1503 let slot = Arc::new(BridgeSlot {
1504 name: "default".to_string(),
1505 broker_url: "tcp://localhost:61616".to_string(),
1506 broker_type: BrokerType::ActiveMq,
1507 credentials: None,
1508 state_rx,
1509 state_tx,
1510 process: Arc::new(tokio::sync::Mutex::new(None)),
1511 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1512 });
1513 pool.slots.insert("default".to_string(), slot);
1514
1515 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1516 let mut producer = LazyJmsProducer {
1517 pool: Arc::clone(&pool),
1518 broker_name: "default".to_string(),
1519 endpoint_config,
1520 resolved_broker_type: BrokerType::ActiveMq,
1521 runtime: test_rt(),
1522 };
1523
1524 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1525 assert!(
1526 matches!(result, Poll::Pending),
1527 "poll_ready must be Pending when Starting; got: {:?}",
1528 result
1529 );
1530 }
1531
1532 #[tokio::test]
1533 async fn poll_ready_returns_error_when_degraded() {
1534 use tokio::sync::watch;
1535 use tower::Service;
1536
1537 let pool = Arc::new(
1538 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1539 "tcp://localhost:61616",
1540 BrokerType::ActiveMq,
1541 ))
1542 .unwrap(),
1543 );
1544
1545 let (state_tx, state_rx) =
1546 watch::channel(BridgeState::Degraded("health check failed".to_string()));
1547 let slot = Arc::new(BridgeSlot {
1548 name: "default".to_string(),
1549 broker_url: "tcp://localhost:61616".to_string(),
1550 broker_type: BrokerType::ActiveMq,
1551 credentials: None,
1552 state_rx,
1553 state_tx,
1554 process: Arc::new(tokio::sync::Mutex::new(None)),
1555 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1556 });
1557 pool.slots.insert("default".to_string(), slot);
1558
1559 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1560 let mut producer = LazyJmsProducer {
1561 pool: Arc::clone(&pool),
1562 broker_name: "default".to_string(),
1563 endpoint_config,
1564 resolved_broker_type: BrokerType::ActiveMq,
1565 runtime: test_rt(),
1566 };
1567
1568 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1569 assert!(
1570 matches!(result, Poll::Ready(Err(_))),
1571 "poll_ready must be Err when Degraded; got: {:?}",
1572 result
1573 );
1574 let err_msg = match result {
1575 Poll::Ready(Err(e)) => e.to_string(),
1576 _ => unreachable!(),
1577 };
1578 assert!(
1579 err_msg.contains("degraded"),
1580 "error must mention degraded: {}",
1581 err_msg
1582 );
1583 }
1584
1585 #[tokio::test]
1586 async fn poll_ready_returns_error_when_stopped() {
1587 use tokio::sync::watch;
1588 use tower::Service;
1589
1590 let pool = Arc::new(
1591 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1592 "tcp://localhost:61616",
1593 BrokerType::ActiveMq,
1594 ))
1595 .unwrap(),
1596 );
1597
1598 let (state_tx, state_rx) = watch::channel(BridgeState::Stopped);
1599 let slot = Arc::new(BridgeSlot {
1600 name: "default".to_string(),
1601 broker_url: "tcp://localhost:61616".to_string(),
1602 broker_type: BrokerType::ActiveMq,
1603 credentials: None,
1604 state_rx,
1605 state_tx,
1606 process: Arc::new(tokio::sync::Mutex::new(None)),
1607 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1608 });
1609 pool.slots.insert("default".to_string(), slot);
1610
1611 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1612 let mut producer = LazyJmsProducer {
1613 pool: Arc::clone(&pool),
1614 broker_name: "default".to_string(),
1615 endpoint_config,
1616 resolved_broker_type: BrokerType::ActiveMq,
1617 runtime: test_rt(),
1618 };
1619
1620 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1621 assert!(
1622 matches!(result, Poll::Ready(Err(_))),
1623 "poll_ready must be Err when Stopped; got: {:?}",
1624 result
1625 );
1626 }
1627
1628 #[tokio::test]
1629 async fn poll_ready_returns_ready_when_slot_ready() {
1630 use tokio::sync::watch;
1631 use tonic::transport::Endpoint as TonicEndpoint;
1632 use tower::Service;
1633
1634 let pool = Arc::new(
1635 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1636 "tcp://localhost:61616",
1637 BrokerType::ActiveMq,
1638 ))
1639 .unwrap(),
1640 );
1641
1642 let lazy_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1643 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1644 channel: lazy_channel,
1645 });
1646 let slot = Arc::new(BridgeSlot {
1647 name: "default".to_string(),
1648 broker_url: "tcp://localhost:61616".to_string(),
1649 broker_type: BrokerType::ActiveMq,
1650 credentials: None,
1651 state_rx,
1652 state_tx,
1653 process: Arc::new(tokio::sync::Mutex::new(None)),
1654 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1655 });
1656 pool.slots.insert("default".to_string(), slot);
1657
1658 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1659 let mut producer = LazyJmsProducer {
1660 pool: Arc::clone(&pool),
1661 broker_name: "default".to_string(),
1662 endpoint_config,
1663 resolved_broker_type: BrokerType::ActiveMq,
1664 runtime: test_rt(),
1665 };
1666
1667 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1668 assert!(
1669 matches!(result, Poll::Ready(Ok(()))),
1670 "poll_ready must be Ready(Ok) when bridge is Ready; got: {:?}",
1671 result
1672 );
1673 }
1674
1675 #[tokio::test]
1676 async fn poll_ready_returns_ready_when_no_slot_exists() {
1677 use tower::Service;
1678
1679 let pool = Arc::new(
1680 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1681 "tcp://localhost:61616",
1682 BrokerType::ActiveMq,
1683 ))
1684 .unwrap(),
1685 );
1686
1687 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1688 let mut producer = LazyJmsProducer {
1689 pool: Arc::clone(&pool),
1690 broker_name: "default".to_string(),
1691 endpoint_config,
1692 resolved_broker_type: BrokerType::ActiveMq,
1693 runtime: test_rt(),
1694 };
1695
1696 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1698 assert!(
1699 matches!(result, Poll::Ready(Ok(()))),
1700 "poll_ready must be Ready(Ok) when no slot exists; got: {:?}",
1701 result
1702 );
1703 }
1704
1705 #[tokio::test]
1708 async fn pool_shutdown_awaits_health_monitor() {
1709 use tokio::sync::watch;
1710
1711 let pool = Arc::new(
1712 JmsBridgePool::from_config(JmsPoolConfig {
1713 brokers: HashMap::from([(
1714 "default".to_string(),
1715 BrokerConfig {
1716 broker_url: "tcp://localhost:61616".to_string(),
1717 broker_type: BrokerType::ActiveMq,
1718 username: None,
1719 password: None,
1720 },
1721 )]),
1722 health_check_interval_ms: 100,
1723 ..JmsPoolConfig::default()
1724 })
1725 .unwrap(),
1726 );
1727
1728 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1730 channel: tonic::transport::Endpoint::from_static("http://127.0.0.1:1").connect_lazy(),
1731 });
1732 let monitor_handle_ref: Arc<tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>> =
1733 Arc::new(tokio::sync::Mutex::new(None));
1734
1735 let state_rx_clone = state_rx.clone();
1737 let handle = tokio::spawn(async move {
1738 loop {
1739 if matches!(*state_rx_clone.borrow(), BridgeState::Stopped) {
1740 break;
1741 }
1742 tokio::time::sleep(Duration::from_millis(50)).await;
1743 }
1744 });
1745 *monitor_handle_ref.lock().await = Some(handle);
1746
1747 let slot = Arc::new(BridgeSlot {
1748 name: "default".to_string(),
1749 broker_url: "tcp://localhost:61616".to_string(),
1750 broker_type: BrokerType::ActiveMq,
1751 credentials: None,
1752 state_rx,
1753 state_tx,
1754 process: Arc::new(tokio::sync::Mutex::new(None)),
1755 health_monitor_handle: monitor_handle_ref,
1756 });
1757 pool.slots.insert("default".to_string(), slot);
1758
1759 let result = pool.shutdown().await;
1761 let _ = result;
1763 }
1764
1765 #[tokio::test]
1766 async fn health_monitor_handle_stored_after_spawn() {
1767 use tokio::sync::watch;
1768
1769 let pool = Arc::new(
1770 JmsBridgePool::from_config(JmsPoolConfig {
1771 brokers: HashMap::from([(
1772 "default".to_string(),
1773 BrokerConfig {
1774 broker_url: "tcp://localhost:61616".to_string(),
1775 broker_type: BrokerType::ActiveMq,
1776 username: None,
1777 password: None,
1778 },
1779 )]),
1780 health_check_interval_ms: 100,
1781 bridge_start_timeout_ms: 100,
1782 ..JmsPoolConfig::default()
1783 })
1784 .unwrap(),
1785 );
1786
1787 let (state_tx, state_rx) = watch::channel(BridgeState::Stopped);
1788 let slot = Arc::new(BridgeSlot {
1789 name: "default".to_string(),
1790 broker_url: "tcp://localhost:61616".to_string(),
1791 broker_type: BrokerType::ActiveMq,
1792 credentials: None,
1793 state_rx,
1794 state_tx,
1795 process: Arc::new(tokio::sync::Mutex::new(None)),
1796 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1797 });
1798 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1799
1800 pool.spawn_health_monitor(Arc::clone(&slot)).await;
1801
1802 tokio::time::sleep(Duration::from_millis(50)).await;
1803 let guard = slot.health_monitor_handle.lock().await;
1804 assert!(
1805 guard.is_some(),
1806 "health monitor handle must be stored after spawn_health_monitor"
1807 );
1808 }
1809
1810 #[test]
1813 fn redact_url_strips_userinfo_with_password() {
1814 assert_eq!(
1815 redact_url("tcp://admin:s3cret@broker:61616"),
1816 "tcp://***@broker:61616"
1817 );
1818 }
1819
1820 #[test]
1821 fn redact_url_strips_userinfo_without_password() {
1822 assert_eq!(
1823 redact_url("tcp://admin@broker:61616"),
1824 "tcp://***@broker:61616"
1825 );
1826 }
1827
1828 #[test]
1829 fn redact_url_passes_clean_url_unchanged() {
1830 assert_eq!(redact_url("tcp://localhost:61616"), "tcp://localhost:61616");
1831 }
1832
1833 #[test]
1834 fn redact_url_handles_ssl_scheme() {
1835 assert_eq!(
1836 redact_url("ssl://user:pass@secure-broker:61617"),
1837 "ssl://***@secure-broker:61617"
1838 );
1839 }
1840
1841 #[tokio::test]
1844 async fn concurrent_slot_creation_respects_max_bridges() {
1845 let pool = Arc::new(
1846 JmsBridgePool::from_config(JmsPoolConfig {
1847 brokers: HashMap::from([
1848 (
1849 "b1".to_string(),
1850 BrokerConfig {
1851 broker_url: "tcp://b1:61616".to_string(),
1852 broker_type: BrokerType::ActiveMq,
1853 username: None,
1854 password: None,
1855 },
1856 ),
1857 (
1858 "b2".to_string(),
1859 BrokerConfig {
1860 broker_url: "tcp://b2:61616".to_string(),
1861 broker_type: BrokerType::ActiveMq,
1862 username: None,
1863 password: None,
1864 },
1865 ),
1866 (
1867 "b3".to_string(),
1868 BrokerConfig {
1869 broker_url: "tcp://b3:61616".to_string(),
1870 broker_type: BrokerType::ActiveMq,
1871 username: None,
1872 password: None,
1873 },
1874 ),
1875 ]),
1876 max_bridges: 2,
1877 bridge_start_timeout_ms: 100,
1878 ..JmsPoolConfig::default()
1879 })
1880 .unwrap(),
1881 );
1882
1883 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1884 for name in &["b1", "b2"] {
1885 let slot = Arc::new(BridgeSlot {
1886 name: name.to_string(),
1887 broker_url: format!("tcp://{name}:61616"),
1888 broker_type: BrokerType::ActiveMq,
1889 credentials: None,
1890 state_rx: state_rx.clone(),
1891 state_tx: state_tx.clone(),
1892 process: Arc::new(tokio::sync::Mutex::new(None)),
1893 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1894 });
1895 pool.slots.insert(name.to_string(), slot);
1896 }
1897
1898 assert_eq!(pool.slots.len(), 2);
1899
1900 let guard = pool.bridge_create_lock.lock().await;
1901 let total_count = pool.slots.len();
1902 assert!(total_count >= pool.max_bridges);
1903 let result = if total_count >= pool.max_bridges {
1904 Err(CamelError::Config(format!(
1905 "JMS bridge limit reached: {total_count} bridge(s) >= max_bridges ({})",
1906 pool.max_bridges
1907 )))
1908 } else {
1909 Ok(())
1910 };
1911 drop(guard);
1912
1913 assert!(result.is_err(), "3rd broker should be rejected");
1914 let err_msg = result.unwrap_err().to_string();
1915 assert!(
1916 err_msg.contains("max_bridges"),
1917 "error must mention max_bridges, got: {err_msg}"
1918 );
1919 }
1920
1921 #[tokio::test]
1924 async fn transport_error_refreshes_channel_but_does_not_resend() {
1925 use tokio::sync::watch;
1926 use tonic::transport::Endpoint as TonicEndpoint;
1927 use tower::Service;
1928
1929 let dead_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1930
1931 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1932 channel: dead_channel.clone(),
1933 });
1934
1935 let pool = Arc::new(
1936 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1937 "tcp://localhost:61616",
1938 BrokerType::ActiveMq,
1939 ))
1940 .unwrap(),
1941 );
1942
1943 let slot = Arc::new(BridgeSlot {
1944 name: "default".to_string(),
1945 broker_url: "tcp://localhost:61616".to_string(),
1946 broker_type: BrokerType::ActiveMq,
1947 credentials: None,
1948 state_rx: state_rx.clone(),
1949 state_tx: state_tx.clone(),
1950 process: Arc::new(tokio::sync::Mutex::new(None)),
1951 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1952 });
1953 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1954
1955 let endpoint_config =
1956 crate::config::JmsEndpointConfig::from_uri("jms:queue:test-no-resend").unwrap();
1957
1958 let mut producer = LazyJmsProducer {
1959 pool: Arc::clone(&pool),
1960 broker_name: "default".to_string(),
1961 endpoint_config,
1962 resolved_broker_type: BrokerType::ActiveMq,
1963 runtime: test_rt(),
1964 };
1965
1966 let mut exchange = Exchange::default();
1967 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1968
1969 let result = producer.call(exchange).await;
1970 assert!(result.is_err(), "expected send to fail");
1971
1972 let state_after = state_rx.borrow().clone();
1973 assert!(
1974 matches!(state_after, BridgeState::Restarting { .. }),
1975 "slot must enter Restarting; got: {:?}",
1976 state_after
1977 );
1978
1979 let err_msg = result.unwrap_err().to_string();
1980 assert!(
1981 err_msg.contains(BRIDGE_TRANSPORT_ERROR_PREFIX),
1982 "error must be original transport error, got: {}",
1983 err_msg
1984 );
1985 }
1986
1987 #[tokio::test]
1990 async fn test_jms_bridge_pool_drop_cleans_up_slots() {
1991 use std::sync::atomic::{AtomicBool, Ordering};
1992 use tokio::sync::watch;
1993
1994 let pool = Arc::new(
1995 JmsBridgePool::from_config(JmsPoolConfig {
1996 brokers: HashMap::from([(
1997 "default".to_string(),
1998 BrokerConfig {
1999 broker_url: "tcp://localhost:61616".to_string(),
2000 broker_type: BrokerType::ActiveMq,
2001 username: None,
2002 password: None,
2003 },
2004 )]),
2005 health_check_interval_ms: 50,
2006 ..JmsPoolConfig::default()
2007 })
2008 .unwrap(),
2009 );
2010
2011 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
2013 channel: tonic::transport::Endpoint::from_static("http://127.0.0.1:1").connect_lazy(),
2014 });
2015
2016 let monitor_exited = Arc::new(AtomicBool::new(false));
2017 let exited = monitor_exited.clone();
2018 let rx = state_rx.clone();
2019 let handle = tokio::spawn(async move {
2020 loop {
2021 if matches!(*rx.borrow(), BridgeState::Stopped) {
2022 break;
2023 }
2024 tokio::time::sleep(Duration::from_millis(10)).await;
2025 }
2026 exited.store(true, Ordering::SeqCst);
2027 });
2028
2029 let monitor_handle_ref: Arc<tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>> =
2030 Arc::new(tokio::sync::Mutex::new(None));
2031 *monitor_handle_ref.lock().await = Some(handle);
2032
2033 let slot = Arc::new(BridgeSlot {
2034 name: "default".to_string(),
2035 broker_url: "tcp://localhost:61616".to_string(),
2036 broker_type: BrokerType::ActiveMq,
2037 credentials: None,
2038 state_rx,
2039 state_tx,
2040 process: Arc::new(tokio::sync::Mutex::new(None)),
2041 health_monitor_handle: monitor_handle_ref,
2042 });
2043 pool.slots.insert("default".to_string(), slot);
2044
2045 drop(pool);
2047
2048 tokio::time::sleep(Duration::from_millis(200)).await;
2050
2051 assert!(
2052 monitor_exited.load(Ordering::SeqCst),
2053 "health monitor should have exited after pool drop (Stopped signal sent by Drop)"
2054 );
2055 }
2056}