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 async fn concurrent_get_or_create_slot_no_deadlock() {
1123 use tokio::time::timeout;
1124
1125 struct EnvGuard {
1126 key: &'static str,
1127 prev: Option<std::ffi::OsString>,
1128 }
1129 impl Drop for EnvGuard {
1130 fn drop(&mut self) {
1131 if let Some(v) = &self.prev {
1132 unsafe { std::env::set_var(self.key, v) };
1134 } else {
1135 unsafe { std::env::remove_var(self.key) };
1137 }
1138 }
1139 }
1140
1141 let env_key = "CAMEL_JMS_BRIDGE_BINARY_PATH";
1142 let _guard = EnvGuard {
1143 key: env_key,
1144 prev: std::env::var_os(env_key),
1145 };
1146 unsafe { std::env::set_var(env_key, "/bin/false") };
1148
1149 let pool = Arc::new(
1150 JmsBridgePool::from_config(JmsPoolConfig {
1151 brokers: HashMap::from([(
1152 "test".to_string(),
1153 BrokerConfig {
1154 broker_url: "tcp://localhost:61616".to_string(),
1155 broker_type: BrokerType::ActiveMq,
1156 username: None,
1157 password: None,
1158 },
1159 )]),
1160 bridge_start_timeout_ms: 100,
1161 ..JmsPoolConfig::default()
1162 })
1163 .unwrap(),
1164 );
1165
1166 let handles: Vec<_> = (0..5)
1167 .map(|_| {
1168 let pool = Arc::clone(&pool);
1169 tokio::spawn(async move {
1170 let _ = pool.get_or_create_slot("test").await;
1171 })
1172 })
1173 .collect();
1174
1175 let result = timeout(Duration::from_secs(5), async {
1176 for h in handles {
1177 let _ = h.await;
1178 }
1179 })
1180 .await;
1181
1182 assert!(result.is_ok(), "Concurrent get_or_create_slot deadlocked!");
1183 }
1184
1185 #[tokio::test]
1186 async fn lazy_producer_reports_degraded_when_bridge_start_fails() {
1187 use tower::Service;
1188
1189 struct EnvGuard {
1190 key: &'static str,
1191 prev: Option<std::ffi::OsString>,
1192 }
1193 impl Drop for EnvGuard {
1194 fn drop(&mut self) {
1195 if let Some(v) = &self.prev {
1196 unsafe { std::env::set_var(self.key, v) };
1198 } else {
1199 unsafe { std::env::remove_var(self.key) };
1201 }
1202 }
1203 }
1204
1205 let env_key = "CAMEL_JMS_BRIDGE_BINARY_PATH";
1206 let _guard = EnvGuard {
1207 key: env_key,
1208 prev: std::env::var_os(env_key),
1209 };
1210 unsafe { std::env::set_var(env_key, "/bin/false") };
1212
1213 let pool = Arc::new(
1214 JmsBridgePool::from_config(JmsPoolConfig {
1215 brokers: HashMap::from([(
1216 "default".to_string(),
1217 BrokerConfig {
1218 broker_url: "tcp://localhost:61616".to_string(),
1219 broker_type: BrokerType::ActiveMq,
1220 username: None,
1221 password: None,
1222 },
1223 )]),
1224 bridge_start_timeout_ms: 100,
1225 ..JmsPoolConfig::default()
1226 })
1227 .unwrap(),
1228 );
1229
1230 let component = JmsComponent::with_scheme("jms", pool);
1231 let endpoint = component
1232 .create_endpoint(
1233 "jms:queue:orders",
1234 &camel_component_api::NoOpComponentContext,
1235 )
1236 .unwrap();
1237 let mut producer = endpoint
1238 .create_producer(rt(), &camel_component_api::ProducerContext::default())
1239 .unwrap();
1240
1241 let mut exchange = Exchange::default();
1242 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1243
1244 let err = producer.call(exchange).await.unwrap_err();
1245 assert!(err.to_string().contains("is degraded"), "got: {}", err);
1246 }
1247
1248 #[tokio::test]
1252 async fn lazy_producer_requests_restart_when_refresh_unavailable() {
1253 use tokio::sync::watch;
1254 use tonic::transport::Endpoint as TonicEndpoint;
1255 use tower::Service;
1256
1257 let dead_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1260
1261 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1262 channel: dead_channel.clone(),
1263 });
1264
1265 let pool = Arc::new(
1266 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1267 "tcp://localhost:61616",
1268 BrokerType::ActiveMq,
1269 ))
1270 .unwrap(),
1271 );
1272
1273 let slot = Arc::new(BridgeSlot {
1275 name: "default".to_string(),
1276 broker_url: "tcp://localhost:61616".to_string(),
1277 broker_type: BrokerType::ActiveMq,
1278 credentials: None,
1279 state_rx: state_rx.clone(),
1280 state_tx: state_tx.clone(),
1281 process: Arc::new(tokio::sync::Mutex::new(None)),
1282 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1283 });
1284 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1285
1286 let endpoint_config =
1287 crate::config::JmsEndpointConfig::from_uri("jms:queue:test-retry").unwrap();
1288
1289 let mut producer = LazyJmsProducer {
1290 pool: Arc::clone(&pool),
1291 broker_name: "default".to_string(),
1292 endpoint_config,
1293 resolved_broker_type: BrokerType::ActiveMq,
1294 runtime: test_rt(),
1295 };
1296
1297 let mut exchange = Exchange::default();
1298 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1299
1300 let result = producer.call(exchange).await;
1302 assert!(result.is_err(), "expected send to fail");
1303
1304 let state_after = state_rx.borrow().clone();
1307 assert!(
1308 matches!(state_after, BridgeState::Restarting { .. }),
1309 "slot must enter Restarting when refresh is unavailable; got: {:?}",
1310 state_after
1311 );
1312 }
1313
1314 #[test]
1317 fn transport_error_detects_send_error() {
1318 let err = CamelError::ProcessorError(format!(
1319 "{}send error: connection refused",
1320 BRIDGE_TRANSPORT_ERROR_PREFIX
1321 ));
1322 assert!(
1323 is_bridge_transport_error(&err),
1324 "send error must be classified as transport"
1325 );
1326 }
1327
1328 #[test]
1329 fn transport_error_detects_subscribe_error() {
1330 let err = CamelError::ProcessorError(format!(
1331 "{}subscribe error: stream reset",
1332 BRIDGE_TRANSPORT_ERROR_PREFIX
1333 ));
1334 assert!(
1335 is_bridge_transport_error(&err),
1336 "subscribe error must be classified as transport"
1337 );
1338 }
1339
1340 #[test]
1341 fn transport_error_rejects_business_errors() {
1342 let err = CamelError::ProcessorError("JMS broker 'main' is degraded: timeout".to_string());
1343 assert!(
1344 !is_bridge_transport_error(&err),
1345 "degraded state error must NOT be transport"
1346 );
1347 }
1348
1349 #[test]
1350 fn transport_error_rejects_config_errors() {
1351 let err = CamelError::Config("bridge_start_timeout_ms must be > 0".to_string());
1352 assert!(
1353 !is_bridge_transport_error(&err),
1354 "config error must NOT be transport"
1355 );
1356 }
1357
1358 #[test]
1359 fn transport_error_prefix_is_used_by_producer_and_consumer() {
1360 assert!(
1363 BRIDGE_TRANSPORT_ERROR_PREFIX.starts_with("JMS gRPC "),
1364 "prefix must start with 'JMS gRPC '"
1365 );
1366 }
1367
1368 #[tokio::test]
1371 async fn pool_enforces_max_bridges_limit() {
1372 use tokio::sync::watch;
1373
1374 let pool = Arc::new(
1375 JmsBridgePool::from_config(JmsPoolConfig {
1376 brokers: HashMap::from([
1377 (
1378 "b1".to_string(),
1379 BrokerConfig {
1380 broker_url: "tcp://b1:61616".to_string(),
1381 broker_type: BrokerType::ActiveMq,
1382 username: None,
1383 password: None,
1384 },
1385 ),
1386 (
1387 "b2".to_string(),
1388 BrokerConfig {
1389 broker_url: "tcp://b2:61616".to_string(),
1390 broker_type: BrokerType::ActiveMq,
1391 username: None,
1392 password: None,
1393 },
1394 ),
1395 (
1396 "b3".to_string(),
1397 BrokerConfig {
1398 broker_url: "tcp://b3:61616".to_string(),
1399 broker_type: BrokerType::ActiveMq,
1400 username: None,
1401 password: None,
1402 },
1403 ),
1404 ]),
1405 max_bridges: 2,
1406 ..JmsPoolConfig::default()
1407 })
1408 .unwrap(),
1409 );
1410
1411 for name in &["b1", "b2"] {
1414 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1415 let slot = Arc::new(BridgeSlot {
1416 name: name.to_string(),
1417 broker_url: format!("tcp://{name}:61616"),
1418 broker_type: BrokerType::ActiveMq,
1419 credentials: None,
1420 state_rx,
1421 state_tx,
1422 process: Arc::new(tokio::sync::Mutex::new(None)),
1423 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1424 });
1425 pool.slots.insert(name.to_string(), slot);
1426 }
1427
1428 let err = pool.get_or_create_slot("b3").await.unwrap_err();
1430 assert!(
1431 err.to_string().contains("max_bridges"),
1432 "expected max_bridges error, got: {}",
1433 err
1434 );
1435 }
1436
1437 #[tokio::test]
1438 async fn pool_allows_slot_when_below_max_bridges() {
1439 use tokio::sync::watch;
1440
1441 let pool = Arc::new(
1442 JmsBridgePool::from_config(JmsPoolConfig {
1443 brokers: HashMap::from([(
1444 "b1".to_string(),
1445 BrokerConfig {
1446 broker_url: "tcp://b1:61616".to_string(),
1447 broker_type: BrokerType::ActiveMq,
1448 username: None,
1449 password: None,
1450 },
1451 )]),
1452 max_bridges: 2,
1453 bridge_start_timeout_ms: 100,
1454 ..JmsPoolConfig::default()
1455 })
1456 .unwrap(),
1457 );
1458
1459 let (state_tx, state_rx) = watch::channel(BridgeState::Degraded("test".to_string()));
1461 let slot = Arc::new(BridgeSlot {
1462 name: "b1".to_string(),
1463 broker_url: "tcp://b1:61616".to_string(),
1464 broker_type: BrokerType::ActiveMq,
1465 credentials: None,
1466 state_rx,
1467 state_tx,
1468 process: Arc::new(tokio::sync::Mutex::new(None)),
1469 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1470 });
1471 pool.slots.insert("b1".to_string(), slot);
1472
1473 let result = pool.get_or_create_slot("b1").await;
1476 assert!(result.is_ok(), "existing slot must be returned");
1477 }
1478
1479 #[tokio::test]
1482 async fn poll_ready_returns_pending_when_starting() {
1483 use tokio::sync::watch;
1484 use tower::Service;
1485
1486 let pool = Arc::new(
1487 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1488 "tcp://localhost:61616",
1489 BrokerType::ActiveMq,
1490 ))
1491 .unwrap(),
1492 );
1493
1494 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1495 let slot = Arc::new(BridgeSlot {
1496 name: "default".to_string(),
1497 broker_url: "tcp://localhost:61616".to_string(),
1498 broker_type: BrokerType::ActiveMq,
1499 credentials: None,
1500 state_rx,
1501 state_tx,
1502 process: Arc::new(tokio::sync::Mutex::new(None)),
1503 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1504 });
1505 pool.slots.insert("default".to_string(), slot);
1506
1507 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1508 let mut producer = LazyJmsProducer {
1509 pool: Arc::clone(&pool),
1510 broker_name: "default".to_string(),
1511 endpoint_config,
1512 resolved_broker_type: BrokerType::ActiveMq,
1513 runtime: test_rt(),
1514 };
1515
1516 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1517 assert!(
1518 matches!(result, Poll::Pending),
1519 "poll_ready must be Pending when Starting; got: {:?}",
1520 result
1521 );
1522 }
1523
1524 #[tokio::test]
1525 async fn poll_ready_returns_error_when_degraded() {
1526 use tokio::sync::watch;
1527 use tower::Service;
1528
1529 let pool = Arc::new(
1530 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1531 "tcp://localhost:61616",
1532 BrokerType::ActiveMq,
1533 ))
1534 .unwrap(),
1535 );
1536
1537 let (state_tx, state_rx) =
1538 watch::channel(BridgeState::Degraded("health check failed".to_string()));
1539 let slot = Arc::new(BridgeSlot {
1540 name: "default".to_string(),
1541 broker_url: "tcp://localhost:61616".to_string(),
1542 broker_type: BrokerType::ActiveMq,
1543 credentials: None,
1544 state_rx,
1545 state_tx,
1546 process: Arc::new(tokio::sync::Mutex::new(None)),
1547 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1548 });
1549 pool.slots.insert("default".to_string(), slot);
1550
1551 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1552 let mut producer = LazyJmsProducer {
1553 pool: Arc::clone(&pool),
1554 broker_name: "default".to_string(),
1555 endpoint_config,
1556 resolved_broker_type: BrokerType::ActiveMq,
1557 runtime: test_rt(),
1558 };
1559
1560 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1561 assert!(
1562 matches!(result, Poll::Ready(Err(_))),
1563 "poll_ready must be Err when Degraded; got: {:?}",
1564 result
1565 );
1566 let err_msg = match result {
1567 Poll::Ready(Err(e)) => e.to_string(),
1568 _ => unreachable!(),
1569 };
1570 assert!(
1571 err_msg.contains("degraded"),
1572 "error must mention degraded: {}",
1573 err_msg
1574 );
1575 }
1576
1577 #[tokio::test]
1578 async fn poll_ready_returns_error_when_stopped() {
1579 use tokio::sync::watch;
1580 use tower::Service;
1581
1582 let pool = Arc::new(
1583 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1584 "tcp://localhost:61616",
1585 BrokerType::ActiveMq,
1586 ))
1587 .unwrap(),
1588 );
1589
1590 let (state_tx, state_rx) = watch::channel(BridgeState::Stopped);
1591 let slot = Arc::new(BridgeSlot {
1592 name: "default".to_string(),
1593 broker_url: "tcp://localhost:61616".to_string(),
1594 broker_type: BrokerType::ActiveMq,
1595 credentials: None,
1596 state_rx,
1597 state_tx,
1598 process: Arc::new(tokio::sync::Mutex::new(None)),
1599 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1600 });
1601 pool.slots.insert("default".to_string(), slot);
1602
1603 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1604 let mut producer = LazyJmsProducer {
1605 pool: Arc::clone(&pool),
1606 broker_name: "default".to_string(),
1607 endpoint_config,
1608 resolved_broker_type: BrokerType::ActiveMq,
1609 runtime: test_rt(),
1610 };
1611
1612 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1613 assert!(
1614 matches!(result, Poll::Ready(Err(_))),
1615 "poll_ready must be Err when Stopped; got: {:?}",
1616 result
1617 );
1618 }
1619
1620 #[tokio::test]
1621 async fn poll_ready_returns_ready_when_slot_ready() {
1622 use tokio::sync::watch;
1623 use tonic::transport::Endpoint as TonicEndpoint;
1624 use tower::Service;
1625
1626 let pool = Arc::new(
1627 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1628 "tcp://localhost:61616",
1629 BrokerType::ActiveMq,
1630 ))
1631 .unwrap(),
1632 );
1633
1634 let lazy_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1635 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1636 channel: lazy_channel,
1637 });
1638 let slot = Arc::new(BridgeSlot {
1639 name: "default".to_string(),
1640 broker_url: "tcp://localhost:61616".to_string(),
1641 broker_type: BrokerType::ActiveMq,
1642 credentials: None,
1643 state_rx,
1644 state_tx,
1645 process: Arc::new(tokio::sync::Mutex::new(None)),
1646 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1647 });
1648 pool.slots.insert("default".to_string(), slot);
1649
1650 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1651 let mut producer = LazyJmsProducer {
1652 pool: Arc::clone(&pool),
1653 broker_name: "default".to_string(),
1654 endpoint_config,
1655 resolved_broker_type: BrokerType::ActiveMq,
1656 runtime: test_rt(),
1657 };
1658
1659 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1660 assert!(
1661 matches!(result, Poll::Ready(Ok(()))),
1662 "poll_ready must be Ready(Ok) when bridge is Ready; got: {:?}",
1663 result
1664 );
1665 }
1666
1667 #[tokio::test]
1668 async fn poll_ready_returns_ready_when_no_slot_exists() {
1669 use tower::Service;
1670
1671 let pool = Arc::new(
1672 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1673 "tcp://localhost:61616",
1674 BrokerType::ActiveMq,
1675 ))
1676 .unwrap(),
1677 );
1678
1679 let endpoint_config = crate::config::JmsEndpointConfig::from_uri("jms:queue:test").unwrap();
1680 let mut producer = LazyJmsProducer {
1681 pool: Arc::clone(&pool),
1682 broker_name: "default".to_string(),
1683 endpoint_config,
1684 resolved_broker_type: BrokerType::ActiveMq,
1685 runtime: test_rt(),
1686 };
1687
1688 let result = producer.poll_ready(&mut Context::from_waker(futures::task::noop_waker_ref()));
1690 assert!(
1691 matches!(result, Poll::Ready(Ok(()))),
1692 "poll_ready must be Ready(Ok) when no slot exists; got: {:?}",
1693 result
1694 );
1695 }
1696
1697 #[tokio::test]
1700 async fn pool_shutdown_awaits_health_monitor() {
1701 use tokio::sync::watch;
1702
1703 let pool = Arc::new(
1704 JmsBridgePool::from_config(JmsPoolConfig {
1705 brokers: HashMap::from([(
1706 "default".to_string(),
1707 BrokerConfig {
1708 broker_url: "tcp://localhost:61616".to_string(),
1709 broker_type: BrokerType::ActiveMq,
1710 username: None,
1711 password: None,
1712 },
1713 )]),
1714 health_check_interval_ms: 100,
1715 ..JmsPoolConfig::default()
1716 })
1717 .unwrap(),
1718 );
1719
1720 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1722 channel: tonic::transport::Endpoint::from_static("http://127.0.0.1:1").connect_lazy(),
1723 });
1724 let monitor_handle_ref: Arc<tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>> =
1725 Arc::new(tokio::sync::Mutex::new(None));
1726
1727 let state_rx_clone = state_rx.clone();
1729 let handle = tokio::spawn(async move {
1730 loop {
1731 if matches!(*state_rx_clone.borrow(), BridgeState::Stopped) {
1732 break;
1733 }
1734 tokio::time::sleep(Duration::from_millis(50)).await;
1735 }
1736 });
1737 *monitor_handle_ref.lock().await = Some(handle);
1738
1739 let slot = Arc::new(BridgeSlot {
1740 name: "default".to_string(),
1741 broker_url: "tcp://localhost:61616".to_string(),
1742 broker_type: BrokerType::ActiveMq,
1743 credentials: None,
1744 state_rx,
1745 state_tx,
1746 process: Arc::new(tokio::sync::Mutex::new(None)),
1747 health_monitor_handle: monitor_handle_ref,
1748 });
1749 pool.slots.insert("default".to_string(), slot);
1750
1751 let result = pool.shutdown().await;
1753 let _ = result;
1755 }
1756
1757 #[tokio::test]
1758 async fn health_monitor_handle_stored_after_spawn() {
1759 use tokio::sync::watch;
1760
1761 let pool = Arc::new(
1762 JmsBridgePool::from_config(JmsPoolConfig {
1763 brokers: HashMap::from([(
1764 "default".to_string(),
1765 BrokerConfig {
1766 broker_url: "tcp://localhost:61616".to_string(),
1767 broker_type: BrokerType::ActiveMq,
1768 username: None,
1769 password: None,
1770 },
1771 )]),
1772 health_check_interval_ms: 100,
1773 bridge_start_timeout_ms: 100,
1774 ..JmsPoolConfig::default()
1775 })
1776 .unwrap(),
1777 );
1778
1779 let (state_tx, state_rx) = watch::channel(BridgeState::Stopped);
1780 let slot = Arc::new(BridgeSlot {
1781 name: "default".to_string(),
1782 broker_url: "tcp://localhost:61616".to_string(),
1783 broker_type: BrokerType::ActiveMq,
1784 credentials: None,
1785 state_rx,
1786 state_tx,
1787 process: Arc::new(tokio::sync::Mutex::new(None)),
1788 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1789 });
1790 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1791
1792 pool.spawn_health_monitor(Arc::clone(&slot)).await;
1793
1794 tokio::time::sleep(Duration::from_millis(50)).await;
1795 let guard = slot.health_monitor_handle.lock().await;
1796 assert!(
1797 guard.is_some(),
1798 "health monitor handle must be stored after spawn_health_monitor"
1799 );
1800 }
1801
1802 #[test]
1805 fn redact_url_strips_userinfo_with_password() {
1806 assert_eq!(
1807 redact_url("tcp://admin:s3cret@broker:61616"),
1808 "tcp://***@broker:61616"
1809 );
1810 }
1811
1812 #[test]
1813 fn redact_url_strips_userinfo_without_password() {
1814 assert_eq!(
1815 redact_url("tcp://admin@broker:61616"),
1816 "tcp://***@broker:61616"
1817 );
1818 }
1819
1820 #[test]
1821 fn redact_url_passes_clean_url_unchanged() {
1822 assert_eq!(redact_url("tcp://localhost:61616"), "tcp://localhost:61616");
1823 }
1824
1825 #[test]
1826 fn redact_url_handles_ssl_scheme() {
1827 assert_eq!(
1828 redact_url("ssl://user:pass@secure-broker:61617"),
1829 "ssl://***@secure-broker:61617"
1830 );
1831 }
1832
1833 #[tokio::test]
1836 async fn concurrent_slot_creation_respects_max_bridges() {
1837 let pool = Arc::new(
1838 JmsBridgePool::from_config(JmsPoolConfig {
1839 brokers: HashMap::from([
1840 (
1841 "b1".to_string(),
1842 BrokerConfig {
1843 broker_url: "tcp://b1:61616".to_string(),
1844 broker_type: BrokerType::ActiveMq,
1845 username: None,
1846 password: None,
1847 },
1848 ),
1849 (
1850 "b2".to_string(),
1851 BrokerConfig {
1852 broker_url: "tcp://b2:61616".to_string(),
1853 broker_type: BrokerType::ActiveMq,
1854 username: None,
1855 password: None,
1856 },
1857 ),
1858 (
1859 "b3".to_string(),
1860 BrokerConfig {
1861 broker_url: "tcp://b3:61616".to_string(),
1862 broker_type: BrokerType::ActiveMq,
1863 username: None,
1864 password: None,
1865 },
1866 ),
1867 ]),
1868 max_bridges: 2,
1869 bridge_start_timeout_ms: 100,
1870 ..JmsPoolConfig::default()
1871 })
1872 .unwrap(),
1873 );
1874
1875 let (state_tx, state_rx) = watch::channel(BridgeState::Starting);
1876 for name in &["b1", "b2"] {
1877 let slot = Arc::new(BridgeSlot {
1878 name: name.to_string(),
1879 broker_url: format!("tcp://{name}:61616"),
1880 broker_type: BrokerType::ActiveMq,
1881 credentials: None,
1882 state_rx: state_rx.clone(),
1883 state_tx: state_tx.clone(),
1884 process: Arc::new(tokio::sync::Mutex::new(None)),
1885 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1886 });
1887 pool.slots.insert(name.to_string(), slot);
1888 }
1889
1890 assert_eq!(pool.slots.len(), 2);
1891
1892 let guard = pool.bridge_create_lock.lock().await;
1893 let total_count = pool.slots.len();
1894 assert!(total_count >= pool.max_bridges);
1895 let result = if total_count >= pool.max_bridges {
1896 Err(CamelError::Config(format!(
1897 "JMS bridge limit reached: {total_count} bridge(s) >= max_bridges ({})",
1898 pool.max_bridges
1899 )))
1900 } else {
1901 Ok(())
1902 };
1903 drop(guard);
1904
1905 assert!(result.is_err(), "3rd broker should be rejected");
1906 let err_msg = result.unwrap_err().to_string();
1907 assert!(
1908 err_msg.contains("max_bridges"),
1909 "error must mention max_bridges, got: {err_msg}"
1910 );
1911 }
1912
1913 #[tokio::test]
1916 async fn transport_error_refreshes_channel_but_does_not_resend() {
1917 use tokio::sync::watch;
1918 use tonic::transport::Endpoint as TonicEndpoint;
1919 use tower::Service;
1920
1921 let dead_channel = TonicEndpoint::from_static("http://127.0.0.1:1").connect_lazy();
1922
1923 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
1924 channel: dead_channel.clone(),
1925 });
1926
1927 let pool = Arc::new(
1928 JmsBridgePool::from_config(JmsPoolConfig::single_broker(
1929 "tcp://localhost:61616",
1930 BrokerType::ActiveMq,
1931 ))
1932 .unwrap(),
1933 );
1934
1935 let slot = Arc::new(BridgeSlot {
1936 name: "default".to_string(),
1937 broker_url: "tcp://localhost:61616".to_string(),
1938 broker_type: BrokerType::ActiveMq,
1939 credentials: None,
1940 state_rx: state_rx.clone(),
1941 state_tx: state_tx.clone(),
1942 process: Arc::new(tokio::sync::Mutex::new(None)),
1943 health_monitor_handle: Arc::new(tokio::sync::Mutex::new(None)),
1944 });
1945 pool.slots.insert("default".to_string(), Arc::clone(&slot));
1946
1947 let endpoint_config =
1948 crate::config::JmsEndpointConfig::from_uri("jms:queue:test-no-resend").unwrap();
1949
1950 let mut producer = LazyJmsProducer {
1951 pool: Arc::clone(&pool),
1952 broker_name: "default".to_string(),
1953 endpoint_config,
1954 resolved_broker_type: BrokerType::ActiveMq,
1955 runtime: test_rt(),
1956 };
1957
1958 let mut exchange = Exchange::default();
1959 exchange.input.body = camel_component_api::Body::Text("hello".to_string());
1960
1961 let result = producer.call(exchange).await;
1962 assert!(result.is_err(), "expected send to fail");
1963
1964 let state_after = state_rx.borrow().clone();
1965 assert!(
1966 matches!(state_after, BridgeState::Restarting { .. }),
1967 "slot must enter Restarting; got: {:?}",
1968 state_after
1969 );
1970
1971 let err_msg = result.unwrap_err().to_string();
1972 assert!(
1973 err_msg.contains(BRIDGE_TRANSPORT_ERROR_PREFIX),
1974 "error must be original transport error, got: {}",
1975 err_msg
1976 );
1977 }
1978
1979 #[tokio::test]
1982 async fn test_jms_bridge_pool_drop_cleans_up_slots() {
1983 use std::sync::atomic::{AtomicBool, Ordering};
1984 use tokio::sync::watch;
1985
1986 let pool = Arc::new(
1987 JmsBridgePool::from_config(JmsPoolConfig {
1988 brokers: HashMap::from([(
1989 "default".to_string(),
1990 BrokerConfig {
1991 broker_url: "tcp://localhost:61616".to_string(),
1992 broker_type: BrokerType::ActiveMq,
1993 username: None,
1994 password: None,
1995 },
1996 )]),
1997 health_check_interval_ms: 50,
1998 ..JmsPoolConfig::default()
1999 })
2000 .unwrap(),
2001 );
2002
2003 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
2005 channel: tonic::transport::Endpoint::from_static("http://127.0.0.1:1").connect_lazy(),
2006 });
2007
2008 let monitor_exited = Arc::new(AtomicBool::new(false));
2009 let exited = monitor_exited.clone();
2010 let rx = state_rx.clone();
2011 let handle = tokio::spawn(async move {
2012 loop {
2013 if matches!(*rx.borrow(), BridgeState::Stopped) {
2014 break;
2015 }
2016 tokio::time::sleep(Duration::from_millis(10)).await;
2017 }
2018 exited.store(true, Ordering::SeqCst);
2019 });
2020
2021 let monitor_handle_ref: Arc<tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>> =
2022 Arc::new(tokio::sync::Mutex::new(None));
2023 *monitor_handle_ref.lock().await = Some(handle);
2024
2025 let slot = Arc::new(BridgeSlot {
2026 name: "default".to_string(),
2027 broker_url: "tcp://localhost:61616".to_string(),
2028 broker_type: BrokerType::ActiveMq,
2029 credentials: None,
2030 state_rx,
2031 state_tx,
2032 process: Arc::new(tokio::sync::Mutex::new(None)),
2033 health_monitor_handle: monitor_handle_ref,
2034 });
2035 pool.slots.insert("default".to_string(), slot);
2036
2037 drop(pool);
2039
2040 tokio::time::sleep(Duration::from_millis(200)).await;
2042
2043 assert!(
2044 monitor_exited.load(Ordering::SeqCst),
2045 "health monitor should have exited after pool drop (Stopped signal sent by Drop)"
2046 );
2047 }
2048}