1use std::future::Future;
2use std::pin::Pin;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5use std::task::{Context, Poll};
6use std::time::Duration;
7
8use async_trait::async_trait;
9use tokio::sync::{Semaphore, TryAcquireError};
10use tokio_util::sync::CancellationToken;
11use tokio_util::task::TaskTracker;
12use tower::{Service, ServiceExt};
13
14use camel_api::{CamelError, Exchange, StepLifecycle, StepShutdownReason};
15
16#[derive(Clone)]
21pub struct WireTapConfig {
22 pub max_concurrent: Option<usize>,
24 pub shutdown_grace: std::time::Duration,
27}
28
29impl Default for WireTapConfig {
30 fn default() -> Self {
31 Self {
32 max_concurrent: Some(20),
33 shutdown_grace: std::time::Duration::from_secs(5),
34 }
35 }
36}
37
38impl WireTapConfig {
39 pub fn validate(&self) {
43 if self.max_concurrent == Some(0) {
44 panic!("max_concurrent must be > 0 when set");
45 }
46 }
47
48 pub fn bounded(max_concurrent: usize) -> Self {
50 assert!(max_concurrent > 0, "max_concurrent must be > 0");
51 Self {
52 max_concurrent: Some(max_concurrent),
53 shutdown_grace: std::time::Duration::from_secs(5),
54 }
55 }
56}
57
58#[derive(Debug)]
66struct WireTapSharedInner {
67 open: bool,
68 tracker: TaskTracker,
69 cancel: CancellationToken,
70 semaphore: Option<Arc<Semaphore>>,
71 shutdown_grace: Duration,
72}
73
74#[derive(Debug)]
84struct WireTapShared {
85 inner: Mutex<WireTapSharedInner>,
86}
87
88impl Drop for WireTapShared {
89 fn drop(&mut self) {
90 self.inner
103 .lock()
104 .expect("WireTapShared mutex poisoned") .cancel
106 .cancel();
107 }
108}
109
110pub struct WireTapService {
111 tap_endpoint: camel_api::BoxProcessor,
112 shared: Arc<WireTapShared>,
113}
114
115impl Clone for WireTapService {
122 fn clone(&self) -> Self {
123 Self {
124 tap_endpoint: self.tap_endpoint.clone(),
125 shared: Arc::clone(&self.shared),
126 }
127 }
128}
129
130impl WireTapService {
131 pub fn new(tap_endpoint: camel_api::BoxProcessor) -> Self {
133 Self::with_config(tap_endpoint, WireTapConfig::default())
134 }
135
136 pub fn with_config(tap_endpoint: camel_api::BoxProcessor, config: WireTapConfig) -> Self {
138 config.validate();
139 let semaphore = config
140 .max_concurrent
141 .map(|limit| Arc::new(Semaphore::new(limit)));
142 let shared = Arc::new(WireTapShared {
143 inner: Mutex::new(WireTapSharedInner {
144 open: true,
145 tracker: TaskTracker::new(),
146 cancel: CancellationToken::new(),
147 semaphore,
148 shutdown_grace: config.shutdown_grace,
149 }),
150 });
151 Self {
152 tap_endpoint,
153 shared,
154 }
155 }
156
157 #[cfg(test)]
163 pub(crate) fn in_flight_count(&self) -> usize {
164 self.shared
165 .inner
166 .lock()
167 .expect("WireTapShared mutex poisoned") .tracker
169 .len()
170 }
171}
172
173#[derive(Debug)]
180pub struct WireTapLifecycle {
181 shared: Arc<WireTapShared>,
182 shutdown_called: AtomicBool,
183}
184
185#[async_trait]
186impl StepLifecycle for WireTapLifecycle {
187 fn name(&self) -> &'static str {
188 "wiretap"
189 }
190
191 async fn start(&self) -> Result<(), CamelError> {
198 {
199 let mut guard = self
200 .shared
201 .inner
202 .lock()
203 .expect("WireTapShared mutex poisoned"); guard.open = true;
205 guard.cancel = CancellationToken::new();
206 guard.tracker = TaskTracker::new();
207 }
208 self.shutdown_called.store(false, Ordering::SeqCst);
209 Ok(())
210 }
211
212 async fn shutdown(&self, _reason: StepShutdownReason) -> Result<(), CamelError> {
213 if self.shutdown_called.swap(true, Ordering::SeqCst) {
215 return Ok(());
216 }
217
218 let (tracker, cancel, grace) = {
220 let mut guard = self
221 .shared
222 .inner
223 .lock()
224 .expect("WireTapShared mutex poisoned"); guard.open = false;
226 guard.tracker.close();
227 (
228 guard.tracker.clone(),
229 guard.cancel.clone(),
230 guard.shutdown_grace,
231 )
232 };
234
235 if !grace.is_zero() {
239 let _ = tokio::time::timeout(grace, tracker.wait()).await;
240 }
241
242 cancel.cancel();
244
245 let _ = tracker.wait().await;
247
248 Ok(())
249 }
250}
251
252impl WireTapService {
253 pub fn lifecycle(&self) -> Arc<dyn StepLifecycle> {
257 Arc::new(WireTapLifecycle {
258 shared: Arc::clone(&self.shared),
259 shutdown_called: AtomicBool::new(false),
260 })
261 }
262}
263
264impl Service<Exchange> for WireTapService {
265 type Response = Exchange;
266 type Error = CamelError;
267 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
268
269 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
274 Poll::Ready(Ok(()))
275 }
276
277 fn call(&mut self, exchange: Exchange) -> Self::Future {
278 let tap_endpoint = self.tap_endpoint.clone();
279 let tap_exchange = exchange.clone();
280
281 let inner = self
287 .shared
288 .inner
289 .lock()
290 .expect("WireTapShared mutex poisoned"); if !inner.open {
292 tracing::warn!("WireTap admission closed, dropping tap");
293 drop(inner);
294 return Box::pin(async move { Ok(exchange) });
295 }
296
297 match &inner.semaphore {
298 Some(sem) => match Arc::clone(sem).try_acquire_owned() {
299 Ok(permit) => {
300 let cancel = inner.cancel.clone();
304 inner.tracker.spawn(async move {
305 let _permit = permit;
306 run_tap(tap_endpoint, tap_exchange, cancel).await;
307 });
308 drop(inner);
309 Box::pin(async move { Ok(exchange) })
310 }
311 Err(TryAcquireError::NoPermits) => {
312 let cancel = inner.cancel.clone();
318 drop(inner);
319 Box::pin(async move {
320 run_tap(tap_endpoint, tap_exchange, cancel).await;
321 Ok(exchange)
322 })
323 }
324 Err(TryAcquireError::Closed) => {
325 tracing::warn!("WireTap semaphore closed, dropping tap");
326 drop(inner);
327 Box::pin(async move { Ok(exchange) })
328 }
329 },
330 None => {
331 let cancel = inner.cancel.clone();
333 inner.tracker.spawn(async move {
334 run_tap(tap_endpoint, tap_exchange, cancel).await;
335 });
336 drop(inner);
337 Box::pin(async move { Ok(exchange) })
338 }
339 }
340 }
341}
342
343async fn run_tap(
349 mut tap_endpoint: camel_api::BoxProcessor,
350 tap_exchange: Exchange,
351 cancel: CancellationToken,
352) {
353 {
355 let ready_fut = tap_endpoint.ready();
356 tokio::pin!(ready_fut);
357 let ready_result = tokio::select! {
358 biased;
359 _ = cancel.cancelled() => { return; }
360 r = &mut ready_fut => r,
361 };
362 if let Err(e) = ready_result {
363 tracing::warn!("WireTap endpoint poll_ready failed: {}", e);
365 return;
366 }
367 }
368 {
370 let call_fut = tap_endpoint.call(tap_exchange);
371 tokio::pin!(call_fut);
372 let call_result = tokio::select! {
373 biased;
374 _ = cancel.cancelled() => { return; }
375 r = &mut call_fut => r,
376 };
377 if let Err(e) = call_result {
378 tracing::warn!("WireTap processing error: {}", e);
380 }
381 }
382}
383
384pub struct WireTapLayer {
386 tap_endpoint: camel_api::BoxProcessor,
387 config: WireTapConfig,
388}
389
390impl WireTapLayer {
391 pub fn new(tap_endpoint: camel_api::BoxProcessor) -> Self {
393 Self {
394 tap_endpoint,
395 config: WireTapConfig::default(),
396 }
397 }
398
399 pub fn bounded(tap_endpoint: camel_api::BoxProcessor, max_concurrent: usize) -> Self {
401 Self {
402 tap_endpoint,
403 config: WireTapConfig::bounded(max_concurrent),
404 }
405 }
406}
407
408impl<S> tower::Layer<S> for WireTapLayer {
409 type Service = WireTapService;
410
411 fn layer(&self, _inner: S) -> Self::Service {
412 WireTapService::with_config(self.tap_endpoint.clone(), self.config.clone())
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use camel_api::{BoxProcessor, BoxProcessorExt, Message};
420 use std::sync::Arc;
421 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
422 use tower::ServiceExt;
423
424 #[tokio::test]
427 async fn test_wire_tap_returns_original_immediately() {
428 let tap_processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
429
430 let mut wire_tap = WireTapService::new(tap_processor);
431 let exchange = Exchange::new(Message::new("test message"));
432
433 let result = wire_tap
434 .ready()
435 .await
436 .unwrap()
437 .call(exchange)
438 .await
439 .unwrap();
440
441 assert_eq!(result.input.body.as_text(), Some("test message"));
442 }
443
444 #[tokio::test]
445 async fn test_wire_tap_endpoint_receives_clone() {
446 let received_count = Arc::new(AtomicUsize::new(0));
447 let count_clone = received_count.clone();
448
449 let tap_processor = BoxProcessor::from_fn(move |ex| {
450 let count = count_clone.clone();
451 Box::pin(async move {
452 count.fetch_add(1, Ordering::SeqCst);
453 Ok(ex)
454 })
455 });
456
457 let mut wire_tap = WireTapService::new(tap_processor);
458 let exchange = Exchange::new(Message::new("test"));
459
460 let _result = wire_tap
461 .ready()
462 .await
463 .unwrap()
464 .call(exchange)
465 .await
466 .unwrap();
467
468 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
469
470 assert_eq!(received_count.load(Ordering::SeqCst), 1);
471 }
472
473 #[tokio::test]
474 async fn test_wire_tap_isolates_errors() {
475 let tap_processor = BoxProcessor::from_fn(|_ex| {
476 Box::pin(async move { Err(CamelError::ProcessorError("tap error".into())) })
477 });
478
479 let mut wire_tap = WireTapService::new(tap_processor);
480 let exchange = Exchange::new(Message::new("test"));
481
482 let result = wire_tap.ready().await.unwrap().call(exchange).await;
483
484 assert!(result.is_ok());
485 assert_eq!(result.unwrap().input.body.as_text(), Some("test"));
486 }
487
488 #[tokio::test]
489 async fn test_wire_tap_layer() {
490 use tower::Layer;
491
492 let tap_processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
493
494 let layer = super::WireTapLayer::new(tap_processor);
495 let inner = camel_api::IdentityProcessor;
496 let mut svc = layer.layer(inner);
497
498 let exchange = Exchange::new(Message::new("test"));
499 let result = svc.ready().await.unwrap().call(exchange).await.unwrap();
500
501 assert_eq!(result.input.body.as_text(), Some("test"));
502 }
503
504 #[tokio::test]
505 async fn test_wiretap_bounded_concurrency() {
506 let concurrent = Arc::new(AtomicUsize::new(0));
513 let max_concurrent = Arc::new(AtomicUsize::new(0));
514
515 let c = Arc::clone(&concurrent);
516 let mc = Arc::clone(&max_concurrent);
517 let tap_processor = BoxProcessor::from_fn(move |ex| {
518 let c = Arc::clone(&c);
519 let mc = Arc::clone(&mc);
520 Box::pin(async move {
521 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
522 mc.fetch_max(current, Ordering::SeqCst);
523 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
524 c.fetch_sub(1, Ordering::SeqCst);
525 Ok(ex)
526 })
527 });
528
529 let config = super::WireTapConfig::bounded(2);
530 let mut svc = super::WireTapService::with_config(tap_processor, config);
531
532 for _ in 0..3 {
533 let ex = Exchange::new(Message::new("test"));
534 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
535 }
536
537 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
538
539 let observed_max = max_concurrent.load(Ordering::SeqCst);
540 assert!(
542 observed_max <= 3,
543 "max concurrency was {observed_max}, expected <= bound+1 (=3) under CallerRuns"
544 );
545 }
546
547 #[tokio::test]
548 async fn test_wire_tap_survives_per_request_clone_drop() {
549 let completed = Arc::new(AtomicUsize::new(0));
555 let completed_clone = completed.clone();
556
557 let tap_processor = BoxProcessor::from_fn(move |ex| {
558 let c = completed_clone.clone();
559 Box::pin(async move {
560 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
561 c.fetch_add(1, Ordering::SeqCst);
562 Ok(ex)
563 })
564 });
565
566 let canonical = WireTapService::new(tap_processor);
567
568 for _ in 0..3 {
569 let mut clone = canonical.clone();
570 let _ = clone
571 .ready()
572 .await
573 .unwrap()
574 .call(Exchange::new(Message::new("req")))
575 .await
576 .unwrap();
577 }
578
579 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), async {
580 while completed.load(Ordering::SeqCst) < 3 {
581 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
582 }
583 })
584 .await;
585 assert_eq!(
586 completed.load(Ordering::SeqCst),
587 3,
588 "all tap tasks must complete despite per-request clone drops"
589 );
590 }
591
592 #[test]
593 fn test_wiretap_config_default_is_bounded_20() {
594 let cfg = WireTapConfig::default();
595 assert_eq!(cfg.max_concurrent, Some(20));
596 assert_eq!(cfg.shutdown_grace, std::time::Duration::from_secs(5));
597 }
598
599 #[test]
600 fn test_wiretap_config_bounded_zero_panics() {
601 let result = std::panic::catch_unwind(|| WireTapConfig::bounded(0));
602 assert!(result.is_err());
603 if let Err(payload) = result {
604 let msg = payload
605 .downcast_ref::<&str>()
606 .expect("panic payload should be &str");
607 assert!(
608 msg.contains("max_concurrent"),
609 "panic message should contain 'max_concurrent', got: {msg}"
610 );
611 }
612 }
613
614 #[test]
615 fn test_wiretap_config_validate_rejects_zero_bound() {
616 let cfg = WireTapConfig {
617 max_concurrent: Some(0),
618 shutdown_grace: std::time::Duration::from_secs(5),
619 };
620 let result = std::panic::catch_unwind(|| cfg.validate());
621 assert!(result.is_err());
622 let payload = result.unwrap_err();
623 let msg = payload
624 .downcast_ref::<&str>()
625 .expect("panic payload should be &str");
626 assert!(
627 msg.contains("max_concurrent"),
628 "panic message should contain 'max_concurrent', got: {msg}"
629 );
630 }
631
632 #[tokio::test]
633 async fn test_wire_tap_drop_aborts_spawned_tasks() {
634 let task_started = Arc::new(AtomicBool::new(false));
640 let task_completed = Arc::new(AtomicBool::new(false));
641 let started_clone = task_started.clone();
642 let completed_clone = task_completed.clone();
643
644 let tap_processor = BoxProcessor::from_fn(move |_ex| {
645 let started = started_clone.clone();
646 let completed = completed_clone.clone();
647 Box::pin(async move {
648 started.store(true, Ordering::SeqCst);
649 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
650 completed.store(true, Ordering::SeqCst);
651 Ok(Exchange::default())
652 })
653 });
654
655 let mut service = WireTapService::new(tap_processor);
656 let _ = service
657 .ready()
658 .await
659 .unwrap()
660 .call(Exchange::default())
661 .await
662 .unwrap();
663
664 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
665 assert!(
666 task_started.load(Ordering::SeqCst),
667 "tap task should be running"
668 );
669 assert!(
670 !task_completed.load(Ordering::SeqCst),
671 "task should not have completed yet"
672 );
673
674 drop(service);
675
676 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
677
678 assert!(
679 !task_completed.load(Ordering::SeqCst),
680 "task should have been aborted, not completed"
681 );
682 }
683
684 #[tokio::test]
687 async fn test_wiretap_bounded_detached_count_never_exceeds_bound() {
688 let tap_processor = BoxProcessor::from_fn(|_ex| {
693 Box::pin(async move {
694 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
695 Ok(Exchange::default())
696 })
697 });
698
699 let canonical = WireTapService::with_config(tap_processor, WireTapConfig::bounded(2));
700 let max_seen = Arc::new(AtomicUsize::new(0));
701 let stop = Arc::new(AtomicBool::new(false));
702
703 let sampler_svc = canonical.clone();
706 let sampler_max = Arc::clone(&max_seen);
707 let sampler_stop = Arc::clone(&stop);
708 let sampler = tokio::spawn(async move {
709 while !sampler_stop.load(Ordering::SeqCst) {
710 let n = sampler_svc.in_flight_count();
711 sampler_max.fetch_max(n, Ordering::SeqCst);
712 tokio::task::yield_now().await;
713 }
714 });
715
716 let mut callers = Vec::new();
718 for _ in 0..5 {
719 let mut caller_svc = canonical.clone();
720 callers.push(tokio::spawn(async move {
721 let _ = caller_svc
722 .ready()
723 .await
724 .unwrap()
725 .call(Exchange::new(Message::new("x")))
726 .await;
727 }));
728 }
729 for h in callers {
730 let _ = h.await;
731 }
732
733 stop.store(true, Ordering::SeqCst);
734 let _ = sampler.await;
735
736 let observed = max_seen.load(Ordering::SeqCst);
737 assert!(
738 observed <= 2,
739 "detached tracked task count peaked at {observed}, expected <= bound (=2)"
740 );
741 }
742
743 #[tokio::test]
744 async fn test_wiretap_caller_backpressured_when_saturated() {
745 use tokio::sync::Notify;
750
751 let notify = Arc::new(Notify::new());
752 let tap_notify = Arc::clone(¬ify);
753 let tap_processor = BoxProcessor::from_fn(move |_ex| {
754 let n = Arc::clone(&tap_notify);
755 Box::pin(async move {
756 n.notified().await;
757 Ok(Exchange::default())
758 })
759 });
760
761 let mut svc = WireTapService::with_config(tap_processor, WireTapConfig::bounded(1));
762
763 let _ = svc
765 .ready()
766 .await
767 .unwrap()
768 .call(Exchange::default())
769 .await
770 .unwrap();
771 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
773
774 let mut svc2 = svc.clone();
777 let mut fut2 = Box::pin(
778 svc2.ready()
779 .await
780 .unwrap()
781 .call(Exchange::new(Message::new("inline"))),
782 );
783
784 let pending_after_50ms = tokio::select! {
787 r = &mut fut2 => {
788 panic!(
789 "fut2 should be Pending after 50ms under CallerRuns back-pressure; resolved early: {:?}",
790 r.is_ok()
791 );
792 }
793 _ = tokio::time::sleep(std::time::Duration::from_millis(50)) => true,
794 };
795 assert!(
796 pending_after_50ms,
797 "fut2 should be Pending (inline tap awaiting Notify) after 50ms under CallerRuns back-pressure"
798 );
799
800 notify.notify_waiters();
803 let result = fut2.await;
804 assert!(
805 result.is_ok(),
806 "fut2 should resolve Ok after notify_waiters"
807 );
808 }
809
810 #[tokio::test]
811 async fn test_wiretap_unbounded_none_path_detaches_without_permit() {
812 let tap_processor = BoxProcessor::from_fn(|_ex| {
816 Box::pin(async move {
817 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
818 Ok(Exchange::default())
819 })
820 });
821
822 let mut svc = WireTapService::with_config(
823 tap_processor,
824 WireTapConfig {
825 max_concurrent: None,
826 shutdown_grace: std::time::Duration::from_secs(5),
827 },
828 );
829
830 let sampler_svc = svc.clone();
831 let peak = Arc::new(AtomicUsize::new(0));
832 let peak_clone = Arc::clone(&peak);
833 let done = Arc::new(AtomicBool::new(false));
834 let done_clone = Arc::clone(&done);
835 let sampler = tokio::spawn(async move {
836 while !done_clone.load(Ordering::SeqCst) {
837 let n = sampler_svc.in_flight_count();
838 peak_clone.fetch_max(n, Ordering::SeqCst);
839 tokio::task::yield_now().await;
840 }
841 });
842
843 for _ in 0..50 {
844 let ex = Exchange::new(Message::new("x"));
845 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
846 }
847
848 let drained = tokio::time::timeout(std::time::Duration::from_secs(2), async {
850 loop {
851 if svc.in_flight_count() == 0 {
852 return;
853 }
854 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
855 }
856 })
857 .await
858 .is_ok();
859 done.store(true, Ordering::SeqCst);
860 let _ = sampler.await;
861
862 assert!(drained, "unbounded path tasks should drain to 0 within 2s");
863 assert!(
864 peak.load(Ordering::SeqCst) > 0,
865 "unbounded path should have observed tracked tasks (peak > 0)"
866 );
867 }
868
869 #[tokio::test]
870 async fn test_wiretap_no_unbounded_task_growth_across_bursts() {
871 let tap_processor =
875 BoxProcessor::from_fn(|_ex| Box::pin(async move { Ok(Exchange::default()) }));
876
877 let svc = WireTapService::with_config(tap_processor, WireTapConfig::default());
878
879 let drain_to_zero = |svc: &WireTapService| {
880 let s = svc.clone();
881 async move {
882 tokio::time::timeout(std::time::Duration::from_secs(2), async {
883 loop {
884 if s.in_flight_count() == 0 {
885 return;
886 }
887 tokio::time::sleep(std::time::Duration::from_millis(2)).await;
888 }
889 })
890 .await
891 .is_ok()
892 }
893 };
894
895 let mut callers = Vec::new();
897 for _ in 0..1000 {
898 let mut s = svc.clone();
899 callers.push(tokio::spawn(async move {
900 let _ = s.ready().await.unwrap().call(Exchange::default()).await;
901 }));
902 }
903 for h in callers {
904 let _ = h.await;
905 }
906 assert!(
907 drain_to_zero(&svc).await,
908 "burst 1 must drain to in_flight_count == 0 within 2s"
909 );
910
911 let mut callers = Vec::new();
913 for _ in 0..1000 {
914 let mut s = svc.clone();
915 callers.push(tokio::spawn(async move {
916 let _ = s.ready().await.unwrap().call(Exchange::default()).await;
917 }));
918 }
919 for h in callers {
920 let _ = h.await;
921 }
922 assert!(
923 drain_to_zero(&svc).await,
924 "burst 2 must drain to in_flight_count == 0 within 2s (no accumulation across bursts)"
925 );
926 }
927
928 #[derive(Clone)]
934 struct CapturingWriter {
935 sink: Arc<Mutex<Vec<u8>>>,
936 }
937
938 impl std::io::Write for CapturingWriter {
939 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
940 self.sink.lock().unwrap().extend_from_slice(buf); Ok(buf.len())
942 }
943 fn flush(&mut self) -> std::io::Result<()> {
944 Ok(())
945 }
946 }
947
948 impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CapturingWriter {
949 type Writer = CapturingWriter;
950 fn make_writer(&'a self) -> Self::Writer {
951 self.clone()
952 }
953 }
954
955 fn ensure_global_tracing_default() {
968 static INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
969 if INIT.set(()).is_ok() {
970 let _ = tracing::subscriber::set_global_default(tracing_subscriber::registry());
971 }
972 }
973
974 fn capture_sink() -> (Arc<Mutex<Vec<u8>>>, impl tracing::Subscriber) {
975 ensure_global_tracing_default();
976 let sink: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
977 let writer = CapturingWriter {
978 sink: Arc::clone(&sink),
979 };
980 let subscriber = tracing_subscriber::fmt()
981 .with_writer(writer)
982 .with_ansi(false)
983 .finish();
984 (sink, subscriber)
985 }
986
987 #[derive(Clone)]
990 struct ReadyFailingSvc {
991 err_msg: &'static str,
992 }
993
994 impl Service<Exchange> for ReadyFailingSvc {
995 type Response = Exchange;
996 type Error = CamelError;
997 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
998 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
999 Poll::Ready(Err(CamelError::ProcessorError(self.err_msg.into())))
1000 }
1001 fn call(&mut self, ex: Exchange) -> Self::Future {
1002 Box::pin(async move { Ok(ex) })
1003 }
1004 }
1005
1006 #[tokio::test]
1007 async fn test_wiretap_tap_readiness_error_suppressed_with_log() {
1008 let tap: camel_api::BoxProcessor = camel_api::BoxProcessor::new(ReadyFailingSvc {
1009 err_msg: "ready-boom",
1010 });
1011 let mut svc = WireTapService::new(tap);
1012
1013 let (sink, subscriber) = capture_sink();
1014 let exchange = Exchange::new(Message::new("main"));
1015
1016 let _guard = tracing::subscriber::set_default(subscriber);
1018 tracing::callsite::rebuild_interest_cache();
1022 let result = svc.ready().await.unwrap().call(exchange).await;
1023
1024 assert!(result.is_ok(), "tap readiness error must be suppressed");
1025 assert_eq!(result.unwrap().input.body.as_text(), Some("main"));
1026
1027 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1029 drop(_guard);
1030
1031 let captured = String::from_utf8(sink.lock().unwrap().clone()).unwrap(); assert!(
1033 captured.contains("ready-boom"),
1034 "a warn! record mentioning the readiness error should have been emitted; got: {captured}"
1035 );
1036 }
1037
1038 #[tokio::test]
1039 async fn test_wiretap_tap_processing_error_suppressed_with_log() {
1040 let tap_processor = BoxProcessor::from_fn(|_ex| {
1041 Box::pin(async move { Err(CamelError::ProcessorError("call-boom".into())) })
1042 });
1043 let mut svc = WireTapService::new(tap_processor);
1044
1045 let (sink, subscriber) = capture_sink();
1046 let exchange = Exchange::new(Message::new("main"));
1047
1048 let _guard = tracing::subscriber::set_default(subscriber);
1049 tracing::callsite::rebuild_interest_cache();
1053 let result = svc.ready().await.unwrap().call(exchange).await;
1054
1055 assert!(result.is_ok(), "tap processing error must be suppressed");
1056 assert_eq!(result.unwrap().input.body.as_text(), Some("main"));
1057
1058 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1059 drop(_guard);
1060
1061 let captured = String::from_utf8(sink.lock().unwrap().clone()).unwrap(); assert!(
1063 captured.contains("call-boom"),
1064 "a warn! record mentioning the processing error should have been emitted; got: {captured}"
1065 );
1066 }
1067
1068 #[tokio::test]
1069 async fn test_wiretap_poll_ready_always_ready() {
1070 let tap: camel_api::BoxProcessor = camel_api::BoxProcessor::new(ReadyFailingSvc {
1073 err_msg: "would-fail",
1074 });
1075 let mut svc = WireTapService::new(tap);
1076
1077 let waker = futures::task::noop_waker();
1078 let mut cx = Context::from_waker(&waker);
1079 let poll = svc.poll_ready(&mut cx);
1080 assert!(
1081 matches!(poll, Poll::Ready(Ok(()))),
1082 "poll_ready must be Ready(Ok(())) unconditionally (ADR-0019), got opposite"
1083 );
1084 }
1085
1086 #[tokio::test]
1089 async fn test_wiretap_shutdown_drains_fast_aborts_slow() {
1090 let fast_done = Arc::new(AtomicBool::new(false));
1091 let slow_done = Arc::new(AtomicBool::new(false));
1092 let call_idx = Arc::new(AtomicUsize::new(0));
1093
1094 let fd = fast_done.clone();
1095 let sd = slow_done.clone();
1096 let ci = call_idx.clone();
1097 let tap_processor = BoxProcessor::from_fn(move |ex| {
1098 let fd = fd.clone();
1099 let sd = sd.clone();
1100 let ci = ci.clone();
1101 Box::pin(async move {
1102 let n = ci.fetch_add(1, Ordering::SeqCst);
1103 if n == 0 {
1104 tokio::time::sleep(Duration::from_millis(10)).await;
1105 fd.store(true, Ordering::SeqCst);
1106 } else {
1107 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1108 sd.store(true, Ordering::SeqCst);
1109 }
1110 Ok(ex)
1111 })
1112 });
1113
1114 let config = WireTapConfig {
1115 max_concurrent: Some(20),
1116 shutdown_grace: Duration::from_millis(200),
1117 };
1118 let mut svc = WireTapService::with_config(tap_processor, config);
1119
1120 let _ = svc
1121 .ready()
1122 .await
1123 .unwrap()
1124 .call(Exchange::new(Message::new("fast")))
1125 .await
1126 .unwrap();
1127 let _ = svc
1128 .ready()
1129 .await
1130 .unwrap()
1131 .call(Exchange::new(Message::new("slow")))
1132 .await
1133 .unwrap();
1134
1135 tokio::time::sleep(Duration::from_millis(20)).await;
1136
1137 let lifecycle = svc.lifecycle();
1138 let start = tokio::time::Instant::now();
1139 lifecycle
1140 .shutdown(StepShutdownReason::RouteStop)
1141 .await
1142 .unwrap();
1143 let elapsed = start.elapsed();
1144
1145 assert!(
1146 fast_done.load(Ordering::SeqCst),
1147 "fast tap should drain before grace expires"
1148 );
1149 assert!(
1150 !slow_done.load(Ordering::SeqCst),
1151 "slow tap should be aborted after grace, not complete"
1152 );
1153 assert!(
1154 elapsed < Duration::from_millis(500),
1155 "shutdown took {:?}, expected < 500ms",
1156 elapsed
1157 );
1158 }
1159
1160 #[tokio::test]
1161 async fn test_wiretap_shutdown_idempotent() {
1162 let slow_done = Arc::new(AtomicBool::new(false));
1163 let sd = slow_done.clone();
1164 let tap_processor = BoxProcessor::from_fn(move |ex| {
1165 let sd = sd.clone();
1166 Box::pin(async move {
1167 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1168 sd.store(true, Ordering::SeqCst);
1169 Ok(ex)
1170 })
1171 });
1172
1173 let config = WireTapConfig {
1174 max_concurrent: Some(20),
1175 shutdown_grace: Duration::from_millis(50),
1176 };
1177 let mut svc = WireTapService::with_config(tap_processor, config);
1178
1179 let _ = svc
1180 .ready()
1181 .await
1182 .unwrap()
1183 .call(Exchange::new(Message::new("slow")))
1184 .await
1185 .unwrap();
1186
1187 tokio::time::sleep(Duration::from_millis(20)).await;
1188
1189 let lifecycle = svc.lifecycle();
1190 lifecycle
1191 .shutdown(StepShutdownReason::RouteStop)
1192 .await
1193 .unwrap();
1194
1195 let start = tokio::time::Instant::now();
1196 let result = lifecycle.shutdown(StepShutdownReason::HotSwap).await;
1197 let elapsed = start.elapsed();
1198
1199 assert!(result.is_ok(), "second shutdown must return Ok");
1200 assert!(
1201 elapsed < Duration::from_millis(100),
1202 "second shutdown must return promptly, took {:?}",
1203 elapsed
1204 );
1205 assert!(
1206 !slow_done.load(Ordering::SeqCst),
1207 "slow tap must be aborted, not completed"
1208 );
1209 }
1210
1211 #[tokio::test]
1212 async fn test_wiretap_calls_after_close_rejected() {
1213 let tap_invoked = Arc::new(AtomicBool::new(false));
1214 let ti = tap_invoked.clone();
1215 let tap_processor = BoxProcessor::from_fn(move |ex| {
1216 let ti = ti.clone();
1217 Box::pin(async move {
1218 ti.store(true, Ordering::SeqCst);
1219 Ok(ex)
1220 })
1221 });
1222
1223 let mut svc = WireTapService::new(tap_processor);
1224 let lifecycle = svc.lifecycle();
1225 lifecycle
1226 .shutdown(StepShutdownReason::RouteStop)
1227 .await
1228 .unwrap();
1229
1230 let result = svc
1231 .ready()
1232 .await
1233 .unwrap()
1234 .call(Exchange::new(Message::new("post-close")))
1235 .await;
1236
1237 assert!(
1238 result.is_ok(),
1239 "call after close must return Ok(original exchange)"
1240 );
1241 assert!(
1242 !tap_invoked.load(Ordering::SeqCst),
1243 "tap must not be invoked after admission closed"
1244 );
1245 }
1246
1247 #[tokio::test]
1248 async fn test_wiretap_cancellation_while_pending_readiness() {
1249 #[derive(Clone)]
1254 struct ForeverPendingSvc {
1255 called: Arc<AtomicBool>,
1256 }
1257
1258 impl Service<Exchange> for ForeverPendingSvc {
1259 type Response = Exchange;
1260 type Error = CamelError;
1261 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1262
1263 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1264 Poll::Pending
1265 }
1266
1267 fn call(&mut self, ex: Exchange) -> Self::Future {
1268 self.called.store(true, Ordering::SeqCst);
1269 Box::pin(async move { Ok(ex) })
1270 }
1271 }
1272
1273 let called = Arc::new(AtomicBool::new(false));
1274 let tap: camel_api::BoxProcessor = camel_api::BoxProcessor::new(ForeverPendingSvc {
1275 called: called.clone(),
1276 });
1277 let mut svc = WireTapService::new(tap);
1278
1279 let _ = svc
1280 .ready()
1281 .await
1282 .unwrap()
1283 .call(Exchange::new(Message::new("hanging")))
1284 .await
1285 .unwrap();
1286
1287 tokio::time::sleep(Duration::from_millis(20)).await;
1288
1289 let lifecycle = svc.lifecycle();
1290 let result = lifecycle.shutdown(StepShutdownReason::RouteStop).await;
1291
1292 assert!(
1293 result.is_ok(),
1294 "shutdown must succeed even with pending readiness: {:?}",
1295 result
1296 );
1297 assert!(
1298 !called.load(Ordering::SeqCst),
1299 "tap call() must never be reached — cancelled during readiness phase"
1300 );
1301 }
1302
1303 #[tokio::test]
1304 async fn test_wiretap_zero_grace_immediate_cancel() {
1305 let slow_done = Arc::new(AtomicBool::new(false));
1306 let sd = slow_done.clone();
1307 let tap_processor = BoxProcessor::from_fn(move |ex| {
1308 let sd = sd.clone();
1309 Box::pin(async move {
1310 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1311 sd.store(true, Ordering::SeqCst);
1312 Ok(ex)
1313 })
1314 });
1315
1316 let config = WireTapConfig {
1317 max_concurrent: Some(20),
1318 shutdown_grace: Duration::ZERO,
1319 };
1320 let mut svc = WireTapService::with_config(tap_processor, config);
1321
1322 let _ = svc
1323 .ready()
1324 .await
1325 .unwrap()
1326 .call(Exchange::new(Message::new("slow")))
1327 .await
1328 .unwrap();
1329
1330 tokio::time::sleep(Duration::from_millis(20)).await;
1331
1332 let lifecycle = svc.lifecycle();
1333 let start = tokio::time::Instant::now();
1334 lifecycle
1335 .shutdown(StepShutdownReason::RouteStop)
1336 .await
1337 .unwrap();
1338 let elapsed = start.elapsed();
1339
1340 assert!(
1341 !slow_done.load(Ordering::SeqCst),
1342 "slow tap must be aborted immediately (zero grace)"
1343 );
1344 assert!(
1345 elapsed < Duration::from_millis(200),
1346 "zero-grace shutdown must return quickly, took {:?}",
1347 elapsed
1348 );
1349 }
1350
1351 #[tokio::test(flavor = "multi_thread")]
1352 async fn test_wiretap_admission_shutdown_no_orphan_task() {
1353 const ITERATIONS: usize = 200;
1357
1358 for _ in 0..ITERATIONS {
1359 let tap_processor = BoxProcessor::from_fn(|_ex| {
1360 Box::pin(async move {
1361 tokio::time::sleep(Duration::from_millis(1)).await;
1362 Ok(Exchange::default())
1363 })
1364 });
1365
1366 let svc = WireTapService::new(tap_processor);
1367 let lifecycle = svc.lifecycle();
1368
1369 let mut handles = Vec::new();
1371 for _ in 0..4 {
1372 let mut c = svc.clone();
1373 handles.push(tokio::spawn(async move {
1374 let _ = c.ready().await.unwrap().call(Exchange::default()).await;
1375 }));
1376 }
1377
1378 tokio::task::yield_now().await;
1380 tokio::time::sleep(Duration::from_millis(1)).await;
1381
1382 lifecycle
1384 .shutdown(StepShutdownReason::RouteStop)
1385 .await
1386 .unwrap();
1387
1388 for h in handles {
1389 let _ = h.await;
1390 }
1391
1392 let drained = tokio::time::timeout(Duration::from_secs(2), async {
1395 loop {
1396 if svc.in_flight_count() == 0 {
1397 return;
1398 }
1399 tokio::time::sleep(Duration::from_millis(5)).await;
1400 }
1401 })
1402 .await
1403 .is_ok();
1404
1405 assert!(
1406 drained,
1407 "iteration: in_flight_count must drain to 0 after shutdown"
1408 );
1409 }
1410 }
1411}