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() {
967 static INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
968 if INIT.set(()).is_ok() {
969 let _ = tracing::subscriber::set_global_default(tracing_subscriber::registry());
970 }
971 }
972
973 fn capture_sink() -> (Arc<Mutex<Vec<u8>>>, impl tracing::Subscriber) {
974 ensure_global_tracing_default();
975 let sink: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
976 let writer = CapturingWriter {
977 sink: Arc::clone(&sink),
978 };
979 let subscriber = tracing_subscriber::fmt()
980 .with_writer(writer)
981 .with_ansi(false)
982 .finish();
983 (sink, subscriber)
984 }
985
986 #[derive(Clone)]
989 struct ReadyFailingSvc {
990 err_msg: &'static str,
991 }
992
993 impl Service<Exchange> for ReadyFailingSvc {
994 type Response = Exchange;
995 type Error = CamelError;
996 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
997 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
998 Poll::Ready(Err(CamelError::ProcessorError(self.err_msg.into())))
999 }
1000 fn call(&mut self, ex: Exchange) -> Self::Future {
1001 Box::pin(async move { Ok(ex) })
1002 }
1003 }
1004
1005 #[tokio::test]
1006 async fn test_wiretap_tap_readiness_error_suppressed_with_log() {
1007 let tap: camel_api::BoxProcessor = camel_api::BoxProcessor::new(ReadyFailingSvc {
1008 err_msg: "ready-boom",
1009 });
1010 let mut svc = WireTapService::new(tap);
1011
1012 let (sink, subscriber) = capture_sink();
1013 let exchange = Exchange::new(Message::new("main"));
1014
1015 let _guard = tracing::subscriber::set_default(subscriber);
1017 let result = svc.ready().await.unwrap().call(exchange).await;
1018
1019 assert!(result.is_ok(), "tap readiness error must be suppressed");
1020 assert_eq!(result.unwrap().input.body.as_text(), Some("main"));
1021
1022 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1024 drop(_guard);
1025
1026 let captured = String::from_utf8(sink.lock().unwrap().clone()).unwrap(); assert!(
1028 captured.contains("ready-boom"),
1029 "a warn! record mentioning the readiness error should have been emitted; got: {captured}"
1030 );
1031 }
1032
1033 #[tokio::test]
1034 async fn test_wiretap_tap_processing_error_suppressed_with_log() {
1035 let tap_processor = BoxProcessor::from_fn(|_ex| {
1036 Box::pin(async move { Err(CamelError::ProcessorError("call-boom".into())) })
1037 });
1038 let mut svc = WireTapService::new(tap_processor);
1039
1040 let (sink, subscriber) = capture_sink();
1041 let exchange = Exchange::new(Message::new("main"));
1042
1043 let _guard = tracing::subscriber::set_default(subscriber);
1044 let result = svc.ready().await.unwrap().call(exchange).await;
1045
1046 assert!(result.is_ok(), "tap processing error must be suppressed");
1047 assert_eq!(result.unwrap().input.body.as_text(), Some("main"));
1048
1049 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1050 drop(_guard);
1051
1052 let captured = String::from_utf8(sink.lock().unwrap().clone()).unwrap(); assert!(
1054 captured.contains("call-boom"),
1055 "a warn! record mentioning the processing error should have been emitted; got: {captured}"
1056 );
1057 }
1058
1059 #[tokio::test]
1060 async fn test_wiretap_poll_ready_always_ready() {
1061 let tap: camel_api::BoxProcessor = camel_api::BoxProcessor::new(ReadyFailingSvc {
1064 err_msg: "would-fail",
1065 });
1066 let mut svc = WireTapService::new(tap);
1067
1068 let waker = futures::task::noop_waker();
1069 let mut cx = Context::from_waker(&waker);
1070 let poll = svc.poll_ready(&mut cx);
1071 assert!(
1072 matches!(poll, Poll::Ready(Ok(()))),
1073 "poll_ready must be Ready(Ok(())) unconditionally (ADR-0019), got opposite"
1074 );
1075 }
1076
1077 #[tokio::test]
1080 async fn test_wiretap_shutdown_drains_fast_aborts_slow() {
1081 let fast_done = Arc::new(AtomicBool::new(false));
1082 let slow_done = Arc::new(AtomicBool::new(false));
1083 let call_idx = Arc::new(AtomicUsize::new(0));
1084
1085 let fd = fast_done.clone();
1086 let sd = slow_done.clone();
1087 let ci = call_idx.clone();
1088 let tap_processor = BoxProcessor::from_fn(move |ex| {
1089 let fd = fd.clone();
1090 let sd = sd.clone();
1091 let ci = ci.clone();
1092 Box::pin(async move {
1093 let n = ci.fetch_add(1, Ordering::SeqCst);
1094 if n == 0 {
1095 tokio::time::sleep(Duration::from_millis(10)).await;
1096 fd.store(true, Ordering::SeqCst);
1097 } else {
1098 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1099 sd.store(true, Ordering::SeqCst);
1100 }
1101 Ok(ex)
1102 })
1103 });
1104
1105 let config = WireTapConfig {
1106 max_concurrent: Some(20),
1107 shutdown_grace: Duration::from_millis(200),
1108 };
1109 let mut svc = WireTapService::with_config(tap_processor, config);
1110
1111 let _ = svc
1112 .ready()
1113 .await
1114 .unwrap()
1115 .call(Exchange::new(Message::new("fast")))
1116 .await
1117 .unwrap();
1118 let _ = svc
1119 .ready()
1120 .await
1121 .unwrap()
1122 .call(Exchange::new(Message::new("slow")))
1123 .await
1124 .unwrap();
1125
1126 tokio::time::sleep(Duration::from_millis(20)).await;
1127
1128 let lifecycle = svc.lifecycle();
1129 let start = tokio::time::Instant::now();
1130 lifecycle
1131 .shutdown(StepShutdownReason::RouteStop)
1132 .await
1133 .unwrap();
1134 let elapsed = start.elapsed();
1135
1136 assert!(
1137 fast_done.load(Ordering::SeqCst),
1138 "fast tap should drain before grace expires"
1139 );
1140 assert!(
1141 !slow_done.load(Ordering::SeqCst),
1142 "slow tap should be aborted after grace, not complete"
1143 );
1144 assert!(
1145 elapsed < Duration::from_millis(500),
1146 "shutdown took {:?}, expected < 500ms",
1147 elapsed
1148 );
1149 }
1150
1151 #[tokio::test]
1152 async fn test_wiretap_shutdown_idempotent() {
1153 let slow_done = Arc::new(AtomicBool::new(false));
1154 let sd = slow_done.clone();
1155 let tap_processor = BoxProcessor::from_fn(move |ex| {
1156 let sd = sd.clone();
1157 Box::pin(async move {
1158 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1159 sd.store(true, Ordering::SeqCst);
1160 Ok(ex)
1161 })
1162 });
1163
1164 let config = WireTapConfig {
1165 max_concurrent: Some(20),
1166 shutdown_grace: Duration::from_millis(50),
1167 };
1168 let mut svc = WireTapService::with_config(tap_processor, config);
1169
1170 let _ = svc
1171 .ready()
1172 .await
1173 .unwrap()
1174 .call(Exchange::new(Message::new("slow")))
1175 .await
1176 .unwrap();
1177
1178 tokio::time::sleep(Duration::from_millis(20)).await;
1179
1180 let lifecycle = svc.lifecycle();
1181 lifecycle
1182 .shutdown(StepShutdownReason::RouteStop)
1183 .await
1184 .unwrap();
1185
1186 let start = tokio::time::Instant::now();
1187 let result = lifecycle.shutdown(StepShutdownReason::HotSwap).await;
1188 let elapsed = start.elapsed();
1189
1190 assert!(result.is_ok(), "second shutdown must return Ok");
1191 assert!(
1192 elapsed < Duration::from_millis(100),
1193 "second shutdown must return promptly, took {:?}",
1194 elapsed
1195 );
1196 assert!(
1197 !slow_done.load(Ordering::SeqCst),
1198 "slow tap must be aborted, not completed"
1199 );
1200 }
1201
1202 #[tokio::test]
1203 async fn test_wiretap_calls_after_close_rejected() {
1204 let tap_invoked = Arc::new(AtomicBool::new(false));
1205 let ti = tap_invoked.clone();
1206 let tap_processor = BoxProcessor::from_fn(move |ex| {
1207 let ti = ti.clone();
1208 Box::pin(async move {
1209 ti.store(true, Ordering::SeqCst);
1210 Ok(ex)
1211 })
1212 });
1213
1214 let mut svc = WireTapService::new(tap_processor);
1215 let lifecycle = svc.lifecycle();
1216 lifecycle
1217 .shutdown(StepShutdownReason::RouteStop)
1218 .await
1219 .unwrap();
1220
1221 let result = svc
1222 .ready()
1223 .await
1224 .unwrap()
1225 .call(Exchange::new(Message::new("post-close")))
1226 .await;
1227
1228 assert!(
1229 result.is_ok(),
1230 "call after close must return Ok(original exchange)"
1231 );
1232 assert!(
1233 !tap_invoked.load(Ordering::SeqCst),
1234 "tap must not be invoked after admission closed"
1235 );
1236 }
1237
1238 #[tokio::test]
1239 async fn test_wiretap_cancellation_while_pending_readiness() {
1240 #[derive(Clone)]
1245 struct ForeverPendingSvc {
1246 called: Arc<AtomicBool>,
1247 }
1248
1249 impl Service<Exchange> for ForeverPendingSvc {
1250 type Response = Exchange;
1251 type Error = CamelError;
1252 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1253
1254 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1255 Poll::Pending
1256 }
1257
1258 fn call(&mut self, ex: Exchange) -> Self::Future {
1259 self.called.store(true, Ordering::SeqCst);
1260 Box::pin(async move { Ok(ex) })
1261 }
1262 }
1263
1264 let called = Arc::new(AtomicBool::new(false));
1265 let tap: camel_api::BoxProcessor = camel_api::BoxProcessor::new(ForeverPendingSvc {
1266 called: called.clone(),
1267 });
1268 let mut svc = WireTapService::new(tap);
1269
1270 let _ = svc
1271 .ready()
1272 .await
1273 .unwrap()
1274 .call(Exchange::new(Message::new("hanging")))
1275 .await
1276 .unwrap();
1277
1278 tokio::time::sleep(Duration::from_millis(20)).await;
1279
1280 let lifecycle = svc.lifecycle();
1281 let result = lifecycle.shutdown(StepShutdownReason::RouteStop).await;
1282
1283 assert!(
1284 result.is_ok(),
1285 "shutdown must succeed even with pending readiness: {:?}",
1286 result
1287 );
1288 assert!(
1289 !called.load(Ordering::SeqCst),
1290 "tap call() must never be reached — cancelled during readiness phase"
1291 );
1292 }
1293
1294 #[tokio::test]
1295 async fn test_wiretap_zero_grace_immediate_cancel() {
1296 let slow_done = Arc::new(AtomicBool::new(false));
1297 let sd = slow_done.clone();
1298 let tap_processor = BoxProcessor::from_fn(move |ex| {
1299 let sd = sd.clone();
1300 Box::pin(async move {
1301 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1302 sd.store(true, Ordering::SeqCst);
1303 Ok(ex)
1304 })
1305 });
1306
1307 let config = WireTapConfig {
1308 max_concurrent: Some(20),
1309 shutdown_grace: Duration::ZERO,
1310 };
1311 let mut svc = WireTapService::with_config(tap_processor, config);
1312
1313 let _ = svc
1314 .ready()
1315 .await
1316 .unwrap()
1317 .call(Exchange::new(Message::new("slow")))
1318 .await
1319 .unwrap();
1320
1321 tokio::time::sleep(Duration::from_millis(20)).await;
1322
1323 let lifecycle = svc.lifecycle();
1324 let start = tokio::time::Instant::now();
1325 lifecycle
1326 .shutdown(StepShutdownReason::RouteStop)
1327 .await
1328 .unwrap();
1329 let elapsed = start.elapsed();
1330
1331 assert!(
1332 !slow_done.load(Ordering::SeqCst),
1333 "slow tap must be aborted immediately (zero grace)"
1334 );
1335 assert!(
1336 elapsed < Duration::from_millis(200),
1337 "zero-grace shutdown must return quickly, took {:?}",
1338 elapsed
1339 );
1340 }
1341
1342 #[tokio::test(flavor = "multi_thread")]
1343 async fn test_wiretap_admission_shutdown_no_orphan_task() {
1344 const ITERATIONS: usize = 200;
1348
1349 for _ in 0..ITERATIONS {
1350 let tap_processor = BoxProcessor::from_fn(|_ex| {
1351 Box::pin(async move {
1352 tokio::time::sleep(Duration::from_millis(1)).await;
1353 Ok(Exchange::default())
1354 })
1355 });
1356
1357 let svc = WireTapService::new(tap_processor);
1358 let lifecycle = svc.lifecycle();
1359
1360 let mut handles = Vec::new();
1362 for _ in 0..4 {
1363 let mut c = svc.clone();
1364 handles.push(tokio::spawn(async move {
1365 let _ = c.ready().await.unwrap().call(Exchange::default()).await;
1366 }));
1367 }
1368
1369 tokio::task::yield_now().await;
1371 tokio::time::sleep(Duration::from_millis(1)).await;
1372
1373 lifecycle
1375 .shutdown(StepShutdownReason::RouteStop)
1376 .await
1377 .unwrap();
1378
1379 for h in handles {
1380 let _ = h.await;
1381 }
1382
1383 let drained = tokio::time::timeout(Duration::from_secs(2), async {
1386 loop {
1387 if svc.in_flight_count() == 0 {
1388 return;
1389 }
1390 tokio::time::sleep(Duration::from_millis(5)).await;
1391 }
1392 })
1393 .await
1394 .is_ok();
1395
1396 assert!(
1397 drained,
1398 "iteration: in_flight_count must drain to 0 after shutdown"
1399 );
1400 }
1401 }
1402}