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