Skip to main content

camel_processor/
wire_tap.rs

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/// Configuration for [`WireTapService`].
17///
18/// Default concurrency bound is 20 (Camel-faithful flat-semaphore).
19/// `shutdown_grace` defaults to 5 seconds.
20#[derive(Clone)]
21pub struct WireTapConfig {
22    /// Maximum number of concurrent tap tasks. `None` means unlimited.
23    pub max_concurrent: Option<usize>,
24    /// Grace period for in-flight tap tasks to complete on shutdown.
25    /// A value of zero means "skip drain, cancel immediately".
26    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    /// Validate the config, panicking on invalid states.
40    ///
41    /// `shutdown_grace` of zero is valid (means "skip drain, cancel immediately").
42    pub fn validate(&self) {
43        if self.max_concurrent == Some(0) {
44            panic!("max_concurrent must be > 0 when set");
45        }
46    }
47
48    /// Create a config with a bounded concurrency limit.
49    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/// Mutable admission-gate state guarded by [`WireTapShared::inner`].
59///
60/// The `Mutex` over this struct serializes the "check `open` → register task"
61/// critical section so a `shutdown()` racing with a `call()` cannot orphan a
62/// task: either `call()` registers under the lock (and `shutdown` drains it via
63/// `tracker.wait`), or `shutdown` closes admission first (and `call()` returns
64/// early without registering). There is no `await` point while the lock is held.
65#[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/// Shared admission gate, liveness tracker, and cancellation token for a
75/// [`WireTapService`] and its clones.
76///
77/// All clones of a `WireTapService` share the SAME `Arc<WireTapShared>`: per-
78/// request clones drop an `Arc` ref but do NOT close admission or cancel taps.
79/// Only the last-ref drop (canonical-service teardown) fires [`Drop`], which
80/// cancels every in-flight tap. This is defense-in-depth alongside the runtime-
81/// driven `StepLifecycle::shutdown` path (ADR-0022 mandates shutdown-before-drop,
82/// but Drop guarantees cleanup if the runtime fails to call shutdown).
83#[derive(Debug)]
84struct WireTapShared {
85    inner: Mutex<WireTapSharedInner>,
86}
87
88impl Drop for WireTapShared {
89    fn drop(&mut self) {
90        // Defense-in-depth for the cancel-and-drain contract: when the last
91        // `Arc<WireTapShared>` ref drops (canonical-service teardown), cancel
92        // every in-flight tap so spawned tasks unwind promptly via the
93        // `cancel.cancelled()` select branch in `run_tap`. The runtime calls
94        // `StepLifecycle::shutdown` before drop per ADR-0022, but Drop
95        // guarantees cleanup if it does not.
96        //
97        // Cancel BEFORE the inner fields drop: once `cancel.cancel()` fires the
98        // cancellation state is latched into the token (and all clones held by
99        // in-flight tasks), so the subsequent `CancellationToken::drop` and
100        // `TaskTracker::drop` (which detaches rather than aborts) do not race
101        // the cancellation signal.
102        self.inner
103            .lock()
104            .expect("WireTapShared mutex poisoned") // allow-unwrap
105            .cancel
106            .cancel();
107    }
108}
109
110pub struct WireTapService {
111    tap_endpoint: camel_api::BoxProcessor,
112    shared: Arc<WireTapShared>,
113}
114
115// The shared admission gate, liveness tracker, and cancellation token live in
116// `Arc<WireTapShared>`: each clone gets a new ref to the SAME shared state.
117// This is required because the route pipeline clones the `BoxProcessor` per
118// request (`BoxCloneSyncService` contract) and drops the clone once `call()`'s
119// immediate-return future resolves. Per-clone state would close admission on
120// every request drop. Sharing keeps the gate open until canonical teardown.
121impl 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    /// Create a new `WireTapService` with default (bounded-20) concurrency.
132    pub fn new(tap_endpoint: camel_api::BoxProcessor) -> Self {
133        Self::with_config(tap_endpoint, WireTapConfig::default())
134    }
135
136    /// Create a new `WireTapService` from a [`WireTapConfig`].
137    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    /// Test-only accessor for the count of currently-tracked (detached) tap
158    /// tasks. This counts ONLY detached tasks registered with the
159    /// [`TaskTracker`]; the inline CallerRuns path is NOT counted. Therefore
160    /// the bound invariant (`bound + 1` total concurrent execution, where the
161    /// +1 is the inline tap) is not observable through this accessor.
162    #[cfg(test)]
163    pub(crate) fn in_flight_count(&self) -> usize {
164        self.shared
165            .inner
166            .lock()
167            .expect("WireTapShared mutex poisoned") // allow-unwrap
168            .tracker
169            .len()
170    }
171}
172
173/// A lifecycle handle for a [`WireTapService`] implementing graceful-drain-
174/// then-abort teardown via [`StepLifecycle::shutdown`].
175///
176/// Obtain via [`WireTapService::lifecycle`]. The handle shares the same
177/// [`Arc<WireTapShared>`] as the service, so `shutdown` observes the live
178/// admission gate and task tracker.
179#[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    /// Reopen admission after a shutdown (route restart, ADR-0022).
192    ///
193    /// A start-after-shutdown installs a fresh cancellation token and task
194    /// tracker, so taps admitted after the restart are not born cancelled,
195    /// and clears the shutdown idempotency flag, so the next `shutdown`
196    /// again closes admission and drains in-flight copies.
197    async fn start(&self) -> Result<(), CamelError> {
198        {
199            let mut guard = self
200                .shared
201                .inner
202                .lock()
203                .expect("WireTapShared mutex poisoned"); // allow-unwrap
204            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        // Idempotency gate.
214        if self.shutdown_called.swap(true, Ordering::SeqCst) {
215            return Ok(());
216        }
217
218        // (a) Close admission, close the tracker, clone handles out of the lock.
219        let (tracker, cancel, grace) = {
220            let mut guard = self
221                .shared
222                .inner
223                .lock()
224                .expect("WireTapShared mutex poisoned"); // allow-unwrap
225            guard.open = false;
226            guard.tracker.close();
227            (
228                guard.tracker.clone(),
229                guard.cancel.clone(),
230                guard.shutdown_grace,
231            )
232            // MutexGuard dropped here — NO .await held across the guard.
233        };
234
235        // (b) If zero grace, skip drain — go straight to cancel.
236        // (c) DRAIN FIRST: await in-flight taps that complete naturally
237        //     within the grace period.
238        if !grace.is_zero() {
239            let _ = tokio::time::timeout(grace, tracker.wait()).await;
240        }
241
242        // (d) CANCEL: abort any stragglers that did not drain within grace.
243        cancel.cancel();
244
245        // (e) Await cancel-completions (tasks that abort on token fire).
246        let _ = tracker.wait().await;
247
248        Ok(())
249    }
250}
251
252impl WireTapService {
253    /// Obtain a shared lifecycle handle for this service's admission gate and
254    /// task tracker. Callers can invoke [`StepLifecycle::shutdown`] on the
255    /// returned handle for graceful-drain-then-abort teardown.
256    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    /// Always ready (ADR-0019): the main route never blocks on tap readiness.
270    /// Tap endpoint readiness is driven inside `run_tap` on the fire-and-
271    /// forgetget path; a tap readiness error is logged and suppressed, never
272    /// propagated to the main exchange.
273    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        // Admission critical section: hold the lock across "check open →
282        // admit-or-inline decision → register tracked task" so a racing
283        // `shutdown` cannot close the tracker between the open-check and the
284        // task registration. There is NO await point while the lock is held;
285        // `try_acquire_owned`, `tracker.spawn`, and the open-check are all sync.
286        let inner = self
287            .shared
288            .inner
289            .lock()
290            .expect("WireTapShared mutex poisoned"); // allow-unwrap
291        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                    // Admit: register a detached tracked task holding the permit.
301                    // The OwnedSemaphorePermit is MOVED into the task body and
302                    // lives for the task's lifetime, releasing on completion.
303                    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                    // Saturated: run the tap INLINE on the caller's future. No
313                    // permit is acquired or held, so total concurrent execution
314                    // transiently reaches `bound + 1` (this inline tap alongside
315                    // the `bound` detached permit-holders). The caller is
316                    // back-pressured until the inline tap finishes (CallerRuns).
317                    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                // Unbounded: register a detached tracked task with no permit.
332                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
343/// Single private helper shared by the detached path and the inline CallerRuns
344/// path. Drives the tap endpoint to readiness then calls it, racing against the
345/// shared `cancel` token so shutdown/abort unwinds promptly. Tap readiness and
346/// processing errors are logged at `warn!` (category handler-owned per
347/// ADR-0012) and suppressed — the main exchange proceeds unchanged.
348async fn run_tap(
349    mut tap_endpoint: camel_api::BoxProcessor,
350    tap_exchange: Exchange,
351    cancel: CancellationToken,
352) {
353    // Readiness phase: cancel races against `tap_endpoint.ready()`.
354    {
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            // log-policy: handler-owned
364            tracing::warn!("WireTap endpoint poll_ready failed: {}", e);
365            return;
366        }
367    }
368    // Call phase: tap_endpoint is now Ready; cancel races against the call.
369    {
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            // log-policy: handler-owned
379            tracing::warn!("WireTap processing error: {}", e);
380        }
381    }
382}
383
384/// A Tower layer that produces `WireTapService` instances.
385pub struct WireTapLayer {
386    tap_endpoint: camel_api::BoxProcessor,
387    config: WireTapConfig,
388}
389
390impl WireTapLayer {
391    /// Create a new WireTapLayer with the given tap endpoint processor (default bounded-20 concurrency).
392    pub fn new(tap_endpoint: camel_api::BoxProcessor) -> Self {
393        Self {
394            tap_endpoint,
395            config: WireTapConfig::default(),
396        }
397    }
398
399    /// Create a new WireTapLayer with bounded concurrency.
400    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    // --- Existing tests retained / adapted to the new shared-state model ---
425
426    #[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        // Under the new CallerRuns admission model, when the bound is saturated
507        // the next call runs its tap INLINE on the caller's future (without
508        // acquiring a permit). The transient peak concurrent execution is
509        // therefore `bound + 1` (the inline tap alongside the `bound` detached
510        // permit-holders), matching the spec invariant. The old `<= bound`
511        // assertion reflected the leaky spawn-then-acquire model.
512        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        // CallerRuns allows `bound + 1` (the inline tap).
541        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        // Regression for the clone-abort bug (rc-vq91): the route pipeline
550        // clones the BoxProcessor per request and drops the clone once call()'s
551        // immediate-return future resolves. With per-clone state, that drop
552        // would close admission. Sharing `Arc<WireTapShared>` keeps the gate
553        // open across clone drops; only the last-ref drop fires cancellation.
554        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        // Under the new shared-state model, dropping the canonical service
635        // drops the last `Arc<WireTapShared>` ref, firing `WireTapShared::drop`
636        // which cancels the token. The spawned tap's `run_tap` selects on
637        // `cancel.cancelled()` and returns promptly, so the 10s sleep is
638        // aborted and `task_completed` stays false.
639        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    // --- New tests for the bounded-admission + TaskTracker + cancellation model ---
685
686    #[tokio::test]
687    async fn test_wiretap_bounded_detached_count_never_exceeds_bound() {
688        // Detached tracked count must stay `<= bound`. The inline CallerRuns tap
689        // is NOT tracked so it cannot be observed here (transient total
690        // execution may briefly reach `bound + 1` — that invariant is exercised
691        // by `test_wiretap_bounded_concurrency` above).
692        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        // Background sampler: continuously polls in_flight_count() via a shared
704        // clone and tracks the peak observed detached task count.
705        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        // Fire 5 call() futures from spawned callers under contention.
717        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        // CallerRuns: when bound is saturated, the next call's tap runs INLINE
746        // on the caller's future. The caller is back-pressured until the inline
747        // tap finishes. The leaky spawn-then-acquire version would resolve the
748        // call immediately regardless of the tap's progress.
749        use tokio::sync::Notify;
750
751        let notify = Arc::new(Notify::new());
752        let tap_notify = Arc::clone(&notify);
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        // First call: acquires the sole permit, spawns detached tap awaiting Notify.
764        let _ = svc
765            .ready()
766            .await
767            .unwrap()
768            .call(Exchange::default())
769            .await
770            .unwrap();
771        // Yield to let the spawned tap actually register its `notified()` waiter.
772        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
773
774        // Second call: try_acquire fails (NoPermits). CallerRuns path runs the
775        // tap inline on this future, which awaits Notify.
776        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        // Race fut2 against a 50ms sleep: fut2 should still be Pending (it is
785        // running the tap inline, awaiting Notify).
786        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        // Release all waiters: notify_waiters wakes both the detached tap 1 and
801        // the inline tap (fut2). fut2 resolves Ok.
802        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        // Explicit unbounded (max_concurrent = None) path: tasks detach into the
813        // tracker without acquiring a permit. The tracker count rises above 0
814        // then drains to 0 as tasks complete.
815        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        // Poll until the tracker drains to 0 within 2s.
849        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        // Regression for the leaky spawn-then-acquire model: completed tasks
872        // MUST decrement the tracker's len() so subsequent bursts do not
873        // accumulate.
874        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        // Burst 1.
896        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        // Burst 2.
912        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    // --- Tracing capture helper for warn-log assertions ---
929
930    /// `MakeWriter` that appends formatted events to a shared `Vec<u8>` sink.
931    /// Used by the warn-suppression tests to assert that a `warn!` record was
932    /// emitted. The sink collects the ANSI-stripped fmt layer output.
933    #[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); // allow-unwrap: test-only
941            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    /// Install a bare registry as the process-global tracing default, once
956    /// per test binary. Guards the `capture_sink` tests against
957    /// callsite-interest poisoning: `tracing` caches each callsite's
958    /// `Interest` process-wide from its FIRST macro execution, evaluated
959    /// against the executing thread's dispatcher. Subscriber-less sibling
960    /// tests in this binary hit the shared tap `warn!` callsites below
961    /// (`wire_tap.rs:364/379`: poll_ready failed / processing error) first
962    /// and cache `Interest::never`, so a later thread-local `set_default`
963    /// capture silently drops events. The global registry heals prior
964    /// poison and floors future interest evaluations at `sometimes`
965    /// (fix pattern: c3853198; bd rc-img5).
966    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    /// Custom Service whose `poll_ready` always returns `Err`. Used to exercise
987    /// the tap-readiness-error suppression path.
988    #[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        // set_default propagates to tasks spawned via tokio within this scope.
1016        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        // Give the spawned tap task time to run ready() and log warn.
1023        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(); // allow-unwrap: test-only
1027        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(); // allow-unwrap: test-only
1053        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        // poll_ready returns Ready(Ok(())) unconditionally (ADR-0019), even
1062        // when the tap endpoint's own readiness would fail.
1063        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    // --- WireTapLifecycle + StepLifecycle shutdown tests (Task 4) ---
1078
1079    #[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        // Service whose poll_ready returns Pending indefinitely, so the
1241        // spawned task blocks in run_tap's ready() phase. Shutdown cancels
1242        // the token, the biased select! picks it up, and the task exits
1243        // cleanly without reaching call().
1244        #[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        // Stress test: fire concurrent call()s and shutdown() across many
1345        // randomized iterations. Verify in_flight_count() == 0 after shutdown
1346        // completes — no orphan task escaped the tracker.
1347        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            // Fire several concurrent callers.
1361            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            // Yield to let spawns register in the tracker.
1370            tokio::task::yield_now().await;
1371            tokio::time::sleep(Duration::from_millis(1)).await;
1372
1373            // Shutdown concurrently with callers still in-flight.
1374            lifecycle
1375                .shutdown(StepShutdownReason::RouteStop)
1376                .await
1377                .unwrap();
1378
1379            for h in handles {
1380                let _ = h.await;
1381            }
1382
1383            // Poll until tracker drains (already waited in shutdown, but
1384            // defensive check).
1385            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}