use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::{Service, ServiceExt};
use camel_api::{BoxProcessor, CamelError, Exchange};
use crate::wire_tap::WireTapService;
#[derive(Clone)]
struct DivertService {
tap: WireTapService,
real: BoxProcessor,
}
impl Service<Exchange> for DivertService {
type Response = Exchange;
type Error = CamelError;
type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, exchange: Exchange) -> Self::Future {
let mut tap = self.tap.clone();
let mut real = self.real.clone();
Box::pin(async move {
let original = tap.ready().await?.call(exchange).await?;
real.ready().await?;
real.call(original).await
})
}
}
pub fn compose_divert(tap: WireTapService, real: BoxProcessor) -> BoxProcessor {
BoxProcessor::new(DivertService { tap, real })
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use tokio::sync::Notify;
use tower::{Service, ServiceExt};
use crate::wire_tap::WireTapService;
use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message, Value};
use super::*;
#[derive(Clone)]
struct EventRealSvc {
events: Arc<Mutex<Vec<&'static str>>>,
}
impl Service<Exchange> for EventRealSvc {
type Response = Exchange;
type Error = CamelError;
type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.events.lock().unwrap().push("ready"); Poll::Ready(Ok(()))
}
fn call(&mut self, mut ex: Exchange) -> Self::Future {
self.events.lock().unwrap().push("call"); ex.input.headers.insert(
"X-Sentinel".to_string(),
Value::String("real-ok".to_string()),
);
Box::pin(async move { Ok(ex) })
}
}
#[derive(Clone)]
struct ReadyFailingRealSvc {
events: Arc<Mutex<Vec<&'static str>>>,
}
impl Service<Exchange> for ReadyFailingRealSvc {
type Response = Exchange;
type Error = CamelError;
type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Err(CamelError::ProcessorError("sentinel-ready".into())))
}
fn call(&mut self, _ex: Exchange) -> Self::Future {
self.events.lock().unwrap().push("call"); Box::pin(async move { Ok(Exchange::default()) })
}
}
#[tokio::test]
async fn real_producer_readiness_is_driven_before_call_success_order() {
let events: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let copy_stub = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
let real_stub = BoxProcessor::new(EventRealSvc {
events: events.clone(),
});
let tap = WireTapService::new(copy_stub);
let svc = compose_divert(tap, real_stub);
let result = svc
.oneshot(Exchange::new(Message::new("main")))
.await
.unwrap();
assert_eq!(
*events.lock().unwrap(), vec!["ready", "call"],
"real producer readiness must be driven before call"
);
assert_eq!(
result.input.headers.get("X-Sentinel"),
Some(&Value::String("real-ok".to_string())),
"returned exchange must be the real producer's sentinel"
);
}
#[tokio::test]
async fn real_producer_readiness_failure_returns_verbatim_and_skips_call() {
let events: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let copy_stub = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
let real_stub = BoxProcessor::new(ReadyFailingRealSvc {
events: events.clone(),
});
let tap = WireTapService::new(copy_stub);
let svc = compose_divert(tap, real_stub);
let err = svc
.oneshot(Exchange::new(Message::new("main")))
.await
.unwrap_err();
match err {
CamelError::ProcessorError(msg) => assert_eq!(msg, "sentinel-ready"),
other => panic!("expected ProcessorError(\"sentinel-ready\"), got {other:?}"),
}
assert!(
events.lock().unwrap().is_empty(), "real producer call must be skipped on readiness failure"
);
}
#[tokio::test]
async fn wiretap_lifecycle_start_reopens_admission_with_fresh_token() {
use camel_api::StepShutdownReason;
let arrivals = Arc::new(AtomicUsize::new(0));
let arrived = Arc::new(Notify::new());
let counter = arrivals.clone();
let notify = arrived.clone();
let copy_stub = BoxProcessor::from_fn(move |ex| {
let counter = counter.clone();
let notify = notify.clone();
Box::pin(async move {
counter.fetch_add(1, Ordering::SeqCst);
notify.notify_one();
Ok(ex)
})
});
let svc = WireTapService::new(copy_stub);
let lifecycle = svc.lifecycle();
lifecycle
.shutdown(StepShutdownReason::RouteStop)
.await
.unwrap();
let _ = svc
.clone()
.oneshot(Exchange::new(Message::new("after-shutdown")))
.await
.unwrap();
assert_eq!(
arrivals.load(Ordering::SeqCst),
0,
"no copy must run while admission is closed"
);
lifecycle.start().await.unwrap();
let _ = svc
.clone()
.oneshot(Exchange::new(Message::new("after-restart")))
.await
.unwrap();
arrived.notified().await;
assert_eq!(
arrivals.load(Ordering::SeqCst),
1,
"copy must arrive after restart reopens admission"
);
lifecycle
.shutdown(StepShutdownReason::RouteStop)
.await
.unwrap();
let _ = svc
.clone()
.oneshot(Exchange::new(Message::new("after-second-shutdown")))
.await
.unwrap();
assert_eq!(
arrivals.load(Ordering::SeqCst),
1,
"second shutdown after restart must close admission again"
);
}
#[derive(Clone)]
struct CapturingWriter {
sink: Arc<Mutex<Vec<u8>>>,
}
impl std::io::Write for CapturingWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.sink.lock().unwrap().extend_from_slice(buf); Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CapturingWriter {
type Writer = CapturingWriter;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
#[tokio::test]
async fn copy_call_failure_is_suppressed_and_logged() {
let copy_done = Arc::new(Notify::new());
let notify = copy_done.clone();
let copy_stub = BoxProcessor::from_fn(move |_ex| {
let notify = notify.clone();
Box::pin(async move {
notify.notify_one();
Err(CamelError::ProcessorError("copy-boom".into()))
})
});
let real_stub = BoxProcessor::from_fn(|mut ex| {
Box::pin(async move {
ex.input.headers.insert(
"X-Sentinel".to_string(),
Value::String("real-ok".to_string()),
);
Ok(ex)
})
});
let sink: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
let subscriber = tracing_subscriber::fmt()
.with_writer(CapturingWriter { sink: sink.clone() })
.with_ansi(false)
.finish();
let _guard = tracing::subscriber::set_default(subscriber);
tracing::callsite::rebuild_interest_cache();
let tap = WireTapService::new(copy_stub);
let svc = compose_divert(tap, real_stub);
let result = svc
.clone()
.oneshot(Exchange::new(Message::new("main")))
.await
.unwrap();
assert_eq!(
result.input.headers.get("X-Sentinel"),
Some(&Value::String("real-ok".to_string())),
"real producer result must be returned verbatim"
);
copy_done.notified().await;
let captured = String::from_utf8(sink.lock().unwrap().clone()).unwrap(); assert!(
captured.contains("copy-boom"),
"a warn record mentioning the copy failure should have been emitted; got: {captured}"
);
}
}