camel_processor/
intercept_compose.rs1use std::future::Future;
8use std::pin::Pin;
9use std::task::{Context, Poll};
10
11use tower::{Service, ServiceExt};
12
13use camel_api::{BoxProcessor, CamelError, Exchange};
14
15use crate::wire_tap::WireTapService;
16
17#[derive(Clone)]
23struct DivertService {
24 tap: WireTapService,
25 real: BoxProcessor,
26}
27
28impl Service<Exchange> for DivertService {
29 type Response = Exchange;
30 type Error = CamelError;
31 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
32
33 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
36 Poll::Ready(Ok(()))
37 }
38
39 fn call(&mut self, exchange: Exchange) -> Self::Future {
40 let mut tap = self.tap.clone();
41 let mut real = self.real.clone();
42 Box::pin(async move {
43 let original = tap.ready().await?.call(exchange).await?;
46 real.ready().await?;
49 real.call(original).await
50 })
51 }
52}
53
54pub fn compose_divert(tap: WireTapService, real: BoxProcessor) -> BoxProcessor {
59 BoxProcessor::new(DivertService { tap, real })
60}
61
62#[cfg(test)]
63mod tests {
64 use std::future::Future;
65 use std::pin::Pin;
66 use std::sync::atomic::{AtomicUsize, Ordering};
67 use std::sync::{Arc, Mutex};
68 use std::task::{Context, Poll};
69
70 use tokio::sync::Notify;
71 use tower::{Service, ServiceExt};
72
73 use crate::wire_tap::WireTapService;
74 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message, Value};
75
76 use super::*;
77
78 #[derive(Clone)]
82 struct EventRealSvc {
83 events: Arc<Mutex<Vec<&'static str>>>,
84 }
85
86 impl Service<Exchange> for EventRealSvc {
87 type Response = Exchange;
88 type Error = CamelError;
89 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
90
91 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
92 self.events.lock().unwrap().push("ready"); Poll::Ready(Ok(()))
94 }
95
96 fn call(&mut self, mut ex: Exchange) -> Self::Future {
97 self.events.lock().unwrap().push("call"); ex.input.headers.insert(
99 "X-Sentinel".to_string(),
100 Value::String("real-ok".to_string()),
101 );
102 Box::pin(async move { Ok(ex) })
103 }
104 }
105
106 #[derive(Clone)]
109 struct ReadyFailingRealSvc {
110 events: Arc<Mutex<Vec<&'static str>>>,
111 }
112
113 impl Service<Exchange> for ReadyFailingRealSvc {
114 type Response = Exchange;
115 type Error = CamelError;
116 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
117
118 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
119 Poll::Ready(Err(CamelError::ProcessorError("sentinel-ready".into())))
120 }
121
122 fn call(&mut self, _ex: Exchange) -> Self::Future {
123 self.events.lock().unwrap().push("call"); Box::pin(async move { Ok(Exchange::default()) })
125 }
126 }
127
128 #[tokio::test]
129 async fn real_producer_readiness_is_driven_before_call_success_order() {
130 let events: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
131 let copy_stub = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
132 let real_stub = BoxProcessor::new(EventRealSvc {
133 events: events.clone(),
134 });
135
136 let tap = WireTapService::new(copy_stub);
137 let svc = compose_divert(tap, real_stub);
138
139 let result = svc
140 .oneshot(Exchange::new(Message::new("main")))
141 .await
142 .unwrap();
143
144 assert_eq!(
145 *events.lock().unwrap(), vec!["ready", "call"],
147 "real producer readiness must be driven before call"
148 );
149 assert_eq!(
150 result.input.headers.get("X-Sentinel"),
151 Some(&Value::String("real-ok".to_string())),
152 "returned exchange must be the real producer's sentinel"
153 );
154 }
155
156 #[tokio::test]
157 async fn real_producer_readiness_failure_returns_verbatim_and_skips_call() {
158 let events: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
159 let copy_stub = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
160 let real_stub = BoxProcessor::new(ReadyFailingRealSvc {
161 events: events.clone(),
162 });
163
164 let tap = WireTapService::new(copy_stub);
165 let svc = compose_divert(tap, real_stub);
166
167 let err = svc
168 .oneshot(Exchange::new(Message::new("main")))
169 .await
170 .unwrap_err();
171
172 match err {
173 CamelError::ProcessorError(msg) => assert_eq!(msg, "sentinel-ready"),
174 other => panic!("expected ProcessorError(\"sentinel-ready\"), got {other:?}"),
175 }
176 assert!(
177 events.lock().unwrap().is_empty(), "real producer call must be skipped on readiness failure"
179 );
180 }
181
182 #[tokio::test]
183 async fn wiretap_lifecycle_start_reopens_admission_with_fresh_token() {
184 use camel_api::StepShutdownReason;
185
186 let arrivals = Arc::new(AtomicUsize::new(0));
187 let arrived = Arc::new(Notify::new());
188
189 let counter = arrivals.clone();
190 let notify = arrived.clone();
191 let copy_stub = BoxProcessor::from_fn(move |ex| {
192 let counter = counter.clone();
193 let notify = notify.clone();
194 Box::pin(async move {
195 counter.fetch_add(1, Ordering::SeqCst);
196 notify.notify_one();
197 Ok(ex)
198 })
199 });
200
201 let svc = WireTapService::new(copy_stub);
202 let lifecycle = svc.lifecycle();
203
204 lifecycle
206 .shutdown(StepShutdownReason::RouteStop)
207 .await
208 .unwrap();
209 let _ = svc
210 .clone()
211 .oneshot(Exchange::new(Message::new("after-shutdown")))
212 .await
213 .unwrap();
214 assert_eq!(
215 arrivals.load(Ordering::SeqCst),
216 0,
217 "no copy must run while admission is closed"
218 );
219
220 lifecycle.start().await.unwrap();
222 let _ = svc
223 .clone()
224 .oneshot(Exchange::new(Message::new("after-restart")))
225 .await
226 .unwrap();
227 arrived.notified().await;
228 assert_eq!(
229 arrivals.load(Ordering::SeqCst),
230 1,
231 "copy must arrive after restart reopens admission"
232 );
233
234 lifecycle
236 .shutdown(StepShutdownReason::RouteStop)
237 .await
238 .unwrap();
239 let _ = svc
240 .clone()
241 .oneshot(Exchange::new(Message::new("after-second-shutdown")))
242 .await
243 .unwrap();
244 assert_eq!(
245 arrivals.load(Ordering::SeqCst),
246 1,
247 "second shutdown after restart must close admission again"
248 );
249 }
250
251 #[derive(Clone)]
254 struct CapturingWriter {
255 sink: Arc<Mutex<Vec<u8>>>,
256 }
257
258 impl std::io::Write for CapturingWriter {
259 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
260 self.sink.lock().unwrap().extend_from_slice(buf); Ok(buf.len())
262 }
263 fn flush(&mut self) -> std::io::Result<()> {
264 Ok(())
265 }
266 }
267
268 impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CapturingWriter {
269 type Writer = CapturingWriter;
270 fn make_writer(&'a self) -> Self::Writer {
271 self.clone()
272 }
273 }
274
275 #[tokio::test]
276 async fn copy_call_failure_is_suppressed_and_logged() {
277 let copy_done = Arc::new(Notify::new());
278 let notify = copy_done.clone();
279 let copy_stub = BoxProcessor::from_fn(move |_ex| {
280 let notify = notify.clone();
281 Box::pin(async move {
282 notify.notify_one();
283 Err(CamelError::ProcessorError("copy-boom".into()))
284 })
285 });
286 let real_stub = BoxProcessor::from_fn(|mut ex| {
287 Box::pin(async move {
288 ex.input.headers.insert(
289 "X-Sentinel".to_string(),
290 Value::String("real-ok".to_string()),
291 );
292 Ok(ex)
293 })
294 });
295
296 let sink: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
297 let subscriber = tracing_subscriber::fmt()
298 .with_writer(CapturingWriter { sink: sink.clone() })
299 .with_ansi(false)
300 .finish();
301
302 let _guard = tracing::subscriber::set_default(subscriber);
306 tracing::callsite::rebuild_interest_cache();
307
308 let tap = WireTapService::new(copy_stub);
309 let svc = compose_divert(tap, real_stub);
310
311 let result = svc
315 .clone()
316 .oneshot(Exchange::new(Message::new("main")))
317 .await
318 .unwrap();
319 assert_eq!(
320 result.input.headers.get("X-Sentinel"),
321 Some(&Value::String("real-ok".to_string())),
322 "real producer result must be returned verbatim"
323 );
324
325 copy_done.notified().await;
329 let captured = String::from_utf8(sink.lock().unwrap().clone()).unwrap(); assert!(
332 captured.contains("copy-boom"),
333 "a warn record mentioning the copy failure should have been emitted; got: {captured}"
334 );
335 }
336}