camel_processor/
wire_tap.rs1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Mutex;
4use std::task::{Context, Poll};
5
6use tokio::sync::Semaphore;
7use tokio::task::JoinSet;
8use tower::{Service, ServiceExt};
9
10use camel_api::{CamelError, Exchange};
11
12#[derive(Clone, Default)]
14pub struct WireTapConfig {
15 pub max_concurrent: Option<usize>,
17}
18
19impl WireTapConfig {
20 pub fn bounded(max_concurrent: usize) -> Self {
22 assert!(max_concurrent > 0, "max_concurrent must be > 0");
23 Self {
24 max_concurrent: Some(max_concurrent),
25 }
26 }
27}
28
29pub struct WireTapService {
30 tap_endpoint: camel_api::BoxProcessor,
31 semaphore: Option<std::sync::Arc<Semaphore>>,
32 in_flight: Mutex<JoinSet<()>>,
33}
34
35impl Clone for WireTapService {
42 fn clone(&self) -> Self {
43 Self {
44 tap_endpoint: self.tap_endpoint.clone(),
45 semaphore: self.semaphore.clone(),
46 in_flight: Mutex::new(JoinSet::new()),
47 }
48 }
49}
50
51impl WireTapService {
52 pub fn new(tap_endpoint: camel_api::BoxProcessor) -> Self {
54 Self {
55 tap_endpoint,
56 semaphore: None,
57 in_flight: Mutex::new(JoinSet::new()),
58 }
59 }
60
61 pub fn with_config(tap_endpoint: camel_api::BoxProcessor, config: WireTapConfig) -> Self {
63 let semaphore = config
64 .max_concurrent
65 .map(|limit| std::sync::Arc::new(Semaphore::new(limit)));
66 Self {
67 tap_endpoint,
68 semaphore,
69 in_flight: Mutex::new(JoinSet::new()),
70 }
71 }
72}
73
74impl Service<Exchange> for WireTapService {
75 type Response = Exchange;
76 type Error = CamelError;
77 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
78
79 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
80 self.tap_endpoint.poll_ready(cx)
81 }
82
83 fn call(&mut self, exchange: Exchange) -> Self::Future {
84 let mut tap_endpoint = self.tap_endpoint.clone();
85 let tap_exchange = exchange.clone();
86 let semaphore = self.semaphore.clone();
87
88 self.in_flight
90 .lock()
91 .expect("in_flight mutex poisoned") .spawn(async move {
93 let _permit = match &semaphore {
95 Some(sem) => match sem.acquire().await {
96 Ok(p) => Some(p),
97 Err(_) => {
98 tracing::warn!("WireTap semaphore closed, dropping tap");
99 return;
100 }
101 },
102 None => None,
103 };
104
105 if let Err(e) = tap_endpoint.ready().await {
106 tracing::warn!("WireTap endpoint poll_ready failed: {}", e);
107 return;
108 }
109 if let Err(e) = tap_endpoint.call(tap_exchange).await {
110 tracing::warn!("WireTap processing error: {}", e);
112 }
113 });
114 Box::pin(async move { Ok(exchange) })
115 }
116}
117
118pub struct WireTapLayer {
120 tap_endpoint: camel_api::BoxProcessor,
121 config: WireTapConfig,
122}
123
124impl WireTapLayer {
125 pub fn new(tap_endpoint: camel_api::BoxProcessor) -> Self {
127 Self {
128 tap_endpoint,
129 config: WireTapConfig::default(),
130 }
131 }
132
133 pub fn bounded(tap_endpoint: camel_api::BoxProcessor, max_concurrent: usize) -> Self {
135 Self {
136 tap_endpoint,
137 config: WireTapConfig::bounded(max_concurrent),
138 }
139 }
140}
141
142impl<S> tower::Layer<S> for WireTapLayer {
143 type Service = WireTapService;
144
145 fn layer(&self, _inner: S) -> Self::Service {
146 WireTapService::with_config(self.tap_endpoint.clone(), self.config.clone())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use camel_api::{BoxProcessor, BoxProcessorExt, Message};
154 use std::sync::Arc;
155 use std::sync::atomic::{AtomicUsize, Ordering};
156 use tower::ServiceExt;
157
158 #[tokio::test]
159 async fn test_wire_tap_returns_original_immediately() {
160 let tap_processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
161
162 let mut wire_tap = WireTapService::new(tap_processor);
163 let exchange = Exchange::new(Message::new("test message"));
164
165 let result = wire_tap
166 .ready()
167 .await
168 .unwrap()
169 .call(exchange)
170 .await
171 .unwrap();
172
173 assert_eq!(result.input.body.as_text(), Some("test message"));
174 }
175
176 #[tokio::test]
177 async fn test_wire_tap_endpoint_receives_clone() {
178 let received_count = Arc::new(AtomicUsize::new(0));
179 let count_clone = received_count.clone();
180
181 let tap_processor = BoxProcessor::from_fn(move |ex| {
182 let count = count_clone.clone();
183 Box::pin(async move {
184 count.fetch_add(1, Ordering::SeqCst);
185 Ok(ex)
186 })
187 });
188
189 let mut wire_tap = WireTapService::new(tap_processor);
190 let exchange = Exchange::new(Message::new("test"));
191
192 let _result = wire_tap
193 .ready()
194 .await
195 .unwrap()
196 .call(exchange)
197 .await
198 .unwrap();
199
200 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
201
202 assert_eq!(received_count.load(Ordering::SeqCst), 1);
203 }
204
205 #[tokio::test]
206 async fn test_wire_tap_isolates_errors() {
207 let tap_processor = BoxProcessor::from_fn(|_ex| {
208 Box::pin(async move { Err(CamelError::ProcessorError("tap error".into())) })
209 });
210
211 let mut wire_tap = WireTapService::new(tap_processor);
212 let exchange = Exchange::new(Message::new("test"));
213
214 let result = wire_tap.ready().await.unwrap().call(exchange).await;
215
216 assert!(result.is_ok());
217 assert_eq!(result.unwrap().input.body.as_text(), Some("test"));
218 }
219
220 #[tokio::test]
221 async fn test_wire_tap_layer() {
222 use tower::Layer;
223
224 let tap_processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
225
226 let layer = super::WireTapLayer::new(tap_processor);
227 let inner = camel_api::IdentityProcessor;
228 let mut svc = layer.layer(inner);
229
230 let exchange = Exchange::new(Message::new("test"));
231 let result = svc.ready().await.unwrap().call(exchange).await.unwrap();
232
233 assert_eq!(result.input.body.as_text(), Some("test"));
234 }
235
236 #[tokio::test]
237 async fn test_wiretap_bounded_concurrency() {
238 let concurrent = Arc::new(AtomicUsize::new(0));
241 let max_concurrent = Arc::new(AtomicUsize::new(0));
242
243 let c = Arc::clone(&concurrent);
244 let mc = Arc::clone(&max_concurrent);
245 let tap_processor = BoxProcessor::from_fn(move |ex| {
246 let c = Arc::clone(&c);
247 let mc = Arc::clone(&mc);
248 Box::pin(async move {
249 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
250 mc.fetch_max(current, Ordering::SeqCst);
251 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
253 c.fetch_sub(1, Ordering::SeqCst);
254 Ok(ex)
255 })
256 });
257
258 let config = super::WireTapConfig::bounded(2);
259 let mut svc = super::WireTapService::with_config(tap_processor, config);
260
261 for _ in 0..3 {
263 let ex = Exchange::new(Message::new("test"));
264 let _ = svc.ready().await.unwrap().call(ex).await.unwrap();
265 }
266
267 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
269
270 let observed_max = max_concurrent.load(Ordering::SeqCst);
271 assert!(
272 observed_max <= 2,
273 "max concurrency was {observed_max}, expected <= 2"
274 );
275 }
276
277 #[tokio::test]
278 async fn test_wire_tap_drop_aborts_spawned_tasks() {
279 use std::sync::atomic::{AtomicBool, Ordering};
280
281 let task_started = Arc::new(AtomicBool::new(false));
282 let task_completed = Arc::new(AtomicBool::new(false));
283 let started_clone = task_started.clone();
284 let completed_clone = task_completed.clone();
285
286 let tap_processor = BoxProcessor::from_fn(move |_ex| {
287 let started = started_clone.clone();
288 let completed = completed_clone.clone();
289 Box::pin(async move {
290 started.store(true, Ordering::SeqCst);
291 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
293 completed.store(true, Ordering::SeqCst);
294 Ok(Exchange::default())
295 })
296 });
297
298 let mut service = WireTapService::new(tap_processor);
299 let _ = service
300 .ready()
301 .await
302 .unwrap()
303 .call(Exchange::default())
304 .await
305 .unwrap();
306
307 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
309 assert!(
310 task_started.load(Ordering::SeqCst),
311 "tap task should be running"
312 );
313 assert!(
314 !task_completed.load(Ordering::SeqCst),
315 "task should not have completed yet"
316 );
317
318 drop(service);
320
321 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
323
324 assert!(
326 !task_completed.load(Ordering::SeqCst),
327 "task should have been aborted, not completed"
328 );
329 }
330}