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 shutdown(&self, _reason: StepShutdownReason) -> Result<(), CamelError> {
192 if self.shutdown_called.swap(true, Ordering::SeqCst) {
194 return Ok(());
195 }
196
197 let (tracker, cancel, grace) = {
199 let mut guard = self
200 .shared
201 .inner
202 .lock()
203 .expect("WireTapShared mutex poisoned"); guard.open = false;
205 guard.tracker.close();
206 (
207 guard.tracker.clone(),
208 guard.cancel.clone(),
209 guard.shutdown_grace,
210 )
211 };
213
214 if !grace.is_zero() {
218 let _ = tokio::time::timeout(grace, tracker.wait()).await;
219 }
220
221 cancel.cancel();
223
224 let _ = tracker.wait().await;
226
227 Ok(())
228 }
229}
230
231impl WireTapService {
232 pub fn lifecycle(&self) -> Arc<dyn StepLifecycle> {
236 Arc::new(WireTapLifecycle {
237 shared: Arc::clone(&self.shared),
238 shutdown_called: AtomicBool::new(false),
239 })
240 }
241}
242
243impl Service<Exchange> for WireTapService {
244 type Response = Exchange;
245 type Error = CamelError;
246 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
247
248 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
253 Poll::Ready(Ok(()))
254 }
255
256 fn call(&mut self, exchange: Exchange) -> Self::Future {
257 let tap_endpoint = self.tap_endpoint.clone();
258 let tap_exchange = exchange.clone();
259
260 let inner = self
266 .shared
267 .inner
268 .lock()
269 .expect("WireTapShared mutex poisoned"); if !inner.open {
271 tracing::warn!("WireTap admission closed, dropping tap");
272 drop(inner);
273 return Box::pin(async move { Ok(exchange) });
274 }
275
276 match &inner.semaphore {
277 Some(sem) => match Arc::clone(sem).try_acquire_owned() {
278 Ok(permit) => {
279 let cancel = inner.cancel.clone();
283 inner.tracker.spawn(async move {
284 let _permit = permit;
285 run_tap(tap_endpoint, tap_exchange, cancel).await;
286 });
287 drop(inner);
288 Box::pin(async move { Ok(exchange) })
289 }
290 Err(TryAcquireError::NoPermits) => {
291 let cancel = inner.cancel.clone();
297 drop(inner);
298 Box::pin(async move {
299 run_tap(tap_endpoint, tap_exchange, cancel).await;
300 Ok(exchange)
301 })
302 }
303 Err(TryAcquireError::Closed) => {
304 tracing::warn!("WireTap semaphore closed, dropping tap");
305 drop(inner);
306 Box::pin(async move { Ok(exchange) })
307 }
308 },
309 None => {
310 let cancel = inner.cancel.clone();
312 inner.tracker.spawn(async move {
313 run_tap(tap_endpoint, tap_exchange, cancel).await;
314 });
315 drop(inner);
316 Box::pin(async move { Ok(exchange) })
317 }
318 }
319 }
320}
321
322async fn run_tap(
328 mut tap_endpoint: camel_api::BoxProcessor,
329 tap_exchange: Exchange,
330 cancel: CancellationToken,
331) {
332 {
334 let ready_fut = tap_endpoint.ready();
335 tokio::pin!(ready_fut);
336 let ready_result = tokio::select! {
337 biased;
338 _ = cancel.cancelled() => { return; }
339 r = &mut ready_fut => r,
340 };
341 if let Err(e) = ready_result {
342 tracing::warn!("WireTap endpoint poll_ready failed: {}", e);
344 return;
345 }
346 }
347 {
349 let call_fut = tap_endpoint.call(tap_exchange);
350 tokio::pin!(call_fut);
351 let call_result = tokio::select! {
352 biased;
353 _ = cancel.cancelled() => { return; }
354 r = &mut call_fut => r,
355 };
356 if let Err(e) = call_result {
357 tracing::warn!("WireTap processing error: {}", e);
359 }
360 }
361}
362
363pub struct WireTapLayer {
365 tap_endpoint: camel_api::BoxProcessor,
366 config: WireTapConfig,
367}
368
369impl WireTapLayer {
370 pub fn new(tap_endpoint: camel_api::BoxProcessor) -> Self {
372 Self {
373 tap_endpoint,
374 config: WireTapConfig::default(),
375 }
376 }
377
378 pub fn bounded(tap_endpoint: camel_api::BoxProcessor, max_concurrent: usize) -> Self {
380 Self {
381 tap_endpoint,
382 config: WireTapConfig::bounded(max_concurrent),
383 }
384 }
385}
386
387impl<S> tower::Layer<S> for WireTapLayer {
388 type Service = WireTapService;
389
390 fn layer(&self, _inner: S) -> Self::Service {
391 WireTapService::with_config(self.tap_endpoint.clone(), self.config.clone())
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398 use camel_api::{BoxProcessor, BoxProcessorExt, Message};
399 use std::sync::Arc;
400 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
401 use tower::ServiceExt;
402
403 #[tokio::test]
406 async fn test_wire_tap_returns_original_immediately() {
407 let tap_processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
408
409 let mut wire_tap = WireTapService::new(tap_processor);
410 let exchange = Exchange::new(Message::new("test message"));
411
412 let result = wire_tap
413 .ready()
414 .await
415 .unwrap()
416 .call(exchange)
417 .await
418 .unwrap();
419
420 assert_eq!(result.input.body.as_text(), Some("test message"));
421 }
422
423 #[tokio::test]
424 async fn test_wire_tap_endpoint_receives_clone() {
425 let received_count = Arc::new(AtomicUsize::new(0));
426 let count_clone = received_count.clone();
427
428 let tap_processor = BoxProcessor::from_fn(move |ex| {
429 let count = count_clone.clone();
430 Box::pin(async move {
431 count.fetch_add(1, Ordering::SeqCst);
432 Ok(ex)
433 })
434 });
435
436 let mut wire_tap = WireTapService::new(tap_processor);
437 let exchange = Exchange::new(Message::new("test"));
438
439 let _result = wire_tap
440 .ready()
441 .await
442 .unwrap()
443 .call(exchange)
444 .await
445 .unwrap();
446
447 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
448
449 assert_eq!(received_count.load(Ordering::SeqCst), 1);
450 }
451
452 #[tokio::test]
453 async fn test_wire_tap_isolates_errors() {
454 let tap_processor = BoxProcessor::from_fn(|_ex| {
455 Box::pin(async move { Err(CamelError::ProcessorError("tap error".into())) })
456 });
457
458 let mut wire_tap = WireTapService::new(tap_processor);
459 let exchange = Exchange::new(Message::new("test"));
460
461 let result = wire_tap.ready().await.unwrap().call(exchange).await;
462
463 assert!(result.is_ok());
464 assert_eq!(result.unwrap().input.body.as_text(), Some("test"));
465 }
466
467 #[tokio::test]
468 async fn test_wire_tap_layer() {
469 use tower::Layer;
470
471 let tap_processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
472
473 let layer = super::WireTapLayer::new(tap_processor);
474 let inner = camel_api::IdentityProcessor;
475 let mut svc = layer.layer(inner);
476
477 let exchange = Exchange::new(Message::new("test"));
478 let result = svc.ready().await.unwrap().call(exchange).await.unwrap();
479
480 assert_eq!(result.input.body.as_text(), Some("test"));
481 }
482
483 #[tokio::test]
484 async fn test_wiretap_bounded_concurrency() {
485 let concurrent = Arc::new(AtomicUsize::new(0));
492 let max_concurrent = Arc::new(AtomicUsize::new(0));
493
494 let c = Arc::clone(&concurrent);
495 let mc = Arc::clone(&max_concurrent);
496 let tap_processor = BoxProcessor::from_fn(move |ex| {
497 let c = Arc::clone(&c);
498 let mc = Arc::clone(&mc);
499 Box::pin(async move {
500 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
501 mc.fetch_max(current, Ordering::SeqCst);
502 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
503 c.fetch_sub(1, Ordering::SeqCst);
504 Ok(ex)
505 })
506 });
507
508 let config = super::WireTapConfig::bounded(2);
509 let mut svc = super::WireTapService::with_config(tap_processor, config);
510
511 for _ in 0..3 {
512 let ex = Exchange::new(Message::new("test"));
513 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
514 }
515
516 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
517
518 let observed_max = max_concurrent.load(Ordering::SeqCst);
519 assert!(
521 observed_max <= 3,
522 "max concurrency was {observed_max}, expected <= bound+1 (=3) under CallerRuns"
523 );
524 }
525
526 #[tokio::test]
527 async fn test_wire_tap_survives_per_request_clone_drop() {
528 let completed = Arc::new(AtomicUsize::new(0));
534 let completed_clone = completed.clone();
535
536 let tap_processor = BoxProcessor::from_fn(move |ex| {
537 let c = completed_clone.clone();
538 Box::pin(async move {
539 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
540 c.fetch_add(1, Ordering::SeqCst);
541 Ok(ex)
542 })
543 });
544
545 let canonical = WireTapService::new(tap_processor);
546
547 for _ in 0..3 {
548 let mut clone = canonical.clone();
549 let _ = clone
550 .ready()
551 .await
552 .unwrap()
553 .call(Exchange::new(Message::new("req")))
554 .await
555 .unwrap();
556 }
557
558 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), async {
559 while completed.load(Ordering::SeqCst) < 3 {
560 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
561 }
562 })
563 .await;
564 assert_eq!(
565 completed.load(Ordering::SeqCst),
566 3,
567 "all tap tasks must complete despite per-request clone drops"
568 );
569 }
570
571 #[test]
572 fn test_wiretap_config_default_is_bounded_20() {
573 let cfg = WireTapConfig::default();
574 assert_eq!(cfg.max_concurrent, Some(20));
575 assert_eq!(cfg.shutdown_grace, std::time::Duration::from_secs(5));
576 }
577
578 #[test]
579 fn test_wiretap_config_bounded_zero_panics() {
580 let result = std::panic::catch_unwind(|| WireTapConfig::bounded(0));
581 assert!(result.is_err());
582 if let Err(payload) = result {
583 let msg = payload
584 .downcast_ref::<&str>()
585 .expect("panic payload should be &str");
586 assert!(
587 msg.contains("max_concurrent"),
588 "panic message should contain 'max_concurrent', got: {msg}"
589 );
590 }
591 }
592
593 #[test]
594 fn test_wiretap_config_validate_rejects_zero_bound() {
595 let cfg = WireTapConfig {
596 max_concurrent: Some(0),
597 shutdown_grace: std::time::Duration::from_secs(5),
598 };
599 let result = std::panic::catch_unwind(|| cfg.validate());
600 assert!(result.is_err());
601 let payload = result.unwrap_err();
602 let msg = payload
603 .downcast_ref::<&str>()
604 .expect("panic payload should be &str");
605 assert!(
606 msg.contains("max_concurrent"),
607 "panic message should contain 'max_concurrent', got: {msg}"
608 );
609 }
610
611 #[tokio::test]
612 async fn test_wire_tap_drop_aborts_spawned_tasks() {
613 let task_started = Arc::new(AtomicBool::new(false));
619 let task_completed = Arc::new(AtomicBool::new(false));
620 let started_clone = task_started.clone();
621 let completed_clone = task_completed.clone();
622
623 let tap_processor = BoxProcessor::from_fn(move |_ex| {
624 let started = started_clone.clone();
625 let completed = completed_clone.clone();
626 Box::pin(async move {
627 started.store(true, Ordering::SeqCst);
628 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
629 completed.store(true, Ordering::SeqCst);
630 Ok(Exchange::default())
631 })
632 });
633
634 let mut service = WireTapService::new(tap_processor);
635 let _ = service
636 .ready()
637 .await
638 .unwrap()
639 .call(Exchange::default())
640 .await
641 .unwrap();
642
643 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
644 assert!(
645 task_started.load(Ordering::SeqCst),
646 "tap task should be running"
647 );
648 assert!(
649 !task_completed.load(Ordering::SeqCst),
650 "task should not have completed yet"
651 );
652
653 drop(service);
654
655 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
656
657 assert!(
658 !task_completed.load(Ordering::SeqCst),
659 "task should have been aborted, not completed"
660 );
661 }
662
663 #[tokio::test]
666 async fn test_wiretap_bounded_detached_count_never_exceeds_bound() {
667 let tap_processor = BoxProcessor::from_fn(|_ex| {
672 Box::pin(async move {
673 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
674 Ok(Exchange::default())
675 })
676 });
677
678 let canonical = WireTapService::with_config(tap_processor, WireTapConfig::bounded(2));
679 let max_seen = Arc::new(AtomicUsize::new(0));
680 let stop = Arc::new(AtomicBool::new(false));
681
682 let sampler_svc = canonical.clone();
685 let sampler_max = Arc::clone(&max_seen);
686 let sampler_stop = Arc::clone(&stop);
687 let sampler = tokio::spawn(async move {
688 while !sampler_stop.load(Ordering::SeqCst) {
689 let n = sampler_svc.in_flight_count();
690 sampler_max.fetch_max(n, Ordering::SeqCst);
691 tokio::task::yield_now().await;
692 }
693 });
694
695 let mut callers = Vec::new();
697 for _ in 0..5 {
698 let mut caller_svc = canonical.clone();
699 callers.push(tokio::spawn(async move {
700 let _ = caller_svc
701 .ready()
702 .await
703 .unwrap()
704 .call(Exchange::new(Message::new("x")))
705 .await;
706 }));
707 }
708 for h in callers {
709 let _ = h.await;
710 }
711
712 stop.store(true, Ordering::SeqCst);
713 let _ = sampler.await;
714
715 let observed = max_seen.load(Ordering::SeqCst);
716 assert!(
717 observed <= 2,
718 "detached tracked task count peaked at {observed}, expected <= bound (=2)"
719 );
720 }
721
722 #[tokio::test]
723 async fn test_wiretap_caller_backpressured_when_saturated() {
724 use tokio::sync::Notify;
729
730 let notify = Arc::new(Notify::new());
731 let tap_notify = Arc::clone(¬ify);
732 let tap_processor = BoxProcessor::from_fn(move |_ex| {
733 let n = Arc::clone(&tap_notify);
734 Box::pin(async move {
735 n.notified().await;
736 Ok(Exchange::default())
737 })
738 });
739
740 let mut svc = WireTapService::with_config(tap_processor, WireTapConfig::bounded(1));
741
742 let _ = svc
744 .ready()
745 .await
746 .unwrap()
747 .call(Exchange::default())
748 .await
749 .unwrap();
750 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
752
753 let mut svc2 = svc.clone();
756 let mut fut2 = Box::pin(
757 svc2.ready()
758 .await
759 .unwrap()
760 .call(Exchange::new(Message::new("inline"))),
761 );
762
763 let pending_after_50ms = tokio::select! {
766 r = &mut fut2 => {
767 panic!(
768 "fut2 should be Pending after 50ms under CallerRuns back-pressure; resolved early: {:?}",
769 r.is_ok()
770 );
771 }
772 _ = tokio::time::sleep(std::time::Duration::from_millis(50)) => true,
773 };
774 assert!(
775 pending_after_50ms,
776 "fut2 should be Pending (inline tap awaiting Notify) after 50ms under CallerRuns back-pressure"
777 );
778
779 notify.notify_waiters();
782 let result = fut2.await;
783 assert!(
784 result.is_ok(),
785 "fut2 should resolve Ok after notify_waiters"
786 );
787 }
788
789 #[tokio::test]
790 async fn test_wiretap_unbounded_none_path_detaches_without_permit() {
791 let tap_processor = BoxProcessor::from_fn(|_ex| {
795 Box::pin(async move {
796 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
797 Ok(Exchange::default())
798 })
799 });
800
801 let mut svc = WireTapService::with_config(
802 tap_processor,
803 WireTapConfig {
804 max_concurrent: None,
805 shutdown_grace: std::time::Duration::from_secs(5),
806 },
807 );
808
809 let sampler_svc = svc.clone();
810 let peak = Arc::new(AtomicUsize::new(0));
811 let peak_clone = Arc::clone(&peak);
812 let done = Arc::new(AtomicBool::new(false));
813 let done_clone = Arc::clone(&done);
814 let sampler = tokio::spawn(async move {
815 while !done_clone.load(Ordering::SeqCst) {
816 let n = sampler_svc.in_flight_count();
817 peak_clone.fetch_max(n, Ordering::SeqCst);
818 tokio::task::yield_now().await;
819 }
820 });
821
822 for _ in 0..50 {
823 let ex = Exchange::new(Message::new("x"));
824 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
825 }
826
827 let drained = tokio::time::timeout(std::time::Duration::from_secs(2), async {
829 loop {
830 if svc.in_flight_count() == 0 {
831 return;
832 }
833 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
834 }
835 })
836 .await
837 .is_ok();
838 done.store(true, Ordering::SeqCst);
839 let _ = sampler.await;
840
841 assert!(drained, "unbounded path tasks should drain to 0 within 2s");
842 assert!(
843 peak.load(Ordering::SeqCst) > 0,
844 "unbounded path should have observed tracked tasks (peak > 0)"
845 );
846 }
847
848 #[tokio::test]
849 async fn test_wiretap_no_unbounded_task_growth_across_bursts() {
850 let tap_processor =
854 BoxProcessor::from_fn(|_ex| Box::pin(async move { Ok(Exchange::default()) }));
855
856 let svc = WireTapService::with_config(tap_processor, WireTapConfig::default());
857
858 let drain_to_zero = |svc: &WireTapService| {
859 let s = svc.clone();
860 async move {
861 tokio::time::timeout(std::time::Duration::from_secs(2), async {
862 loop {
863 if s.in_flight_count() == 0 {
864 return;
865 }
866 tokio::time::sleep(std::time::Duration::from_millis(2)).await;
867 }
868 })
869 .await
870 .is_ok()
871 }
872 };
873
874 let mut callers = Vec::new();
876 for _ in 0..1000 {
877 let mut s = svc.clone();
878 callers.push(tokio::spawn(async move {
879 let _ = s.ready().await.unwrap().call(Exchange::default()).await;
880 }));
881 }
882 for h in callers {
883 let _ = h.await;
884 }
885 assert!(
886 drain_to_zero(&svc).await,
887 "burst 1 must drain to in_flight_count == 0 within 2s"
888 );
889
890 let mut callers = Vec::new();
892 for _ in 0..1000 {
893 let mut s = svc.clone();
894 callers.push(tokio::spawn(async move {
895 let _ = s.ready().await.unwrap().call(Exchange::default()).await;
896 }));
897 }
898 for h in callers {
899 let _ = h.await;
900 }
901 assert!(
902 drain_to_zero(&svc).await,
903 "burst 2 must drain to in_flight_count == 0 within 2s (no accumulation across bursts)"
904 );
905 }
906
907 #[derive(Clone)]
913 struct CapturingWriter {
914 sink: Arc<Mutex<Vec<u8>>>,
915 }
916
917 impl std::io::Write for CapturingWriter {
918 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
919 self.sink.lock().unwrap().extend_from_slice(buf); Ok(buf.len())
921 }
922 fn flush(&mut self) -> std::io::Result<()> {
923 Ok(())
924 }
925 }
926
927 impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CapturingWriter {
928 type Writer = CapturingWriter;
929 fn make_writer(&'a self) -> Self::Writer {
930 self.clone()
931 }
932 }
933
934 fn capture_sink() -> (Arc<Mutex<Vec<u8>>>, impl tracing::Subscriber) {
935 let sink: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
936 let writer = CapturingWriter {
937 sink: Arc::clone(&sink),
938 };
939 let subscriber = tracing_subscriber::fmt()
940 .with_writer(writer)
941 .with_ansi(false)
942 .finish();
943 (sink, subscriber)
944 }
945
946 #[derive(Clone)]
949 struct ReadyFailingSvc {
950 err_msg: &'static str,
951 }
952
953 impl Service<Exchange> for ReadyFailingSvc {
954 type Response = Exchange;
955 type Error = CamelError;
956 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
957 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
958 Poll::Ready(Err(CamelError::ProcessorError(self.err_msg.into())))
959 }
960 fn call(&mut self, ex: Exchange) -> Self::Future {
961 Box::pin(async move { Ok(ex) })
962 }
963 }
964
965 #[tokio::test]
966 async fn test_wiretap_tap_readiness_error_suppressed_with_log() {
967 let tap: camel_api::BoxProcessor = tower::util::BoxCloneService::new(ReadyFailingSvc {
968 err_msg: "ready-boom",
969 });
970 let mut svc = WireTapService::new(tap);
971
972 let (sink, subscriber) = capture_sink();
973 let exchange = Exchange::new(Message::new("main"));
974
975 let _guard = tracing::subscriber::set_default(subscriber);
977 let result = svc.ready().await.unwrap().call(exchange).await;
978
979 assert!(result.is_ok(), "tap readiness error must be suppressed");
980 assert_eq!(result.unwrap().input.body.as_text(), Some("main"));
981
982 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
984 drop(_guard);
985
986 let captured = String::from_utf8(sink.lock().unwrap().clone()).unwrap(); assert!(
988 captured.contains("ready-boom"),
989 "a warn! record mentioning the readiness error should have been emitted; got: {captured}"
990 );
991 }
992
993 #[tokio::test]
994 async fn test_wiretap_tap_processing_error_suppressed_with_log() {
995 let tap_processor = BoxProcessor::from_fn(|_ex| {
996 Box::pin(async move { Err(CamelError::ProcessorError("call-boom".into())) })
997 });
998 let mut svc = WireTapService::new(tap_processor);
999
1000 let (sink, subscriber) = capture_sink();
1001 let exchange = Exchange::new(Message::new("main"));
1002
1003 let _guard = tracing::subscriber::set_default(subscriber);
1004 let result = svc.ready().await.unwrap().call(exchange).await;
1005
1006 assert!(result.is_ok(), "tap processing error must be suppressed");
1007 assert_eq!(result.unwrap().input.body.as_text(), Some("main"));
1008
1009 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1010 drop(_guard);
1011
1012 let captured = String::from_utf8(sink.lock().unwrap().clone()).unwrap(); assert!(
1014 captured.contains("call-boom"),
1015 "a warn! record mentioning the processing error should have been emitted; got: {captured}"
1016 );
1017 }
1018
1019 #[tokio::test]
1020 async fn test_wiretap_poll_ready_always_ready() {
1021 let tap: camel_api::BoxProcessor = tower::util::BoxCloneService::new(ReadyFailingSvc {
1024 err_msg: "would-fail",
1025 });
1026 let mut svc = WireTapService::new(tap);
1027
1028 let waker = futures::task::noop_waker();
1029 let mut cx = Context::from_waker(&waker);
1030 let poll = svc.poll_ready(&mut cx);
1031 assert!(
1032 matches!(poll, Poll::Ready(Ok(()))),
1033 "poll_ready must be Ready(Ok(())) unconditionally (ADR-0019), got opposite"
1034 );
1035 }
1036
1037 #[tokio::test]
1040 async fn test_wiretap_shutdown_drains_fast_aborts_slow() {
1041 let fast_done = Arc::new(AtomicBool::new(false));
1042 let slow_done = Arc::new(AtomicBool::new(false));
1043 let call_idx = Arc::new(AtomicUsize::new(0));
1044
1045 let fd = fast_done.clone();
1046 let sd = slow_done.clone();
1047 let ci = call_idx.clone();
1048 let tap_processor = BoxProcessor::from_fn(move |ex| {
1049 let fd = fd.clone();
1050 let sd = sd.clone();
1051 let ci = ci.clone();
1052 Box::pin(async move {
1053 let n = ci.fetch_add(1, Ordering::SeqCst);
1054 if n == 0 {
1055 tokio::time::sleep(Duration::from_millis(10)).await;
1056 fd.store(true, Ordering::SeqCst);
1057 } else {
1058 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1059 sd.store(true, Ordering::SeqCst);
1060 }
1061 Ok(ex)
1062 })
1063 });
1064
1065 let config = WireTapConfig {
1066 max_concurrent: Some(20),
1067 shutdown_grace: Duration::from_millis(200),
1068 };
1069 let mut svc = WireTapService::with_config(tap_processor, config);
1070
1071 let _ = svc
1072 .ready()
1073 .await
1074 .unwrap()
1075 .call(Exchange::new(Message::new("fast")))
1076 .await
1077 .unwrap();
1078 let _ = svc
1079 .ready()
1080 .await
1081 .unwrap()
1082 .call(Exchange::new(Message::new("slow")))
1083 .await
1084 .unwrap();
1085
1086 tokio::time::sleep(Duration::from_millis(20)).await;
1087
1088 let lifecycle = svc.lifecycle();
1089 let start = tokio::time::Instant::now();
1090 lifecycle
1091 .shutdown(StepShutdownReason::RouteStop)
1092 .await
1093 .unwrap();
1094 let elapsed = start.elapsed();
1095
1096 assert!(
1097 fast_done.load(Ordering::SeqCst),
1098 "fast tap should drain before grace expires"
1099 );
1100 assert!(
1101 !slow_done.load(Ordering::SeqCst),
1102 "slow tap should be aborted after grace, not complete"
1103 );
1104 assert!(
1105 elapsed < Duration::from_millis(500),
1106 "shutdown took {:?}, expected < 500ms",
1107 elapsed
1108 );
1109 }
1110
1111 #[tokio::test]
1112 async fn test_wiretap_shutdown_idempotent() {
1113 let slow_done = Arc::new(AtomicBool::new(false));
1114 let sd = slow_done.clone();
1115 let tap_processor = BoxProcessor::from_fn(move |ex| {
1116 let sd = sd.clone();
1117 Box::pin(async move {
1118 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1119 sd.store(true, Ordering::SeqCst);
1120 Ok(ex)
1121 })
1122 });
1123
1124 let config = WireTapConfig {
1125 max_concurrent: Some(20),
1126 shutdown_grace: Duration::from_millis(50),
1127 };
1128 let mut svc = WireTapService::with_config(tap_processor, config);
1129
1130 let _ = svc
1131 .ready()
1132 .await
1133 .unwrap()
1134 .call(Exchange::new(Message::new("slow")))
1135 .await
1136 .unwrap();
1137
1138 tokio::time::sleep(Duration::from_millis(20)).await;
1139
1140 let lifecycle = svc.lifecycle();
1141 lifecycle
1142 .shutdown(StepShutdownReason::RouteStop)
1143 .await
1144 .unwrap();
1145
1146 let start = tokio::time::Instant::now();
1147 let result = lifecycle.shutdown(StepShutdownReason::HotSwap).await;
1148 let elapsed = start.elapsed();
1149
1150 assert!(result.is_ok(), "second shutdown must return Ok");
1151 assert!(
1152 elapsed < Duration::from_millis(100),
1153 "second shutdown must return promptly, took {:?}",
1154 elapsed
1155 );
1156 assert!(
1157 !slow_done.load(Ordering::SeqCst),
1158 "slow tap must be aborted, not completed"
1159 );
1160 }
1161
1162 #[tokio::test]
1163 async fn test_wiretap_calls_after_close_rejected() {
1164 let tap_invoked = Arc::new(AtomicBool::new(false));
1165 let ti = tap_invoked.clone();
1166 let tap_processor = BoxProcessor::from_fn(move |ex| {
1167 let ti = ti.clone();
1168 Box::pin(async move {
1169 ti.store(true, Ordering::SeqCst);
1170 Ok(ex)
1171 })
1172 });
1173
1174 let mut svc = WireTapService::new(tap_processor);
1175 let lifecycle = svc.lifecycle();
1176 lifecycle
1177 .shutdown(StepShutdownReason::RouteStop)
1178 .await
1179 .unwrap();
1180
1181 let result = svc
1182 .ready()
1183 .await
1184 .unwrap()
1185 .call(Exchange::new(Message::new("post-close")))
1186 .await;
1187
1188 assert!(
1189 result.is_ok(),
1190 "call after close must return Ok(original exchange)"
1191 );
1192 assert!(
1193 !tap_invoked.load(Ordering::SeqCst),
1194 "tap must not be invoked after admission closed"
1195 );
1196 }
1197
1198 #[tokio::test]
1199 async fn test_wiretap_cancellation_while_pending_readiness() {
1200 #[derive(Clone)]
1205 struct ForeverPendingSvc {
1206 called: Arc<AtomicBool>,
1207 }
1208
1209 impl Service<Exchange> for ForeverPendingSvc {
1210 type Response = Exchange;
1211 type Error = CamelError;
1212 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1213
1214 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1215 Poll::Pending
1216 }
1217
1218 fn call(&mut self, ex: Exchange) -> Self::Future {
1219 self.called.store(true, Ordering::SeqCst);
1220 Box::pin(async move { Ok(ex) })
1221 }
1222 }
1223
1224 let called = Arc::new(AtomicBool::new(false));
1225 let tap: camel_api::BoxProcessor = tower::util::BoxCloneService::new(ForeverPendingSvc {
1226 called: called.clone(),
1227 });
1228 let mut svc = WireTapService::new(tap);
1229
1230 let _ = svc
1231 .ready()
1232 .await
1233 .unwrap()
1234 .call(Exchange::new(Message::new("hanging")))
1235 .await
1236 .unwrap();
1237
1238 tokio::time::sleep(Duration::from_millis(20)).await;
1239
1240 let lifecycle = svc.lifecycle();
1241 let result = lifecycle.shutdown(StepShutdownReason::RouteStop).await;
1242
1243 assert!(
1244 result.is_ok(),
1245 "shutdown must succeed even with pending readiness: {:?}",
1246 result
1247 );
1248 assert!(
1249 !called.load(Ordering::SeqCst),
1250 "tap call() must never be reached — cancelled during readiness phase"
1251 );
1252 }
1253
1254 #[tokio::test]
1255 async fn test_wiretap_zero_grace_immediate_cancel() {
1256 let slow_done = Arc::new(AtomicBool::new(false));
1257 let sd = slow_done.clone();
1258 let tap_processor = BoxProcessor::from_fn(move |ex| {
1259 let sd = sd.clone();
1260 Box::pin(async move {
1261 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1262 sd.store(true, Ordering::SeqCst);
1263 Ok(ex)
1264 })
1265 });
1266
1267 let config = WireTapConfig {
1268 max_concurrent: Some(20),
1269 shutdown_grace: Duration::ZERO,
1270 };
1271 let mut svc = WireTapService::with_config(tap_processor, config);
1272
1273 let _ = svc
1274 .ready()
1275 .await
1276 .unwrap()
1277 .call(Exchange::new(Message::new("slow")))
1278 .await
1279 .unwrap();
1280
1281 tokio::time::sleep(Duration::from_millis(20)).await;
1282
1283 let lifecycle = svc.lifecycle();
1284 let start = tokio::time::Instant::now();
1285 lifecycle
1286 .shutdown(StepShutdownReason::RouteStop)
1287 .await
1288 .unwrap();
1289 let elapsed = start.elapsed();
1290
1291 assert!(
1292 !slow_done.load(Ordering::SeqCst),
1293 "slow tap must be aborted immediately (zero grace)"
1294 );
1295 assert!(
1296 elapsed < Duration::from_millis(200),
1297 "zero-grace shutdown must return quickly, took {:?}",
1298 elapsed
1299 );
1300 }
1301
1302 #[tokio::test(flavor = "multi_thread")]
1303 async fn test_wiretap_admission_shutdown_no_orphan_task() {
1304 const ITERATIONS: usize = 200;
1308
1309 for _ in 0..ITERATIONS {
1310 let tap_processor = BoxProcessor::from_fn(|_ex| {
1311 Box::pin(async move {
1312 tokio::time::sleep(Duration::from_millis(1)).await;
1313 Ok(Exchange::default())
1314 })
1315 });
1316
1317 let svc = WireTapService::new(tap_processor);
1318 let lifecycle = svc.lifecycle();
1319
1320 let mut handles = Vec::new();
1322 for _ in 0..4 {
1323 let mut c = svc.clone();
1324 handles.push(tokio::spawn(async move {
1325 let _ = c.ready().await.unwrap().call(Exchange::default()).await;
1326 }));
1327 }
1328
1329 tokio::task::yield_now().await;
1331 tokio::time::sleep(Duration::from_millis(1)).await;
1332
1333 lifecycle
1335 .shutdown(StepShutdownReason::RouteStop)
1336 .await
1337 .unwrap();
1338
1339 for h in handles {
1340 let _ = h.await;
1341 }
1342
1343 let drained = tokio::time::timeout(Duration::from_secs(2), async {
1346 loop {
1347 if svc.in_flight_count() == 0 {
1348 return;
1349 }
1350 tokio::time::sleep(Duration::from_millis(5)).await;
1351 }
1352 })
1353 .await
1354 .is_ok();
1355
1356 assert!(
1357 drained,
1358 "iteration: in_flight_count must drain to 0 after shutdown"
1359 );
1360 }
1361 }
1362}