1use crate::endpoints::{create_consumer_from_route, create_publisher_from_route};
7use crate::errors::ProcessingError;
8pub use crate::models::Route;
9use crate::models::{Endpoint, EndpointType, Middleware, RouteOptions};
10use crate::traits::{
11 BatchCommitFunc, ConsumerError, Handler, HandlerError, MessageConsumer, MessageDisposition,
12 MessagePublisher, PublisherError, SentBatch,
13};
14use async_channel::{bounded, Sender};
15use serde::de::DeserializeOwned;
16use std::collections::{BTreeMap, HashMap, HashSet};
17use std::sync::{Arc, OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
18use tokio::{
19 select,
20 task::{JoinHandle, JoinSet},
21};
22use tracing::{debug, error, info, trace, warn};
23
24pub use crate::extensions::{
26 get_endpoint_factory, get_middleware_factory, register_endpoint_factory,
27 register_middleware_factory,
28};
29
30#[derive(Debug)]
31pub struct RouteHandle((JoinHandle<()>, Sender<()>));
32
33impl RouteHandle {
34 pub async fn stop(&self) {
35 let _ = self.0 .1.send(()).await;
36 self.0 .1.close();
37 }
38
39 pub async fn join(self) -> Result<(), tokio::task::JoinError> {
40 self.0 .0.await
41 }
42}
43
44pub(crate) async fn run_publisher_connect_hook(
45 route_name: &str,
46 publisher: &Arc<dyn MessagePublisher>,
47) -> anyhow::Result<()> {
48 if let Some(hook) = publisher.on_connect_hook() {
49 hook.await.map_err(|err| {
50 anyhow::anyhow!(
51 "Publisher on_connect hook failed for route '{}': {}",
52 route_name,
53 err
54 )
55 })?;
56 }
57 Ok(())
58}
59
60pub(crate) async fn run_consumer_connect_hook(
61 route_name: &str,
62 consumer: &dyn MessageConsumer,
63) -> anyhow::Result<()> {
64 if let Some(hook) = consumer.on_connect_hook() {
65 hook.await.map_err(|err| {
66 anyhow::anyhow!(
67 "Consumer on_connect hook failed for route '{}': {}",
68 route_name,
69 err
70 )
71 })?;
72 }
73 Ok(())
74}
75
76pub(crate) async fn run_publisher_disconnect_hook(
77 route_name: &str,
78 publisher: &Arc<dyn MessagePublisher>,
79) {
80 if let Some(hook) = publisher.on_disconnect_hook() {
81 if let Err(err) = hook.await {
82 warn!(
83 "Publisher on_disconnect hook failed for route '{}': {}",
84 route_name, err
85 );
86 }
87 }
88}
89
90pub(crate) async fn run_consumer_disconnect_hook(route_name: &str, consumer: &dyn MessageConsumer) {
91 if let Some(hook) = consumer.on_disconnect_hook() {
92 if let Err(err) = hook.await {
93 warn!(
94 "Consumer on_disconnect hook failed for route '{}': {}",
95 route_name, err
96 );
97 }
98 }
99}
100
101impl From<(JoinHandle<()>, Sender<()>)> for RouteHandle {
102 fn from(tuple: (JoinHandle<()>, Sender<()>)) -> Self {
103 RouteHandle(tuple)
104 }
105}
106
107struct ActiveRoute {
108 route: Route,
109 handle: RouteHandle,
110}
111
112static ROUTE_REGISTRY: OnceLock<RwLock<HashMap<String, ActiveRoute>>> = OnceLock::new();
113static ENDPOINT_REF_REGISTRY: OnceLock<RwLock<HashMap<String, Endpoint>>> = OnceLock::new();
114
115fn recover_read_lock<'a, T>(lock: &'a RwLock<T>, name: &str) -> RwLockReadGuard<'a, T> {
116 lock.read().unwrap_or_else(|poisoned| {
117 warn!(lock = name, "Recovering from poisoned read lock");
118 poisoned.into_inner()
119 })
120}
121
122fn recover_write_lock<'a, T>(lock: &'a RwLock<T>, name: &str) -> RwLockWriteGuard<'a, T> {
123 lock.write().unwrap_or_else(|poisoned| {
124 warn!(lock = name, "Recovering from poisoned write lock");
125 poisoned.into_inner()
126 })
127}
128
129pub fn register_endpoint(name: &str, endpoint: Endpoint) {
132 let registry = ENDPOINT_REF_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
133 let mut writer = recover_write_lock(registry, "endpoint_ref_registry");
134 if writer.insert(name.to_string(), endpoint).is_some() {
135 debug!("Overwriting a registered endpoint named '{}'", name);
136 }
137}
138
139pub fn get_endpoint(name: &str) -> Option<Endpoint> {
141 let registry = ENDPOINT_REF_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
142 let reader = recover_read_lock(registry, "endpoint_ref_registry");
143 reader.get(name).cloned()
144}
145
146fn check_fault_middleware_allowed(
147 endpoint: &Endpoint,
148 route_name: &str,
149 role: &str,
150 depth: usize,
151 visited: &mut std::collections::HashSet<String>,
152) -> anyhow::Result<()> {
153 const MAX_DEPTH: usize = 16;
154 if depth > MAX_DEPTH {
155 return Err(anyhow::anyhow!(
156 "[route:{}] Endpoint policy recursion depth exceeded limit of {}",
157 route_name,
158 MAX_DEPTH
159 ));
160 }
161
162 if endpoint
163 .middlewares
164 .iter()
165 .any(|m| matches!(m, Middleware::RandomPanic(cfg) if cfg.enabled))
166 {
167 return Err(anyhow::anyhow!(
168 "[route:{}] random_panic middleware is disabled by default for {} endpoints; set allow_fault_injection: true to enable it",
169 route_name,
170 role
171 ));
172 }
173
174 match &endpoint.endpoint_type {
175 EndpointType::Ref(name) => {
176 if !visited.insert(name.clone()) {
177 return Ok(());
178 }
179 if let Some(referenced) = get_endpoint(name) {
180 check_fault_middleware_allowed(&referenced, route_name, role, depth + 1, visited)?;
181 }
182 }
183 EndpointType::Fanout(endpoints) => {
184 for endpoint in endpoints {
185 check_fault_middleware_allowed(endpoint, route_name, role, depth + 1, visited)?;
186 }
187 }
188 EndpointType::Switch(cfg) => {
189 for endpoint in cfg.cases.values() {
190 check_fault_middleware_allowed(endpoint, route_name, role, depth + 1, visited)?;
191 }
192 if let Some(endpoint) = &cfg.default {
193 check_fault_middleware_allowed(endpoint, route_name, role, depth + 1, visited)?;
194 }
195 }
196 EndpointType::Reader(inner) => {
197 check_fault_middleware_allowed(inner, route_name, role, depth + 1, visited)?;
198 }
199 _ => {}
200 }
201
202 Ok(())
203}
204
205async fn pause_after_empty_batch(delay_ms: u64) {
206 if delay_ms > 0 {
207 tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
208 } else {
209 tokio::task::yield_now().await;
210 }
211}
212
213fn report_route_error(err_tx: &Sender<anyhow::Error>, err: anyhow::Error, context: &str) {
214 match err_tx.try_send(err) {
215 Ok(_) => trace!("Reported error to main task"),
216 Err(err_send) => warn!(
217 error = ?err_send,
218 "{}; main task might be down or busy.",
219 context
220 ),
221 }
222}
223
224struct BatchScratch {
225 message_ids: Vec<u128>,
226 request_ids: HashSet<u128>,
227}
228
229impl BatchScratch {
230 fn with_capacity(capacity: usize) -> Self {
231 Self {
232 message_ids: Vec::with_capacity(capacity),
233 request_ids: HashSet::with_capacity(capacity),
234 }
235 }
236
237 fn fill_from(&mut self, messages: &[crate::CanonicalMessage]) {
238 self.message_ids.clear();
239 self.message_ids
240 .extend(messages.iter().map(|m| m.message_id));
241 self.request_ids.clear();
242 self.request_ids.extend(
243 messages
244 .iter()
245 .filter(|m| m.metadata.contains_key("reply_to"))
246 .map(|m| m.message_id),
247 );
248 }
249}
250
251async fn send_batch_and_commit(
252 publisher: &Arc<dyn MessagePublisher>,
253 messages: Vec<crate::CanonicalMessage>,
254 commit: BatchCommitFunc,
255 has_retry_middleware: bool,
256 err_tx: &Sender<anyhow::Error>,
257 commit_tasks: &mut JoinSet<()>,
258 scratch: &mut BatchScratch,
259) -> anyhow::Result<()> {
260 let batch_len = messages.len();
261 scratch.fill_from(&messages);
262
263 match publisher.send_batch(messages).await {
264 Ok(SentBatch::Ack) => {
265 for id in scratch.message_ids.iter() {
266 if scratch.request_ids.contains(id) {
267 warn!("Message {:032x} expected a reply (reply_to set), but publisher returned Ack. Response loop broken.", id);
268 }
269 }
270 let dispositions = scratch
271 .message_ids
272 .iter()
273 .map(|id| {
274 if scratch.request_ids.contains(id) {
275 MessageDisposition::Nack
276 } else {
277 MessageDisposition::Ack
278 }
279 })
280 .collect();
281 let err_tx = err_tx.clone();
282 commit_tasks.spawn(async move {
283 if let Err(e) = commit(dispositions).await {
284 error!("Commit failed: {}", e);
285 report_route_error(&err_tx, e, "Could not send commit error to main task");
286 }
287 });
288 Ok(())
289 }
290 Ok(SentBatch::Partial { responses, failed }) => {
291 let has_transient = failed.iter().any(|(_, e)| {
292 matches!(
293 e,
294 PublisherError::Retryable(_) | PublisherError::Connection(_)
295 )
296 });
297 if has_transient {
298 let (_, first_err) = failed
299 .iter()
300 .find(|(_, e)| {
301 matches!(
302 e,
303 PublisherError::Retryable(_) | PublisherError::Connection(_)
304 )
305 })
306 .expect("has_transient is true");
307 let err = anyhow::anyhow!(
308 "Transient error in batch send ({} messages failed). First error: {}",
309 failed.len(),
310 first_err
311 );
312 let dispositions = map_responses_to_dispositions(
313 &scratch.message_ids,
314 responses,
315 &failed,
316 &scratch.request_ids,
317 );
318 if let Err(commit_err) = commit(dispositions).await {
319 warn!("Commit after transient failure also failed: {}", commit_err);
320 }
321 if !has_retry_middleware {
322 return Err(err);
323 }
324 warn!(
325 "Transient error in batch, message(s) Nack'ed for re-delivery: {}",
326 err
327 );
328 return Ok(());
329 }
330
331 for (msg, e) in &failed {
332 error!(
333 "Dropping message (ID: {:032x}) due to non-retryable error: {}",
334 msg.message_id, e
335 );
336 }
337 let err_tx = err_tx.clone();
338 let dispositions = map_responses_to_dispositions(
339 &scratch.message_ids,
340 responses,
341 &failed,
342 &scratch.request_ids,
343 );
344 commit_tasks.spawn(async move {
345 if let Err(e) = commit(dispositions).await {
346 error!("Commit failed: {}", e);
347 report_route_error(&err_tx, e, "Could not send commit error to main task");
348 }
349 });
350 Ok(())
351 }
352 Err(e) => {
353 warn!("Publisher error, sending {} Nacks to commit", batch_len);
354 let nack_result = commit(vec![MessageDisposition::Nack; batch_len]).await;
355 debug!("Nack commit result: {:?}", nack_result);
356 Err(e.into())
357 }
358 }
359}
360
361impl Route {
362 pub fn new(input: Endpoint, output: Endpoint) -> Self {
368 Self {
369 input,
370 output,
371 ..Default::default()
372 }
373 }
374
375 pub fn get(name: &str) -> Option<Self> {
377 let registry = ROUTE_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
378 let map = recover_read_lock(registry, "route_registry");
379 map.get(name).map(|active| active.route.clone())
380 }
381
382 pub fn list() -> Vec<String> {
384 let registry = ROUTE_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
385 let map = recover_read_lock(registry, "route_registry");
386 map.keys().cloned().collect()
387 }
388
389 pub fn is_ref(&self) -> bool {
391 matches!(self.input.endpoint_type, EndpointType::Ref(_))
392 && !matches!(self.output.endpoint_type, EndpointType::Ref(_))
393 }
394
395 pub fn register_output_endpoint(&self, name: Option<&str>) -> Result<(), anyhow::Error> {
398 match name {
399 Some(name) => {
400 register_endpoint(name, self.output.clone());
401 }
402 None => {
403 if let EndpointType::Ref(name) = &self.input.endpoint_type {
404 register_endpoint(name, self.output.clone());
405 } else {
406 return Err(anyhow::anyhow!(
407 "No name and input is not a reference endpoint"
408 ));
409 }
410 }
411 };
412 Ok(())
413 }
414
415 pub async fn deploy(&self, name: &str) -> anyhow::Result<()> {
429 Self::stop(name).await;
430
431 let handle = self.run(name).await?;
432 let active = ActiveRoute {
433 route: self.clone(),
434 handle,
435 };
436
437 let registry = ROUTE_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
438 let mut map = recover_write_lock(registry, "route_registry");
439 map.insert(name.to_string(), active);
440 Ok(())
441 }
442
443 pub async fn stop(name: &str) -> bool {
448 let registry = ROUTE_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
449 let active_opt = {
450 let mut map = recover_write_lock(registry, "route_registry");
451 map.remove(name)
452 };
453
454 if let Some(active) = active_opt {
455 let handle = active.handle;
457
458 let _ = handle.0 .1.send(()).await;
460 handle.0 .1.close();
461
462 let mut join_handle = handle.0 .0;
464 tokio::select! {
465 res = &mut join_handle => {
466 let _ = res;
468 }
469 _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
470 join_handle.abort();
472 let _ = join_handle.await;
474 }
475 }
476
477 true
478 } else {
479 false
480 }
481 }
482
483 pub async fn create_publisher(&self) -> anyhow::Result<crate::Publisher> {
498 crate::Publisher::new(self.output.clone()).await
499 }
500
501 pub async fn connect_to_output(
504 &self,
505 name: &str,
506 ) -> anyhow::Result<Box<dyn crate::traits::MessageConsumer>> {
507 create_consumer_from_route(name, &self.output).await
508 }
509
510 pub fn check(
516 &self,
517 name: &str,
518 allowed_endpoints: Option<&[&str]>,
519 ) -> anyhow::Result<Vec<String>> {
520 self.options.validate()?;
521 if !self.options.allow_fault_injection {
522 check_fault_middleware_allowed(
523 &self.input,
524 name,
525 "input",
526 0,
527 &mut std::collections::HashSet::new(),
528 )?;
529 check_fault_middleware_allowed(
530 &self.output,
531 name,
532 "output",
533 0,
534 &mut std::collections::HashSet::new(),
535 )?;
536 }
537
538 let mut warnings = Vec::new();
539 warnings.extend(crate::endpoints::check_consumer(
540 name,
541 &self.input,
542 allowed_endpoints,
543 )?);
544 warnings.extend(crate::endpoints::check_publisher(
545 name,
546 &self.output,
547 allowed_endpoints,
548 )?);
549 Ok(warnings)
550 }
551
552 pub async fn run(&self, name_str: &str) -> anyhow::Result<RouteHandle> {
580 let warnings = self.check(name_str, None)?;
581 for warning in warnings {
582 tracing::warn!(route = name_str, "Configuration warning: {}", warning);
583 }
584 let startup_timeout = std::time::Duration::from_millis(self.options.startup_timeout_ms);
585 let reconnect_interval =
586 tokio::time::Duration::from_millis(self.options.reconnect_interval_ms);
587 let (shutdown_tx, shutdown_rx) = bounded(1);
588 let (ready_tx, ready_rx) = bounded(1);
589 let route = Arc::new(self.clone());
591 let name = Arc::new(name_str.to_string());
592
593 let handle = tokio::spawn(async move {
594 loop {
595 let route_arc = Arc::clone(&route);
596 let name_arc = Arc::clone(&name);
597 let (internal_shutdown_tx, internal_shutdown_rx) = bounded(1);
601 let ready_tx_clone = ready_tx.clone();
602
603 let mut run_task = tokio::spawn(async move {
605 route_arc
606 .run_until_err(&name_arc, Some(internal_shutdown_rx), Some(ready_tx_clone))
607 .await
608 });
609
610 select! {
611 _ = shutdown_rx.recv() => {
612 info!("Shutdown signal received for route '{}'.", name);
613 let _ = internal_shutdown_tx.send(()).await;
615 let _ = run_task.await;
617 break;
618 }
619 res = &mut run_task => {
620 match res {
621 Ok(Ok(should_continue)) if !should_continue => {
622 info!("Route '{}' completed gracefully. Shutting down.", name);
623 break;
624 }
625 Ok(Err(e)) => {
626 let is_permanent =
627 e.downcast_ref::<ProcessingError>().is_some_and(|pe| matches!(pe, ProcessingError::NonRetryable(_)))
628 || e.downcast_ref::<ConsumerError>().is_some_and(|ce| matches!(ce, ConsumerError::EndOfStream));
629
630 if is_permanent {
631 error!("Route '{}' failed with a permanent error: {}. Shutting down.", name, e);
632 break;
633 }
634
635 warn!(
636 "Route '{}' failed: {}. Reconnecting in {}ms...",
637 name,
638 e,
639 reconnect_interval.as_millis()
640 );
641 if !reconnect_interval.is_zero() {
642 tokio::time::sleep(reconnect_interval).await;
643 }
644 }
645 Err(e) => {
646 error!(
647 "Route '{}' task panicked: {}. Reconnecting in {}ms...",
648 name,
649 e,
650 reconnect_interval.as_millis()
651 );
652 if !reconnect_interval.is_zero() {
653 tokio::time::sleep(reconnect_interval).await;
654 }
655 }
656 _ => {} }
658 }
659 }
660 }
661 });
662
663 match tokio::time::timeout(startup_timeout, ready_rx.recv()).await {
664 Ok(Ok(_)) => Ok(RouteHandle((handle, shutdown_tx))),
665 _ => {
666 handle.abort();
667 Err(anyhow::anyhow!(
668 "Route '{}' failed to start within {}ms or encountered an error",
669 name_str,
670 startup_timeout.as_millis()
671 ))
672 }
673 }
674 }
675
676 pub async fn run_until_err(
678 &self,
679 name: &str,
680 shutdown_rx: Option<async_channel::Receiver<()>>,
681 ready_tx: Option<Sender<()>>,
682 ) -> anyhow::Result<bool> {
683 let (_internal_shutdown_tx, internal_shutdown_rx) = bounded(1);
684 let shutdown_rx = shutdown_rx.unwrap_or(internal_shutdown_rx);
685 if let Some(result) = crate::endpoints::try_run_fast_path_route(
686 self,
687 name,
688 shutdown_rx.clone(),
689 ready_tx.clone(),
690 )
691 .await
692 {
693 return result;
694 }
695 if self.options.concurrency == 1 {
696 self.run_sequentially(name, shutdown_rx, ready_tx).await
697 } else {
698 self.run_concurrently(name, shutdown_rx, ready_tx).await
699 }
700 }
701
702 async fn run_sequentially(
704 &self,
705 name: &str,
706 shutdown_rx: async_channel::Receiver<()>,
707 ready_tx: Option<Sender<()>>,
708 ) -> anyhow::Result<bool> {
709 let publisher = create_publisher_from_route(name, &self.output).await?;
710 let mut consumer = create_consumer_from_route(name, &self.input).await?;
711 if let Err(err) = run_publisher_connect_hook(name, &publisher).await {
712 run_publisher_disconnect_hook(name, &publisher).await;
713 return Err(err);
714 }
715 if let Err(err) = run_consumer_connect_hook(name, consumer.as_ref()).await {
716 run_consumer_disconnect_hook(name, consumer.as_ref()).await;
717 run_publisher_disconnect_hook(name, &publisher).await;
718 return Err(err);
719 }
720 let (err_tx, err_rx) = bounded(1);
721 let mut commit_tasks = JoinSet::new();
722
723 let (seq_tx, sequencer_handle) = spawn_sequencer(self.options.commit_concurrency_limit);
725 let mut seq_counter = 0u64;
726
727 if let Some(tx) = ready_tx {
728 let _ = tx.send(()).await;
729 }
730 let mut batch_scratch = BatchScratch::with_capacity(self.options.batch_size);
731 let has_retry_middleware = self.output.has_retry_middleware();
733 let run_result = loop {
734 select! {
735 Ok(err) = err_rx.recv() => break Err(err),
736
737 _ = shutdown_rx.recv() => {
738 info!("Shutdown signal received in sequential runner for route '{}'.", name);
739 break Ok(true); }
741 res = consumer.receive_batch(self.options.batch_size) => {
742 let received_batch = match res {
743 Ok(batch) => {
744 if batch.messages.is_empty() {
745 pause_after_empty_batch(self.options.empty_batch_delay_ms).await;
746 continue; }
748 batch
749 }
750 Err(ConsumerError::EndOfStream) => {
751 info!("Consumer for route '{}' reached end of stream. Shutting down.", name);
752 break Ok(false); }
754 Err(ConsumerError::Connection(e)) => {
755 break Err(e);
757 },
758 Err(ConsumerError::Gap { requested, base }) => {
759 break Err(anyhow::anyhow!("Consumer gap: requested offset {requested} but earliest available is {base}"));
761 }
762 };
763 debug!("Received a batch of {} messages sequentially", received_batch.messages.len());
764
765 let seq = seq_counter;
767 seq_counter += 1;
768 let commit = wrap_commit(received_batch.commit, seq, seq_tx.clone());
769 if let Err(err) = send_batch_and_commit(
770 &publisher,
771 received_batch.messages,
772 commit,
773 has_retry_middleware,
774 &err_tx,
775 &mut commit_tasks,
776 &mut batch_scratch,
777 )
778 .await
779 {
780 break Err(err);
781 }
782
783 tokio::task::yield_now().await;
784 }
785 }
786 };
787
788 drop(seq_tx);
789 loop {
791 select! {
792 res = err_rx.recv() => {
793 if let Ok(err) = res {
794 error!("Error reported during shutdown: {}", err);
795 }
796 }
797 res = commit_tasks.join_next() => {
798 if res.is_none() {
799 break;
800 }
801 }
802 }
803 }
804 drop(err_rx);
805 let _ = sequencer_handle.await;
806 run_consumer_disconnect_hook(name, consumer.as_ref()).await;
807 run_publisher_disconnect_hook(name, &publisher).await;
808 run_result
809 }
810
811 async fn run_concurrently(
813 &self,
814 name: &str,
815 shutdown_rx: async_channel::Receiver<()>,
816 ready_tx: Option<Sender<()>>,
817 ) -> anyhow::Result<bool> {
818 let publisher = create_publisher_from_route(name, &self.output).await?;
819 let mut consumer = create_consumer_from_route(name, &self.input).await?;
820 if let Err(err) = run_publisher_connect_hook(name, &publisher).await {
821 run_publisher_disconnect_hook(name, &publisher).await;
822 return Err(err);
823 }
824 if let Err(err) = run_consumer_connect_hook(name, consumer.as_ref()).await {
825 run_consumer_disconnect_hook(name, consumer.as_ref()).await;
826 run_publisher_disconnect_hook(name, &publisher).await;
827 return Err(err);
828 }
829 if let Some(tx) = ready_tx {
830 let _ = tx.send(()).await;
831 }
832 let (err_tx, err_rx) = bounded(1); let work_capacity = self.options.concurrency;
835 let (work_tx, work_rx) =
836 bounded::<(Vec<crate::CanonicalMessage>, BatchCommitFunc)>(work_capacity);
837 let (seq_tx, sequencer_handle) = spawn_sequencer(self.options.commit_concurrency_limit);
841
842 let mut join_set = JoinSet::new();
844 for i in 0..self.options.concurrency {
845 let work_rx_clone = work_rx.clone();
846 let publisher = Arc::clone(&publisher);
847 let err_tx = err_tx.clone();
848 let mut commit_tasks = JoinSet::new();
849 let has_retry_middleware = self.output.has_retry_middleware();
850 let batch_size = self.options.batch_size;
851 join_set.spawn(async move {
852 debug!("Starting worker {}", i);
853 let mut batch_scratch = BatchScratch::with_capacity(batch_size);
854 while let Ok((messages, commit_func)) = work_rx_clone.recv().await {
855 if let Err(err) = send_batch_and_commit(
856 &publisher,
857 messages,
858 commit_func,
859 has_retry_middleware,
860 &err_tx,
861 &mut commit_tasks,
862 &mut batch_scratch,
863 )
864 .await
865 {
866 error!("Worker failed to process message batch: {}", err);
867 report_route_error(&err_tx, err, "Could not send error to main task");
868 break;
869 }
870 tokio::task::yield_now().await;
871 }
872 while commit_tasks.join_next().await.is_some() {}
874 });
875 }
876
877 let mut seq_counter = 0u64;
878 let mut loop_error: Option<anyhow::Error> = None;
880 loop {
881 select! {
882 biased; Ok(err) = err_rx.recv() => {
885 error!("A worker reported a critical error. Shutting down route.");
886 loop_error = Some(err);
887 break;
888 }
889
890 Some(res) = join_set.join_next() => {
891 match res {
892 Ok(_) => {
893 error!("A worker task finished unexpectedly. Shutting down route.");
894 loop_error = Some(anyhow::anyhow!("Worker task finished unexpectedly"));
895 }
896 Err(e) => {
897 error!("A worker task panicked: {}. Shutting down route.", e);
898 loop_error = Some(e.into());
899 }
900 }
901 break;
902 }
903
904 _ = shutdown_rx.recv() => {
905 info!("Shutdown signal received in concurrent runner for route '{}'.", name);
906 break;
907 }
908
909 res = consumer.receive_batch(self.options.batch_size) => {
910 let (messages, commit) = match res {
911 Ok(batch) => {
912 if batch.messages.is_empty() {
913 pause_after_empty_batch(self.options.empty_batch_delay_ms).await;
914 continue; }
916 (batch.messages, batch.commit)
917 }
918 Err(ConsumerError::EndOfStream) => {
919 info!("Consumer for route '{}' reached end of stream. Shutting down.", name);
920 break; }
922 Err(ConsumerError::Connection(e)) => {
923 loop_error = Some(e);
925 break;
926 }
927 Err(ConsumerError::Gap { requested, base }) => {
928 loop_error = Some(ConsumerError::Gap { requested, base }.into());
930 break;
931 }
932 };
933 debug!("Received a batch of {} messages concurrently", messages.len());
934
935 let seq = seq_counter;
940 let wrapped_commit = wrap_commit(commit, seq, seq_tx.clone());
941
942 match work_tx.send((messages, wrapped_commit)).await {
943 Ok(()) => {
944 seq_counter += 1;
945 }
946 Err(e) => {
947 warn!("Work channel closed, cannot process more messages concurrently. Shutting down.");
948 let (msgs_back, wrapped_commit_back) = e.into_inner();
951 let _ = (wrapped_commit_back)(vec![crate::traits::MessageDisposition::Nack; msgs_back.len()]).await;
952 break;
953 }
954 }
955
956 tokio::task::yield_now().await;
957 }
958 }
959 }
960
961 drop(work_tx);
966 while join_set.join_next().await.is_some() {}
968
969 drop(seq_tx);
971 let _ = sequencer_handle.await;
972 run_consumer_disconnect_hook(name, consumer.as_ref()).await;
973 run_publisher_disconnect_hook(name, &publisher).await;
974
975 if let Some(err) = loop_error {
976 return Err(err);
977 }
978
979 if let Ok(err) = err_rx.try_recv() {
980 return Err(err);
981 }
982
983 Ok(shutdown_rx.is_empty())
986 }
987
988 pub fn with_options(mut self, options: RouteOptions) -> Self {
989 self.options = options;
990 self
991 }
992 pub fn with_concurrency(mut self, concurrency: usize) -> Self {
993 self.options.concurrency = concurrency.max(1);
994 self
995 }
996
997 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
998 self.options.batch_size = batch_size.max(1);
999 self
1000 }
1001 pub fn with_commit_concurrency_limit(mut self, limit: usize) -> Self {
1002 self.options.commit_concurrency_limit = limit.max(1);
1003 self
1004 }
1005
1006 pub fn with_startup_timeout_ms(mut self, timeout_ms: u64) -> Self {
1007 self.options.startup_timeout_ms = timeout_ms;
1008 self
1009 }
1010
1011 pub fn with_reconnect_interval_ms(mut self, interval_ms: u64) -> Self {
1012 self.options.reconnect_interval_ms = interval_ms;
1013 self
1014 }
1015
1016 pub fn with_empty_batch_delay_ms(mut self, delay_ms: u64) -> Self {
1017 self.options.empty_batch_delay_ms = delay_ms;
1018 self
1019 }
1020
1021 pub fn with_fault_injection(mut self, allow: bool) -> Self {
1022 self.options.allow_fault_injection = allow;
1023 self
1024 }
1025
1026 pub fn with_handler(mut self, handler: impl Handler + 'static) -> Self {
1027 self.output.handler = Some(Arc::new(handler));
1028 self
1029 }
1030
1031 pub fn add_handler<T, H, Args>(mut self, type_name: &str, handler: H) -> Self
1054 where
1055 T: DeserializeOwned + Send + Sync + 'static,
1056 H: crate::type_handler::IntoTypedHandler<T, Args>,
1057 Args: Send + Sync + 'static,
1058 {
1059 let handler = Arc::new(handler);
1061 let wrapper = move |msg: crate::CanonicalMessage| {
1062 let handler = handler.clone();
1063 async move {
1064 let data = msg.parse::<T>().map_err(|e| {
1065 HandlerError::NonRetryable(anyhow::anyhow!("Deserialization failed: {}", e))
1066 })?;
1067 let ctx = crate::MessageContext::from(msg);
1068 handler.call(data, ctx).await
1069 }
1070 };
1071 let wrapper = Arc::new(wrapper);
1072
1073 let prev_handler = self.output.handler.take();
1074
1075 let new_handler = if let Some(h) = prev_handler {
1076 if let Some(extended) = h.register_handler(type_name, wrapper.clone()) {
1077 extended
1078 } else {
1079 Arc::new(
1080 crate::type_handler::TypeHandler::new()
1081 .with_fallback(h)
1082 .add_handler(type_name, wrapper),
1083 )
1084 }
1085 } else {
1086 Arc::new(crate::type_handler::TypeHandler::new().add_handler(type_name, wrapper))
1087 };
1088
1089 self.output.handler = Some(new_handler);
1090 self
1091 }
1092 pub fn add_handlers<T, H, Args>(mut self, handlers: HashMap<&str, H>) -> Self
1093 where
1094 T: DeserializeOwned + Send + Sync + 'static,
1095 H: crate::type_handler::IntoTypedHandler<T, Args>,
1096 Args: Send + Sync + 'static,
1097 {
1098 for (type_name, handler) in handlers {
1099 self = self.add_handler(type_name, handler);
1100 }
1101 self
1102 }
1103}
1104
1105type SequencerItem = (
1106 Vec<MessageDisposition>,
1107 BatchCommitFunc,
1108 tokio::sync::oneshot::Sender<anyhow::Result<()>>,
1109);
1110
1111fn spawn_sequencer(buffer_size: usize) -> (Sender<(u64, SequencerItem)>, JoinHandle<()>) {
1112 let (seq_tx, seq_rx) = bounded::<(u64, SequencerItem)>(buffer_size);
1113 let sequencer_handle = tokio::spawn(async move {
1114 let mut buffer: BTreeMap<u64, SequencerItem> = BTreeMap::new();
1115 let mut next_seq = 0u64;
1116
1117 loop {
1118 if let Some((dispositions, commit_func, notify)) = buffer.remove(&next_seq) {
1124 let result = commit_func(dispositions).await;
1125 let _ = notify.send(result);
1126 next_seq += 1;
1127 tokio::task::yield_now().await;
1129 continue;
1130 }
1131
1132 match seq_rx.recv().await {
1134 Ok((seq, item)) => {
1135 if seq < next_seq {
1136 let (_, _, notify) = item;
1137 trace!(
1138 seq,
1139 next_seq,
1140 "Sequencer received late item (seq < next_seq)"
1141 );
1142 let _ = notify.send(Err(anyhow::anyhow!(
1143 "Sequencer received late item (seq {} < next_seq {})",
1144 seq,
1145 next_seq
1146 )));
1147 } else {
1148 buffer.insert(seq, item);
1149 }
1150 }
1151 Err(_) => {
1152 for (_, (_, _, notify)) in buffer {
1154 let _ = notify.send(Err(anyhow::anyhow!("Sequencer is shutting down")));
1155 }
1156 break;
1157 }
1158 }
1159 }
1160 });
1161 (seq_tx, sequencer_handle)
1162}
1163
1164fn wrap_commit(
1165 commit: BatchCommitFunc,
1166 seq: u64,
1167 seq_tx: Sender<(u64, SequencerItem)>,
1168) -> BatchCommitFunc {
1169 Box::new(move |dispositions| {
1170 Box::pin(async move {
1171 let (notify_tx, notify_rx) = tokio::sync::oneshot::channel();
1172 if seq_tx
1173 .send((seq, (dispositions, commit, notify_tx)))
1174 .await
1175 .is_ok()
1176 {
1177 match notify_rx.await {
1178 Ok(res) => res,
1179 Err(_) => Err(anyhow::anyhow!(
1180 "Sequencer dropped the commit channel unexpectedly"
1181 )),
1182 }
1183 } else {
1184 Err(anyhow::anyhow!(
1185 "Failed to send commit to sequencer, route is likely shutting down"
1186 ))
1187 }
1188 })
1189 })
1190}
1191
1192fn map_responses_to_dispositions(
1193 message_ids: &[u128],
1194 responses: Option<Vec<crate::CanonicalMessage>>,
1195 failed: &[(crate::CanonicalMessage, PublisherError)],
1196 request_ids: &std::collections::HashSet<u128>,
1197) -> Vec<MessageDisposition> {
1198 let len = message_ids.len();
1199 if responses.is_none() && failed.is_empty() && (len == 0 || request_ids.is_empty()) {
1200 return vec![MessageDisposition::Ack; len];
1201 }
1202
1203 if len == 1 {
1205 let id = message_ids[0];
1206 if !failed.is_empty() {
1207 return vec![MessageDisposition::Nack];
1208 }
1209 if let Some(mut resps) = responses {
1210 if let Some(resp) = resps.pop() {
1211 return vec![MessageDisposition::Reply(resp)];
1212 }
1213 }
1214 if request_ids.contains(&id) {
1215 error!("Message {:032x} expected a reply (reply_to set), but publisher returned Ack. Nacking to avoid committing a lost response.", id);
1216 return vec![MessageDisposition::Nack];
1217 }
1218 return vec![MessageDisposition::Ack];
1219 }
1220
1221 let mut dispositions = Vec::with_capacity(len);
1222 let mut failed_ids = std::collections::HashSet::with_capacity(failed.len());
1224 for (m, _) in failed {
1225 failed_ids.insert(m.message_id);
1226 }
1227
1228 let mut response_map: std::collections::HashMap<u128, crate::CanonicalMessage> = responses
1230 .unwrap_or_default()
1231 .into_iter()
1232 .map(|r| (r.message_id, r))
1233 .collect();
1234
1235 for id in message_ids {
1236 if failed_ids.contains(id) {
1237 dispositions.push(MessageDisposition::Nack);
1238 } else if let Some(resp) = response_map.remove(id) {
1239 dispositions.push(MessageDisposition::Reply(resp));
1241 } else if request_ids.contains(id) {
1242 error!("Message {:032x} expected a reply (reply_to set), but publisher returned Ack. Nacking to avoid committing a lost response.", id);
1243 dispositions.push(MessageDisposition::Nack);
1244 } else {
1245 dispositions.push(MessageDisposition::Ack);
1247 }
1248 }
1249 dispositions
1250}
1251
1252#[cfg(test)]
1253fn test_map_responses_to_dispositions_logic() {
1254 use crate::{traits::PublisherError, CanonicalMessage};
1255 use anyhow::anyhow;
1256
1257 let ids = vec![1, 2, 3, 4];
1258
1259 let mut resp1 = CanonicalMessage::from("resp1");
1260 resp1.message_id = 1;
1261 let mut resp4 = CanonicalMessage::from("resp4");
1262 resp4.message_id = 4;
1263
1264 let responses = Some(vec![
1265 resp1, resp4, ]);
1268
1269 let mut msg2 = CanonicalMessage::from("msg2");
1270 msg2.message_id = 2;
1271 let failed = vec![(msg2, PublisherError::NonRetryable(anyhow!("failed")))];
1272
1273 let mut request_ids = std::collections::HashSet::new();
1274 request_ids.insert(3); let dispositions = map_responses_to_dispositions(&ids, responses, &failed, &request_ids);
1276
1277 assert_eq!(dispositions.len(), 4);
1278 assert!(matches!(dispositions[0], MessageDisposition::Reply(_))); assert!(matches!(dispositions[1], MessageDisposition::Nack)); assert!(matches!(dispositions[2], MessageDisposition::Nack)); assert!(matches!(dispositions[3], MessageDisposition::Reply(_))); }
1283
1284pub fn get_route(name: &str) -> Option<Route> {
1285 Route::get(name)
1286}
1287
1288pub fn list_routes() -> Vec<String> {
1289 Route::list()
1290}
1291
1292pub async fn stop_route(name: &str) -> bool {
1293 Route::stop(name).await
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298 use super::*;
1299 use crate::models::{
1300 Endpoint, EndpointType, FaultMode, Middleware, RandomPanicMiddleware, RouteOptions,
1301 };
1302 use crate::traits::{
1303 CustomMiddlewareFactory, MessageConsumer, MessagePublisher, ReceivedBatch,
1304 };
1305 use crate::CanonicalMessage;
1306 use std::any::Any;
1307 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1308 use std::sync::Arc;
1309 use std::time::Duration;
1310
1311 #[test]
1312 fn test_route_check_rejects_zero_execution_options() {
1313 let route = Route::new(
1314 Endpoint::new_memory("zero_in", 10),
1315 Endpoint::new_memory("zero_out", 10),
1316 )
1317 .with_options(RouteOptions {
1318 concurrency: 0,
1319 batch_size: 1,
1320 commit_concurrency_limit: 1,
1321 ..Default::default()
1322 });
1323
1324 let err = route.check("zero_options", None).unwrap_err().to_string();
1325 assert!(err.contains("concurrency must be at least 1"));
1326 }
1327
1328 #[test]
1329 fn test_random_panic_requires_fault_injection_opt_in() {
1330 let fault_config = RandomPanicMiddleware {
1331 mode: FaultMode::Timeout,
1332 enabled: true,
1333 ..Default::default()
1334 };
1335 let input = Endpoint::new_memory("fault_policy_in", 10)
1336 .add_middleware(Middleware::RandomPanic(fault_config));
1337 let output = Endpoint::new_memory("fault_policy_out", 10);
1338 let route = Route::new(input, output);
1339
1340 let err = route.check("fault_policy", None).unwrap_err().to_string();
1341 assert!(err.contains("allow_fault_injection"));
1342 assert!(route
1343 .with_fault_injection(true)
1344 .check("fault_policy", None)
1345 .is_ok());
1346 }
1347
1348 #[derive(Debug, Default)]
1349 struct CommitObservation {
1350 completed: Mutex<Vec<u64>>,
1351 active: std::sync::atomic::AtomicUsize,
1352 max_active: std::sync::atomic::AtomicUsize,
1353 }
1354
1355 #[derive(Debug)]
1356 struct CommitTrackingMiddlewareFactory {
1357 observation: Arc<CommitObservation>,
1358 }
1359
1360 #[derive(Debug)]
1361 struct ReorderingPublisherMiddlewareFactory;
1362
1363 struct CommitTrackingConsumer {
1364 inner: Box<dyn MessageConsumer>,
1365 observation: Arc<CommitObservation>,
1366 }
1367
1368 struct ReorderingPublisher {
1369 inner: Box<dyn MessagePublisher>,
1370 }
1371
1372 #[async_trait::async_trait]
1373 impl CustomMiddlewareFactory for CommitTrackingMiddlewareFactory {
1374 async fn apply_consumer(
1375 &self,
1376 consumer: Box<dyn MessageConsumer>,
1377 _route_name: &str,
1378 _config: &serde_json::Value,
1379 ) -> anyhow::Result<Box<dyn MessageConsumer>> {
1380 Ok(Box::new(CommitTrackingConsumer {
1381 inner: consumer,
1382 observation: Arc::clone(&self.observation),
1383 }))
1384 }
1385 }
1386
1387 #[async_trait::async_trait]
1388 impl CustomMiddlewareFactory for ReorderingPublisherMiddlewareFactory {
1389 async fn apply_publisher(
1390 &self,
1391 publisher: Box<dyn MessagePublisher>,
1392 _route_name: &str,
1393 _config: &serde_json::Value,
1394 ) -> anyhow::Result<Box<dyn MessagePublisher>> {
1395 Ok(Box::new(ReorderingPublisher { inner: publisher }))
1396 }
1397 }
1398
1399 #[async_trait::async_trait]
1400 impl MessageConsumer for CommitTrackingConsumer {
1401 async fn receive_batch(
1402 &mut self,
1403 max_messages: usize,
1404 ) -> Result<ReceivedBatch, ConsumerError> {
1405 let mut batch = self.inner.receive_batch(max_messages).await?;
1406 let seq = batch
1407 .messages
1408 .first()
1409 .and_then(|message| message.get_payload_str().parse::<u64>().ok())
1410 .expect("tracking test expects numeric payloads");
1411 let original_commit = batch.commit;
1412 let observation = Arc::clone(&self.observation);
1413 batch.commit = Box::new(move |dispositions| {
1414 let observation = Arc::clone(&observation);
1415 Box::pin(async move {
1416 let active_now = observation.active.fetch_add(1, Ordering::SeqCst) + 1;
1417 let _ = observation.max_active.fetch_update(
1418 Ordering::SeqCst,
1419 Ordering::SeqCst,
1420 |current| (active_now > current).then_some(active_now),
1421 );
1422
1423 tokio::time::sleep(Duration::from_millis(20)).await;
1424 let result = original_commit(dispositions).await;
1425 observation.completed.lock().unwrap().push(seq);
1426 observation.active.fetch_sub(1, Ordering::SeqCst);
1427 result
1428 })
1429 });
1430 Ok(batch)
1431 }
1432
1433 fn as_any(&self) -> &dyn Any {
1434 self
1435 }
1436 }
1437
1438 #[async_trait::async_trait]
1439 impl MessagePublisher for ReorderingPublisher {
1440 async fn send_batch(
1441 &self,
1442 messages: Vec<crate::CanonicalMessage>,
1443 ) -> Result<SentBatch, PublisherError> {
1444 let seq = messages
1445 .first()
1446 .and_then(|message| message.get_payload_str().parse::<u64>().ok())
1447 .expect("tracking test expects numeric payloads");
1448 let delay_ms = 10 * (6u64.saturating_sub(seq.min(6)));
1449 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
1450 self.inner.send_batch(messages).await
1451 }
1452
1453 async fn send(&self, msg: crate::CanonicalMessage) -> Result<Sent, PublisherError> {
1454 self.inner.send(msg).await
1455 }
1456
1457 async fn flush(&self) -> anyhow::Result<()> {
1458 self.inner.flush().await
1459 }
1460
1461 fn as_any(&self) -> &dyn Any {
1462 self
1463 }
1464 }
1465
1466 async fn assert_route_commits_are_ordered_and_non_overlapping(concurrency: usize) {
1467 let unique_id = fast_uuid_v7::gen_id().to_string();
1468 let tracking_name = format!("track_commit_{}", unique_id);
1469 let reorder_name = format!("reorder_publish_{}", unique_id);
1470 let in_topic = format!("ordered_commit_in_{}", unique_id);
1471 let observation = Arc::new(CommitObservation::default());
1472
1473 register_middleware_factory(
1474 &tracking_name,
1475 Arc::new(CommitTrackingMiddlewareFactory {
1476 observation: Arc::clone(&observation),
1477 }),
1478 );
1479 register_middleware_factory(
1480 &reorder_name,
1481 Arc::new(ReorderingPublisherMiddlewareFactory),
1482 );
1483
1484 let input = Endpoint::new_memory(&in_topic, 32).add_middleware(Middleware::Custom {
1485 name: tracking_name,
1486 config: serde_json::Value::Null,
1487 });
1488 let output = Endpoint::new(EndpointType::Null).add_middleware(Middleware::Custom {
1489 name: reorder_name,
1490 config: serde_json::Value::Null,
1491 });
1492
1493 let route = Route::new(input.clone(), output)
1494 .with_concurrency(concurrency)
1495 .with_batch_size(1)
1496 .with_commit_concurrency_limit(1);
1497
1498 let input_channel = input.channel().unwrap();
1499 let messages = (0..6)
1500 .map(|seq| crate::CanonicalMessage::from(seq.to_string()))
1501 .collect();
1502 input_channel.fill_messages(messages).await.unwrap();
1503 input_channel.close();
1504
1505 tokio::time::timeout(
1506 std::time::Duration::from_secs(5),
1507 route.run_until_err("ordered_commit_regression", None, None),
1508 )
1509 .await
1510 .expect("Route should not hang while draining finite input")
1511 .expect("Route should complete without commit errors");
1512 assert_eq!(
1513 *observation.completed.lock().unwrap(),
1514 vec![0, 1, 2, 3, 4, 5],
1515 "Commit execution must follow receive order",
1516 );
1517 assert_eq!(
1518 observation.max_active.load(Ordering::SeqCst),
1519 1,
1520 "Broker-facing commit functions must never overlap",
1521 );
1522 }
1523
1524 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1525 async fn test_sequential_route_commits_are_ordered_and_non_overlapping() {
1526 assert_route_commits_are_ordered_and_non_overlapping(1).await;
1527 }
1528
1529 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1530 async fn test_concurrent_route_commits_are_ordered_and_non_overlapping() {
1531 assert_route_commits_are_ordered_and_non_overlapping(4).await;
1532 }
1533
1534 async fn run_consumer_fault_test(
1536 mode: FaultMode,
1537 expected_payload: &str,
1538 route_should_restart: bool,
1539 concurrency: usize,
1540 ) {
1541 let unique_suffix = fast_uuid_v7::gen_id().to_string();
1542 let in_topic = format!("fault_in_{}_{}_{}", mode, concurrency, unique_suffix);
1543 let out_topic = format!("fault_out_{}_{}_{}", mode, concurrency, unique_suffix);
1544
1545 let fault_config = RandomPanicMiddleware {
1546 mode,
1547 trigger_on_message: Some(1), enabled: true,
1549 ..Default::default()
1550 };
1551
1552 let input = Endpoint::new_memory(&in_topic, 10)
1553 .add_middleware(Middleware::RandomPanic(fault_config));
1554 let output = Endpoint::new_memory(&out_topic, 10);
1555
1556 let route_name = format!("fault_test_{}_{}", mode, concurrency);
1557 let route = Route::new(input.clone(), output.clone())
1558 .with_concurrency(concurrency)
1559 .with_fault_injection(true)
1560 .with_reconnect_interval_ms(100);
1561
1562 route
1564 .deploy(&route_name)
1565 .await
1566 .expect("Failed to deploy route");
1567 let input_ch = input.channel().unwrap();
1569 input_ch
1570 .send_message("persistent_msg".into())
1571 .await
1572 .unwrap();
1573
1574 if route_should_restart {
1575 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1577 } else {
1578 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1580 }
1581
1582 let mut verifier = route.connect_to_output("verifier").await.unwrap();
1584 let received = tokio::time::timeout(std::time::Duration::from_secs(10), verifier.receive())
1585 .await
1586 .expect("Timed out waiting for message after fault")
1587 .expect("Stream closed while waiting for message");
1588
1589 assert_eq!(received.message.get_payload_str(), expected_payload);
1590 (received.commit)(MessageDisposition::Ack).await.unwrap();
1591
1592 Route::stop(&route_name).await;
1594 }
1595
1596 async fn run_publisher_fault_test(
1598 mode: FaultMode,
1599 expected_payload: &str,
1600 route_should_restart: bool,
1601 ) {
1602 let unique_suffix = fast_uuid_v7::gen_id().to_string();
1603 let in_topic = format!("pub_fault_in_{}_{}", mode, unique_suffix);
1604 let out_topic = format!("pub_fault_out_{}_{}", mode, unique_suffix);
1605
1606 let fault_config = RandomPanicMiddleware {
1607 mode,
1608 trigger_on_message: Some(1), enabled: true,
1610 ..Default::default()
1611 };
1612
1613 let mut input = Endpoint::new_memory(&in_topic, 10);
1614 if let EndpointType::Memory(ref mut cfg) = input.endpoint_type {
1616 cfg.enable_nack = true;
1617 }
1618 let output = Endpoint::new_memory(&out_topic, 10)
1620 .add_middleware(Middleware::RandomPanic(fault_config));
1621
1622 let route_name = format!("pub_fault_test_{}", mode);
1623 let route = Route::new(input.clone(), output.clone())
1624 .with_fault_injection(true)
1625 .with_reconnect_interval_ms(100);
1626
1627 route
1628 .deploy(&route_name)
1629 .await
1630 .expect("Failed to deploy route");
1631
1632 let input_ch = input.channel().unwrap();
1633 input_ch
1634 .send_message(expected_payload.into())
1635 .await
1636 .unwrap();
1637
1638 if route_should_restart {
1639 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1640 } else {
1641 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1642 }
1643
1644 let mut verifier = route.connect_to_output("verifier").await.unwrap();
1645 let received = tokio::time::timeout(std::time::Duration::from_secs(10), verifier.receive())
1646 .await
1647 .expect("Timed out waiting for message after publisher fault")
1648 .expect("Stream closed");
1649
1650 assert_eq!(received.message.get_payload_str(), expected_payload);
1651 (received.commit)(MessageDisposition::Ack).await.unwrap();
1652
1653 Route::stop(&route_name).await;
1654 }
1655
1656 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1659 #[ignore = "Takes too much time for regular tests"]
1660 async fn test_route_recovery_from_faults() {
1661 let original_payload = "persistent_msg";
1662
1663 run_consumer_fault_test(FaultMode::Panic, original_payload, true, 2).await;
1665 run_consumer_fault_test(FaultMode::Disconnect, original_payload, true, 2).await;
1666 run_consumer_fault_test(FaultMode::Timeout, original_payload, true, 2).await;
1667 run_consumer_fault_test(FaultMode::Nack, original_payload, true, 2).await;
1668
1669 run_consumer_fault_test(FaultMode::JsonFormatError, "{invalid json}", false, 2).await;
1671 }
1672
1673 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1675 #[ignore = "Takes too much time for regular tests"]
1676 async fn test_route_recovery_from_faults_sequential() {
1677 let original_payload = "persistent_msg";
1678
1679 run_consumer_fault_test(FaultMode::Panic, original_payload, true, 1).await;
1681 run_consumer_fault_test(FaultMode::Disconnect, original_payload, true, 1).await;
1682 }
1683
1684 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1686 #[ignore = "Takes too much time for regular tests"]
1687 async fn test_publisher_recovery_from_faults() {
1688 let original_payload = "persistent_msg";
1689 run_publisher_fault_test(FaultMode::Disconnect, original_payload, true).await;
1694 run_publisher_fault_test(FaultMode::Timeout, original_payload, true).await;
1695 }
1696
1697 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1698 async fn test_route_sequencer_deadlock_fix() {
1699 let unique_id = fast_uuid_v7::gen_id().to_string();
1704 let factory_name = format!("fail_factory_{}", unique_id);
1705 let in_topic = format!("deadlock_in_{}", unique_id);
1706 let out_topic = format!("deadlock_out_{}", unique_id);
1707
1708 #[derive(Debug)]
1709 struct FailingMiddlewareFactory {
1710 fail_flag: Arc<AtomicBool>,
1711 }
1712
1713 #[async_trait::async_trait]
1714 impl CustomMiddlewareFactory for FailingMiddlewareFactory {
1715 async fn apply_publisher(
1716 &self,
1717 publisher: Box<dyn MessagePublisher>,
1718 _route_name: &str,
1719 _config: &serde_json::Value,
1720 ) -> anyhow::Result<Box<dyn MessagePublisher>> {
1721 Ok(Box::new(FailingPublisher {
1722 inner: publisher,
1723 fail_flag: self.fail_flag.clone(),
1724 }))
1725 }
1726 async fn apply_consumer(
1727 &self,
1728 consumer: Box<dyn MessageConsumer>,
1729 _route_name: &str,
1730 _config: &serde_json::Value,
1731 ) -> anyhow::Result<Box<dyn MessageConsumer>> {
1732 Ok(consumer)
1733 }
1734 }
1735
1736 struct FailingPublisher {
1737 inner: Box<dyn MessagePublisher>,
1738 fail_flag: Arc<AtomicBool>,
1739 }
1740
1741 #[async_trait::async_trait]
1742 impl MessagePublisher for FailingPublisher {
1743 async fn send_batch(
1744 &self,
1745 messages: Vec<crate::CanonicalMessage>,
1746 ) -> Result<SentBatch, PublisherError> {
1747 if self
1750 .fail_flag
1751 .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
1752 .is_ok()
1753 {
1754 return Err(PublisherError::Retryable(anyhow::anyhow!(
1755 "Simulated failure"
1756 )));
1757 }
1758 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1761 self.inner.send_batch(messages).await
1762 }
1763 async fn send(
1764 &self,
1765 msg: crate::CanonicalMessage,
1766 ) -> Result<crate::traits::Sent, PublisherError> {
1767 self.inner.send(msg).await
1768 }
1769 async fn flush(&self) -> anyhow::Result<()> {
1770 self.inner.flush().await
1771 }
1772 fn as_any(&self) -> &dyn Any {
1773 self
1774 }
1775 }
1776
1777 let fail_flag = Arc::new(AtomicBool::new(true));
1778 register_middleware_factory(
1779 &factory_name,
1780 Arc::new(FailingMiddlewareFactory {
1781 fail_flag: fail_flag.clone(),
1782 }),
1783 );
1784
1785 let input = Endpoint::new_memory(&in_topic, 100);
1786 let output = Endpoint::new_memory(&out_topic, 100).add_middleware(Middleware::Custom {
1787 name: factory_name,
1788 config: serde_json::Value::Null,
1789 });
1790
1791 let route = Route::new(input.clone(), output.clone())
1793 .with_concurrency(2)
1794 .with_batch_size(1);
1795
1796 let input_ch = input.channel().unwrap();
1798 input_ch.send_message("msg1".into()).await.unwrap();
1799 input_ch.send_message("msg2".into()).await.unwrap();
1800 input_ch.send_message("msg3".into()).await.unwrap();
1801
1802 let run_fut = async {
1805 let (_shutdown_tx, shutdown_rx) = async_channel::bounded(1);
1806 route
1807 .run_until_err("deadlock_test", Some(shutdown_rx), None)
1808 .await
1809 };
1810
1811 let result = tokio::time::timeout(std::time::Duration::from_secs(5), run_fut).await;
1813
1814 match result {
1815 Ok(res) => {
1816 assert!(
1818 res.is_err(),
1819 "Route should have failed with simulated error"
1820 );
1821 }
1822 Err(_) => {
1823 panic!("Route deadlocked! The sequencer likely didn't receive the Nack for the failed batch.");
1824 }
1825 }
1826 }
1827
1828 #[tokio::test]
1829 async fn test_sequencer_ordered_commits() {
1830 use std::time::Duration;
1831 use tokio::time::timeout;
1832
1833 let (seq_tx, sequencer_handle) = spawn_sequencer(16);
1834 let processed: Arc<Mutex<Vec<u64>>> = Arc::new(Mutex::new(Vec::new()));
1835
1836 let seqs = [2u64, 0u64, 1u64, 3u64];
1838 let mut receivers = Vec::new();
1839
1840 for seq in seqs.iter().cloned() {
1841 let (notify_tx, notify_rx) = tokio::sync::oneshot::channel();
1842 let processed_clone = processed.clone();
1843 let commit: BatchCommitFunc = Box::new(move |_dispositions| {
1844 let processed = processed_clone.clone();
1845 Box::pin(async move {
1846 tokio::time::sleep(Duration::from_millis(10 * seq)).await;
1848 processed.lock().unwrap().push(seq);
1849 Ok(())
1850 })
1851 });
1852 seq_tx
1853 .send((seq, (Vec::new(), commit, notify_tx)))
1854 .await
1855 .unwrap();
1856 receivers.push(notify_rx);
1857 }
1858
1859 for rx in receivers {
1861 let res = timeout(Duration::from_secs(2), rx)
1862 .await
1863 .expect("Sequencer notify timed out");
1864 assert!(res.is_ok(), "Sequencer reported an error on commit");
1865 assert!(res.unwrap().is_ok(), "Commit returned an error");
1866 }
1867
1868 drop(seq_tx);
1870 let _ = sequencer_handle.await;
1871
1872 let result = processed.lock().unwrap().clone();
1873 assert_eq!(
1874 result,
1875 vec![0u64, 1u64, 2u64, 3u64],
1876 "Sequencer must process commits in order"
1877 );
1878 }
1879
1880 #[tokio::test]
1881 async fn test_sequencer_shutdown_notifies_pending() {
1882 use std::time::Duration;
1883 use tokio::time::timeout;
1884
1885 let (seq_tx, sequencer_handle) = spawn_sequencer(8);
1886
1887 let (notify_tx1, notify_rx1) = tokio::sync::oneshot::channel();
1889 let (notify_tx2, notify_rx2) = tokio::sync::oneshot::channel();
1890
1891 let commit1: BatchCommitFunc = Box::new(|_dispositions| {
1892 Box::pin(async move {
1893 panic!("Commit should not be executed during shutdown drain");
1895 #[allow(unreachable_code)]
1896 Ok(())
1897 })
1898 });
1899
1900 let commit2: BatchCommitFunc = Box::new(|_dispositions| {
1901 Box::pin(async move {
1902 panic!("Commit should not be executed during shutdown drain");
1903 #[allow(unreachable_code)]
1904 Ok(())
1905 })
1906 });
1907
1908 seq_tx
1909 .send((1u64, (Vec::new(), commit1, notify_tx1)))
1910 .await
1911 .unwrap();
1912 seq_tx
1913 .send((2u64, (Vec::new(), commit2, notify_tx2)))
1914 .await
1915 .unwrap();
1916
1917 drop(seq_tx);
1919
1920 let r1 = timeout(Duration::from_secs(1), notify_rx1)
1922 .await
1923 .expect("Timeout waiting for notify_rx1")
1924 .expect("Sequencer closed notify channel");
1925 assert!(
1926 r1.is_err(),
1927 "Pending commit should receive Err on sequencer shutdown"
1928 );
1929
1930 let r2 = timeout(Duration::from_secs(1), notify_rx2)
1931 .await
1932 .expect("Timeout waiting for notify_rx2")
1933 .expect("Sequencer closed notify channel");
1934 assert!(
1935 r2.is_err(),
1936 "Pending commit should receive Err on sequencer shutdown"
1937 );
1938
1939 let _ = sequencer_handle.await;
1940 }
1941
1942 use crate::traits::{BoxFuture, CustomEndpointFactory, Sent};
1943 use std::sync::Mutex;
1944
1945 type ConsumerBehavior =
1946 Arc<Mutex<dyn FnMut() -> Result<Box<dyn MessageConsumer>, anyhow::Error> + Send + Sync>>;
1947 type PublisherBehavior =
1948 Arc<Mutex<dyn FnMut() -> Result<Box<dyn MessagePublisher>, anyhow::Error> + Send + Sync>>;
1949
1950 struct MockEndpointFactory {
1951 create_consumer_fail: bool,
1952 consumer_behavior: ConsumerBehavior,
1953 publisher_behavior: PublisherBehavior,
1954 }
1955
1956 impl std::fmt::Debug for MockEndpointFactory {
1957 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1958 f.debug_struct("MockEndpointFactory")
1959 .field("create_consumer_fail", &self.create_consumer_fail)
1960 .finish()
1961 }
1962 }
1963
1964 impl MockEndpointFactory {
1965 fn new() -> Self {
1966 Self {
1967 create_consumer_fail: false,
1968 consumer_behavior: Arc::new(Mutex::new(|| Err(anyhow::anyhow!("Not implemented")))),
1969 publisher_behavior: Arc::new(Mutex::new(|| {
1970 Ok(Box::new(NoOpPublisher) as Box<dyn MessagePublisher>)
1971 })),
1972 }
1973 }
1974 }
1975
1976 #[derive(Clone)]
1977 struct NoOpPublisher;
1978 #[async_trait::async_trait]
1979 impl MessagePublisher for NoOpPublisher {
1980 async fn send_batch(
1981 &self,
1982 _: Vec<crate::CanonicalMessage>,
1983 ) -> Result<SentBatch, PublisherError> {
1984 Ok(SentBatch::Ack)
1985 }
1986 async fn send(&self, _: crate::CanonicalMessage) -> Result<Sent, PublisherError> {
1987 Ok(Sent::Ack)
1988 }
1989 fn as_any(&self) -> &dyn Any {
1990 self
1991 }
1992 }
1993
1994 #[async_trait::async_trait]
1995 impl CustomEndpointFactory for MockEndpointFactory {
1996 async fn create_consumer(
1997 &self,
1998 _: &str,
1999 _: &serde_json::Value,
2000 ) -> anyhow::Result<Box<dyn MessageConsumer>> {
2001 if self.create_consumer_fail {
2002 return Err(anyhow::anyhow!("Endpoint unavailable"));
2003 }
2004 (self.consumer_behavior.lock().unwrap())()
2005 }
2006 async fn create_publisher(
2007 &self,
2008 _: &str,
2009 _: &serde_json::Value,
2010 ) -> anyhow::Result<Box<dyn MessagePublisher>> {
2011 (self.publisher_behavior.lock().unwrap())()
2012 }
2013 }
2014
2015 #[derive(Clone, Default)]
2016 struct HookState {
2017 consumer_connects: Arc<AtomicUsize>,
2018 consumer_disconnects: Arc<AtomicUsize>,
2019 publisher_connects: Arc<AtomicUsize>,
2020 publisher_disconnects: Arc<AtomicUsize>,
2021 shared_mutations: Arc<AtomicUsize>,
2022 fail_consumer_connect: Arc<AtomicBool>,
2023 fail_consumer_disconnect: Arc<AtomicBool>,
2024 fail_publisher_disconnect: Arc<AtomicBool>,
2025 }
2026
2027 struct HookConsumer {
2028 state: HookState,
2029 }
2030
2031 struct HookPublisher {
2032 state: HookState,
2033 }
2034
2035 #[async_trait::async_trait]
2036 impl MessageConsumer for HookConsumer {
2037 fn on_connect_hook(&self) -> Option<BoxFuture<'_, anyhow::Result<()>>> {
2038 Some(Box::pin(async move {
2039 self.state.consumer_connects.fetch_add(1, Ordering::SeqCst);
2040 self.state.shared_mutations.fetch_add(1, Ordering::SeqCst);
2041 if self.state.fail_consumer_connect.load(Ordering::SeqCst) {
2042 return Err(anyhow::anyhow!("consumer hook failed"));
2043 }
2044 Ok(())
2045 }))
2046 }
2047
2048 fn on_disconnect_hook(&self) -> Option<BoxFuture<'_, anyhow::Result<()>>> {
2049 Some(Box::pin(async move {
2050 self.state
2051 .consumer_disconnects
2052 .fetch_add(1, Ordering::SeqCst);
2053 if self.state.fail_consumer_disconnect.load(Ordering::SeqCst) {
2054 return Err(anyhow::anyhow!("consumer disconnect hook failed"));
2055 }
2056 Ok(())
2057 }))
2058 }
2059
2060 async fn receive_batch(&mut self, _max: usize) -> Result<ReceivedBatch, ConsumerError> {
2061 Err(ConsumerError::EndOfStream)
2062 }
2063
2064 fn as_any(&self) -> &dyn Any {
2065 self
2066 }
2067 }
2068
2069 #[async_trait::async_trait]
2070 impl MessagePublisher for HookPublisher {
2071 fn on_connect_hook(&self) -> Option<BoxFuture<'_, anyhow::Result<()>>> {
2072 Some(Box::pin(async move {
2073 self.state.publisher_connects.fetch_add(1, Ordering::SeqCst);
2074 self.state.shared_mutations.fetch_add(1, Ordering::SeqCst);
2075 Ok(())
2076 }))
2077 }
2078
2079 fn on_disconnect_hook(&self) -> Option<BoxFuture<'_, anyhow::Result<()>>> {
2080 Some(Box::pin(async move {
2081 self.state
2082 .publisher_disconnects
2083 .fetch_add(1, Ordering::SeqCst);
2084 if self.state.fail_publisher_disconnect.load(Ordering::SeqCst) {
2085 return Err(anyhow::anyhow!("publisher disconnect hook failed"));
2086 }
2087 Ok(())
2088 }))
2089 }
2090
2091 async fn send_batch(
2092 &self,
2093 _: Vec<crate::CanonicalMessage>,
2094 ) -> Result<SentBatch, PublisherError> {
2095 Ok(SentBatch::Ack)
2096 }
2097
2098 fn as_any(&self) -> &dyn Any {
2099 self
2100 }
2101 }
2102
2103 fn hook_route(state: HookState, concurrency: usize) -> Route {
2104 let unique_id = fast_uuid_v7::gen_id().to_string();
2105 let factory_name = format!("hooks_{}", unique_id);
2106 let mut factory = MockEndpointFactory::new();
2107
2108 let consumer_state = state.clone();
2109 factory.consumer_behavior = Arc::new(Mutex::new(move || {
2110 Ok(Box::new(HookConsumer {
2111 state: consumer_state.clone(),
2112 }) as Box<dyn MessageConsumer>)
2113 }));
2114
2115 let publisher_state = state;
2116 factory.publisher_behavior = Arc::new(Mutex::new(move || {
2117 Ok(Box::new(HookPublisher {
2118 state: publisher_state.clone(),
2119 }) as Box<dyn MessagePublisher>)
2120 }));
2121
2122 register_endpoint_factory(&factory_name, Arc::new(factory));
2123
2124 let input = Endpoint {
2125 endpoint_type: EndpointType::Custom {
2126 name: factory_name.clone(),
2127 config: serde_json::Value::Null,
2128 },
2129 middlewares: vec![],
2130 handler: None,
2131 };
2132 let output = Endpoint {
2133 endpoint_type: EndpointType::Custom {
2134 name: factory_name,
2135 config: serde_json::Value::Null,
2136 },
2137 middlewares: vec![],
2138 handler: None,
2139 };
2140 Route::new(input, output).with_concurrency(concurrency)
2141 }
2142
2143 #[tokio::test]
2144 async fn test_lifecycle_hooks_called_once_sequentially() {
2145 let state = HookState::default();
2146 let route = hook_route(state.clone(), 1);
2147
2148 let stopped_by_shutdown = route
2149 .run_until_err("test_lifecycle_sequential", None, None)
2150 .await
2151 .unwrap();
2152
2153 assert!(!stopped_by_shutdown);
2154 assert_eq!(state.consumer_connects.load(Ordering::SeqCst), 1);
2155 assert_eq!(state.consumer_disconnects.load(Ordering::SeqCst), 1);
2156 assert_eq!(state.publisher_connects.load(Ordering::SeqCst), 1);
2157 assert_eq!(state.publisher_disconnects.load(Ordering::SeqCst), 1);
2158 assert_eq!(state.shared_mutations.load(Ordering::SeqCst), 2);
2159 }
2160
2161 #[tokio::test]
2162 async fn test_lifecycle_hooks_called_once_concurrently() {
2163 let state = HookState::default();
2164 let route = hook_route(state.clone(), 4);
2165
2166 route
2167 .run_until_err("test_lifecycle_concurrent", None, None)
2168 .await
2169 .unwrap();
2170
2171 assert_eq!(state.consumer_connects.load(Ordering::SeqCst), 1);
2172 assert_eq!(state.consumer_disconnects.load(Ordering::SeqCst), 1);
2173 assert_eq!(state.publisher_connects.load(Ordering::SeqCst), 1);
2174 assert_eq!(state.publisher_disconnects.load(Ordering::SeqCst), 1);
2175 }
2176
2177 #[tokio::test]
2178 async fn test_lifecycle_on_connect_failure_stops_route() {
2179 let state = HookState::default();
2180 state.fail_consumer_connect.store(true, Ordering::SeqCst);
2181 let route = hook_route(state.clone(), 1);
2182
2183 let err = route
2184 .run_until_err("test_lifecycle_connect_failure", None, None)
2185 .await
2186 .unwrap_err();
2187
2188 assert!(err.to_string().contains("on_connect hook failed"));
2189 assert_eq!(state.publisher_connects.load(Ordering::SeqCst), 1);
2190 assert_eq!(state.consumer_connects.load(Ordering::SeqCst), 1);
2191 }
2192
2193 #[tokio::test]
2194 async fn test_lifecycle_on_disconnect_failure_does_not_stop_route() {
2195 let state = HookState::default();
2196 state.fail_consumer_disconnect.store(true, Ordering::SeqCst);
2197 state
2198 .fail_publisher_disconnect
2199 .store(true, Ordering::SeqCst);
2200 let route = hook_route(state.clone(), 1);
2201
2202 let stopped_by_shutdown = route
2203 .run_until_err("test_lifecycle_disconnect_failure", None, None)
2204 .await
2205 .unwrap();
2206
2207 assert!(!stopped_by_shutdown);
2208 assert_eq!(state.consumer_disconnects.load(Ordering::SeqCst), 1);
2209 assert_eq!(state.publisher_disconnects.load(Ordering::SeqCst), 1);
2210 }
2211
2212 #[tokio::test]
2213 async fn test_start_fails_on_unavailable_endpoint() {
2214 let unique_id = fast_uuid_v7::gen_id().to_string();
2216 let factory_name = format!("unavailable_{}", unique_id);
2217
2218 let factory = Arc::new(MockEndpointFactory {
2219 create_consumer_fail: true,
2220 ..MockEndpointFactory::new()
2221 });
2222 register_endpoint_factory(&factory_name, factory);
2223
2224 let input = Endpoint {
2225 endpoint_type: EndpointType::Custom {
2226 name: factory_name,
2227 config: serde_json::Value::Null,
2228 },
2229 middlewares: vec![],
2230 handler: None,
2231 };
2232 let output = Endpoint::new_memory("out", 10);
2233 let route = Route::new(input, output);
2234
2235 let result = route.run("test_start_fail").await;
2238 assert!(result.is_err());
2239 assert!(result.unwrap_err().to_string().contains("failed to start"));
2240 }
2241
2242 #[tokio::test]
2243 async fn test_reconnect_on_consumer_error() {
2244 let unique_id = fast_uuid_v7::gen_id().to_string();
2246 let factory_name = format!("reconnect_{}", unique_id);
2247
2248 let connection_attempts = Arc::new(AtomicUsize::new(0));
2250 let attempts_clone = connection_attempts.clone();
2251
2252 let consumer_logic = move || -> Result<Box<dyn MessageConsumer>, anyhow::Error> {
2253 let attempt = attempts_clone.fetch_add(1, Ordering::SeqCst);
2254
2255 struct FlakyConsumer {
2256 attempt: usize,
2257 }
2258 #[async_trait::async_trait]
2259 impl MessageConsumer for FlakyConsumer {
2260 async fn receive_batch(
2261 &mut self,
2262 _max: usize,
2263 ) -> Result<ReceivedBatch, ConsumerError> {
2264 if self.attempt == 0 {
2265 self.attempt = 999; Ok(ReceivedBatch {
2268 messages: vec![crate::CanonicalMessage::from("msg1")],
2269 commit: Box::new(|_| Box::pin(async { Ok(()) })),
2270 })
2271 } else if self.attempt == 999 {
2272 Err(ConsumerError::Connection(anyhow::anyhow!(
2274 "Connection dropped"
2275 )))
2276 } else {
2277 tokio::time::sleep(Duration::from_millis(100)).await;
2280 Ok(ReceivedBatch {
2281 messages: vec![crate::CanonicalMessage::from("msg2")],
2282 commit: Box::new(|_| Box::pin(async { Ok(()) })),
2283 })
2284 }
2285 }
2286 fn as_any(&self) -> &dyn Any {
2287 self
2288 }
2289 }
2290 Ok(Box::new(FlakyConsumer { attempt }))
2291 };
2292
2293 let mut factory = MockEndpointFactory::new();
2294 factory.consumer_behavior = Arc::new(Mutex::new(consumer_logic));
2295 register_endpoint_factory(&factory_name, Arc::new(factory));
2296
2297 let input = Endpoint {
2298 endpoint_type: EndpointType::Custom {
2299 name: factory_name,
2300 config: serde_json::Value::Null,
2301 },
2302 middlewares: vec![],
2303 handler: None,
2304 };
2305 let output = Endpoint::new_memory(&format!("out_{}", unique_id), 10);
2306 let route = Route::new(input, output.clone());
2307
2308 route.deploy("test_reconnect").await.unwrap();
2309
2310 let mut verifier = create_consumer_from_route("verifier", &output)
2312 .await
2313 .unwrap();
2314
2315 let msg1 = tokio::time::timeout(std::time::Duration::from_secs(10), verifier.receive())
2317 .await
2318 .expect("Timed out waiting for msg1")
2319 .unwrap();
2320 assert_eq!(msg1.message.get_payload_str(), "msg1");
2321
2322 let msg2 = tokio::time::timeout(std::time::Duration::from_secs(10), verifier.receive())
2325 .await
2326 .expect("Timed out waiting for msg2")
2327 .unwrap();
2328 assert_eq!(msg2.message.get_payload_str(), "msg2");
2329
2330 assert!(connection_attempts.load(Ordering::SeqCst) >= 2);
2331 Route::stop("test_reconnect").await;
2332 }
2333
2334 #[tokio::test]
2335 async fn test_non_retryable_handler_error_does_not_crash_route() {
2336 let unique_id = fast_uuid_v7::gen_id().to_string();
2337 let in_topic = format!("bad_input_in_{}", unique_id);
2338 let out_topic = format!("bad_input_out_{}", unique_id); let input = Endpoint::new_memory(&in_topic, 10);
2341 let output = Endpoint::new_memory(&out_topic, 10);
2342
2343 let handler = |msg: crate::CanonicalMessage| async move {
2345 if msg.get_payload_str() == "poison" {
2346 Err(HandlerError::NonRetryable(anyhow::anyhow!("Invalid input")))
2347 } else {
2348 Ok(crate::Handled::Publish(msg))
2349 }
2350 };
2351
2352 let route = Route::new(input.clone(), output).with_handler(handler);
2353 route.deploy("test_invalid_input").await.unwrap();
2354
2355 let input_ch = input.channel().unwrap();
2356 let out_channel = route.output.channel().unwrap();
2357
2358 input_ch.send_message("poison".into()).await.unwrap();
2360
2361 input_ch.send_message("valid".into()).await.unwrap();
2363
2364 let received = tokio::time::timeout(std::time::Duration::from_secs(5), async {
2366 loop {
2367 if let Some(msg) = out_channel.drain_messages().pop() {
2368 return msg;
2369 }
2370 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2371 }
2372 })
2373 .await
2374 .expect("Timed out waiting for valid message to be processed");
2375 assert_eq!(received.get_payload_str(), "valid");
2376 Route::stop("test_invalid_input").await;
2377 }
2378
2379 #[tokio::test(flavor = "multi_thread")]
2380 async fn test_dlq_and_retry_batch_integration() {
2381 use crate::models::{DeadLetterQueueMiddleware, Middleware, RetryMiddleware};
2382 use crate::traits::{MessagePublisher, PublisherError, SentBatch};
2383 use std::collections::HashMap;
2384 use std::sync::Mutex;
2385
2386 #[derive(Clone)]
2388 struct PartialFailPublisher {
2389 attempts: Arc<Mutex<HashMap<u128, usize>>>,
2390 }
2391
2392 #[async_trait::async_trait]
2393 impl MessagePublisher for PartialFailPublisher {
2394 async fn send_batch(
2395 &self,
2396 messages: Vec<CanonicalMessage>,
2397 ) -> Result<SentBatch, PublisherError> {
2398 let mut failed = Vec::new();
2399 let mut attempts = self.attempts.lock().unwrap();
2400
2401 for msg in messages {
2402 let msg_num: u32 = serde_json::from_slice::<serde_json::Value>(&msg.payload)
2403 .unwrap()["id"]
2404 .as_u64()
2405 .unwrap() as u32;
2406
2407 let attempt_count = attempts.entry(msg.message_id).or_insert(0);
2408 *attempt_count += 1;
2409
2410 if msg_num % 2 == 0 {
2411 failed.push((
2413 msg,
2414 PublisherError::Retryable(anyhow::anyhow!("simulated failure")),
2415 ));
2416 }
2417 }
2419
2420 if failed.is_empty() {
2421 Ok(SentBatch::Ack)
2422 } else {
2423 Ok(SentBatch::Partial {
2424 responses: None,
2425 failed,
2426 })
2427 }
2428 }
2429 async fn send(
2430 &self,
2431 _msg: CanonicalMessage,
2432 ) -> Result<crate::traits::Sent, PublisherError> {
2433 unimplemented!()
2434 }
2435 fn as_any(&self) -> &dyn Any {
2436 self
2437 }
2438 }
2439
2440 let in_topic = "batch_retry_dlq_in";
2442 let out_topic = "batch_retry_dlq_out";
2443 let dlq_topic = "batch_retry_dlq_dlq";
2444
2445 let input = Endpoint::new_memory(in_topic, 10);
2446 let dlq_endpoint = Endpoint::new_memory(dlq_topic, 10);
2447
2448 let mock_publisher = PartialFailPublisher {
2449 attempts: Arc::new(Mutex::new(HashMap::new())),
2450 };
2451
2452 let mut output_with_middlewares = Endpoint::new_memory(out_topic, 10);
2453 output_with_middlewares.middlewares = vec![
2454 Middleware::Retry(RetryMiddleware {
2455 max_attempts: 2,
2456 initial_interval_ms: 1,
2457 ..Default::default()
2458 }),
2459 Middleware::Dlq(Box::new(DeadLetterQueueMiddleware {
2460 endpoint: dlq_endpoint.clone(),
2461 })),
2462 ];
2463
2464 let route = Route::new(input.clone(), output_with_middlewares).with_batch_size(4);
2465 let final_publisher = crate::middleware::apply_middlewares_to_publisher(
2467 Box::new(mock_publisher.clone()),
2468 &route.output,
2469 "test_route",
2470 )
2471 .await
2472 .unwrap();
2473
2474 let (work_tx, work_rx) =
2477 async_channel::bounded::<(Vec<crate::CanonicalMessage>, BatchCommitFunc)>(1);
2478 let (seq_tx, _sequencer_handle) = spawn_sequencer(1);
2479
2480 tokio::spawn(async move {
2482 if let Ok((messages, commit)) = work_rx.recv().await {
2483 let batch_len = messages.len();
2484 match final_publisher.send_batch(messages).await {
2485 Ok(SentBatch::Ack) => {
2486 let _ = commit(vec![MessageDisposition::Ack; batch_len]).await;
2487 }
2488 Ok(SentBatch::Partial { failed, .. }) => {
2489 let dispositions = if failed.is_empty() {
2491 vec![MessageDisposition::Ack; batch_len]
2492 } else {
2493 vec![MessageDisposition::Nack; batch_len]
2496 };
2497 let _ = commit(dispositions).await;
2498 }
2499 Err(_) => {
2500 let _ = commit(vec![MessageDisposition::Nack; batch_len]).await;
2501 }
2502 }
2503 }
2504 });
2505
2506 let mut messages = Vec::new();
2508 for i in 1..=4 {
2509 messages.push(CanonicalMessage::from_json(serde_json::json!({"id": i})).unwrap());
2511 }
2512 let commit = wrap_commit(Box::new(|_| Box::pin(async { Ok(()) })), 0, seq_tx.clone());
2513 work_tx.send((messages, commit)).await.unwrap();
2514
2515 let dlq_channel = dlq_endpoint.channel().unwrap();
2517
2518 let start = std::time::Instant::now();
2519 while dlq_channel.len() < 2 {
2520 if start.elapsed() > std::time::Duration::from_secs(5) {
2521 break;
2522 }
2523 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2524 }
2525
2526 let dlq_msgs = dlq_channel.drain_messages();
2527
2528 assert_eq!(dlq_msgs.len(), 2, "Expected 2 messages to go to DLQ");
2529
2530 let dlq_ids: std::collections::HashSet<u32> = dlq_msgs
2531 .iter()
2532 .map(|m| {
2533 serde_json::from_slice::<serde_json::Value>(&m.payload).unwrap()["id"]
2534 .as_u64()
2535 .unwrap() as u32
2536 })
2537 .collect();
2538
2539 assert!(dlq_ids.contains(&2));
2540 assert!(dlq_ids.contains(&4));
2541
2542 let attempts = mock_publisher.attempts.lock().unwrap();
2544 assert_eq!(attempts.values().filter(|&&c| c == 2).count(), 2);
2546 assert_eq!(attempts.values().filter(|&&c| c == 1).count(), 2);
2548 }
2549
2550 #[tokio::test(flavor = "multi_thread")]
2551 async fn test_route_dlq_integration() {
2552 let unique_id = fast_uuid_v7::gen_id().to_string();
2559 let in_topic = format!("dlq_in_{}", unique_id);
2560 let out_topic = format!("dlq_out_{}", unique_id);
2561 let dlq_topic = format!("dlq_target_{}", unique_id);
2562 let input = Endpoint::new_memory(&in_topic, 10);
2563 let dlq_endpoint = Endpoint::new_memory(&dlq_topic, 10);
2564
2565 let mut output = Endpoint::new_memory(&out_topic, 10);
2566 output.middlewares = vec![
2567 Middleware::RandomPanic(RandomPanicMiddleware {
2569 mode: FaultMode::Timeout, trigger_on_message: None, enabled: true,
2572 ..Default::default()
2573 }),
2574 Middleware::Retry(crate::models::RetryMiddleware {
2576 max_attempts: 2,
2577 initial_interval_ms: 10,
2578 max_interval_ms: 100,
2579 multiplier: 1.0,
2580 }),
2581 Middleware::Dlq(Box::new(crate::models::DeadLetterQueueMiddleware {
2583 endpoint: dlq_endpoint.clone(),
2584 })),
2585 ];
2586
2587 let route = Route::new(input.clone(), output).with_fault_injection(true);
2588 route.deploy("test_dlq_integration").await.unwrap();
2589
2590 let input_ch = input.channel().unwrap();
2592 input_ch.send_message("fail_msg".into()).await.unwrap();
2593
2594 let dlq_ch = dlq_endpoint.channel().unwrap();
2599
2600 let received = tokio::time::timeout(std::time::Duration::from_secs(5), async {
2602 loop {
2603 let batch = dlq_ch.drain_messages();
2604 if !batch.is_empty() {
2605 return batch[0].clone();
2606 }
2607 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2608 }
2609 })
2610 .await
2611 .expect("Timed out waiting for DLQ");
2612
2613 assert_eq!(received.get_payload_str(), "fail_msg");
2614
2615 let out_ch_target = mq_bridge::endpoints::memory::get_or_create_channel(
2616 &mq_bridge::models::MemoryConfig::new(&out_topic, None),
2617 );
2618 assert!(out_ch_target.is_empty(), "Message should not reach target");
2619
2620 Route::stop("test_dlq_integration").await;
2621 }
2622
2623 #[tokio::test(flavor = "multi_thread")]
2624 async fn test_large_message_handling() {
2625 let unique_id = fast_uuid_v7::gen_id().to_string();
2626 let in_topic = format!("large_in_{}", unique_id);
2627 let out_topic = format!("large_out_{}", unique_id);
2628
2629 let input = Endpoint::new_memory(&in_topic, 5); let output = Endpoint::new_memory(&out_topic, 5);
2631
2632 let route = Route::new(input.clone(), output.clone());
2633 route.deploy("test_large_msg").await.unwrap();
2634
2635 let large_payload = vec![b'x'; 5 * 1024 * 1024]; let input_ch = input.channel().unwrap();
2637
2638 input_ch
2639 .send_message(large_payload.clone().into())
2640 .await
2641 .unwrap();
2642
2643 let mut verifier = route.connect_to_output("verifier").await.unwrap();
2644 let received = tokio::time::timeout(std::time::Duration::from_secs(10), verifier.receive())
2645 .await
2646 .expect("Timed out receiving large message")
2647 .unwrap();
2648
2649 assert_eq!(received.message.payload.len(), large_payload.len());
2650 assert_eq!(received.message.payload, large_payload.as_slice());
2651
2652 Route::stop("test_large_msg").await;
2653 }
2654
2655 #[test]
2656 fn test_map_responses_to_dispositions_unit() {
2657 test_map_responses_to_dispositions_logic();
2658 }
2659}