Skip to main content

mq_bridge/
route.rs

1//  mq-bridge
2//  © Copyright 2025, by Marco Mengelkoch
3//  Licensed under MIT License, see License file for more details
4//  git clone https://github.com/marcomq/mq-bridge
5
6use 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
24// Re-export extensions for backward compatibility and internal usage
25pub 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
129/// Registers a named endpoint that can be referenced by other endpoints using `ref: "name"`.
130/// This will overwrite any existing endpoint with the same name.
131pub 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
139/// Retrieves a registered endpoint by name.
140pub 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    /// Creates a new route with default concurrency (1) and batch size (1).
363    ///
364    /// # Arguments
365    /// * `input` - The input/source endpoint for the route
366    /// * `output` - The output/sink endpoint for the route
367    pub fn new(input: Endpoint, output: Endpoint) -> Self {
368        Self {
369            input,
370            output,
371            ..Default::default()
372        }
373    }
374
375    /// Retrieves a registered (and running) route by name.
376    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    /// Returns a list of all registered route names.
383    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    /// Returns true if the input is of type ref (and the output isn't)
390    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    /// Registers the route's output endpoint under the given name.
396    /// This allows other routes to reference this output using `ref: "name"`.
397    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    /// Registers the route and starts it.
416    /// If a route with the same name is already running, it will be stopped first.
417    ///    
418    /// # Examples
419    /// ```
420    /// use mq_bridge::{Route, models::Endpoint};
421    ///
422    /// let route = Route::new(Endpoint::new_memory("in", 10), Endpoint::new_memory("out", 10));
423    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
424    /// route.deploy("global_route").await.unwrap();
425    /// assert!(Route::get("global_route").is_some());
426    /// # });
427    /// ```
428    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    /// Stops a running route by name and removes it from the registry.
444    /// Waits up to 5 seconds for the route task to join; if the timeout elapses
445    /// the task is aborted and the implementation awaits the aborted handle to
446    /// ensure the background task has fully terminated before returning.
447    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            // Move the handle out so we can operate on its internals.
456            let handle = active.handle;
457
458            // Signal the route to stop and close the shutdown channel.
459            let _ = handle.0 .1.send(()).await;
460            handle.0 .1.close();
461
462            // Extract the JoinHandle so we can monitor and, if needed, abort it.
463            let mut join_handle = handle.0 .0;
464            tokio::select! {
465                res = &mut join_handle => {
466                    // The task finished naturally within the 5s window
467                    let _ = res;
468                }
469                _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
470                    // The 5s timer finished first - abort the task to ensure it doesn't linger.
471                    join_handle.abort();
472                    // Await the handle one last time to ensure the task has fully shut down.
473                    let _ = join_handle.await;
474                }
475            }
476
477            true
478        } else {
479            false
480        }
481    }
482
483    /// Creates a new Publisher configured for this route's output.
484    /// This is useful if you want to send messages to the same destination as this route.
485    ///
486    /// # Examples
487    ///
488    /// ```
489    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
490    /// use mq_bridge::{Route, models::Endpoint};
491    ///
492    /// let route = Route::new(Endpoint::new_memory("in", 10), Endpoint::new_memory("out", 10));
493    /// let publisher = route.create_publisher().await;
494    /// assert!(publisher.is_ok());
495    /// # });
496    /// ```
497    pub async fn create_publisher(&self) -> anyhow::Result<crate::Publisher> {
498        crate::Publisher::new(self.output.clone()).await
499    }
500
501    /// Creates a consumer connected to the route's output.
502    /// This is primarily useful for integration tests to verify messages reaching the destination.
503    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    /// Validates the route configuration, checking if endpoints are supported and correctly configured.
511    /// Core types like file, memory, and response are always supported.
512    /// # Arguments
513    /// * `name` - The name of the route
514    /// * `allowed_endpoints` - An optional list of allowed endpoint types
515    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    /// Runs the message processing route with concurrency, error handling, and graceful shutdown.
553    ///
554    /// This function spawns the necessary background tasks to process messages. It waits asynchronously
555    /// until the route is successfully initialized (i.e., connections are established) or until
556    /// a timeout occurs.
557    /// The name_str parameter is just used for logging and tracing.
558    ///
559    /// It returns a `JoinHandle` for the main route task and a `Sender` channel
560    /// that can be used to signal a graceful shutdown. The result is typically converted into a
561    /// [`RouteHandle`] for easier management.
562    ///
563    /// # Examples
564    ///
565    /// ```no_run
566    /// # use mq_bridge::{Route, route::RouteHandle, models::Endpoint};
567    /// # async fn example() -> anyhow::Result<()> {
568    /// let route = Route::new(Endpoint::new_memory("in", 10), Endpoint::new_memory("out", 10));
569    ///
570    /// // Start the route (blocks until initialized) and convert to RouteHandle
571    /// let handle: RouteHandle = route.run("my_route").await?.into();
572    ///
573    /// // Stop the route later
574    /// handle.stop().await;
575    /// handle.join().await?;
576    /// # Ok(())
577    /// # }
578    /// ```
579    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        // Use `Arc` so route/name clones are cheap (pointer copy) in the reconnect loop.
590        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                // Create a new, per-iteration internal shutdown channel.
598                // This avoids a race where both this loop and the inner task
599                // try to consume the same external shutdown signal.
600                let (internal_shutdown_tx, internal_shutdown_rx) = bounded(1);
601                let ready_tx_clone = ready_tx.clone();
602
603                // The actual route logic is in `run_until_err`.
604                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                        // Notify the inner task to shut down.
614                        let _ = internal_shutdown_tx.send(()).await;
615                        // Wait for the inner task to finish gracefully.
616                        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                            _ => {} // The route should continue running.
657                        }
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    /// The core logic of running the route, designed to be called within a reconnect loop.
677    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    /// A simplified, sequential runner for when concurrency is 1.
703    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        // Sequencer setup to ensure ordered commits even with parallel commit tasks
724        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        // Check if retry middleware is present on output
732        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); // Stopped by shutdown signal
740                }
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; // No messages, loop to select! again
747                            }
748                            batch
749                        }
750                        Err(ConsumerError::EndOfStream) => {
751                            info!("Consumer for route '{}' reached end of stream. Shutting down.", name);
752                            break Ok(false); // Graceful exit
753                        }
754                        Err(ConsumerError::Connection(e)) => {
755                            // Propagate error to trigger reconnect by the outer loop
756                            break Err(e);
757                        },
758                        Err(ConsumerError::Gap { requested, base }) => {
759                            // Propagate gap error to trigger reconnect by the outer loop
760                            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                    // Process the batch sequentially without spawning a new task
766                    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        // Drain errors while waiting for tasks to finish to prevent deadlocks and lost errors
790        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    /// The main concurrent runner for when concurrency > 1.
812    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); // For critical, route-stopping errors
833                                           // channel capacity is measured in batches, not messages
834        let work_capacity = self.options.concurrency;
835        let (work_tx, work_rx) =
836            bounded::<(Vec<crate::CanonicalMessage>, BatchCommitFunc)>(work_capacity);
837        // --- Ordered Commit Sequencer ---
838        // To prevent data loss with cumulative-ack brokers (Kafka/AMQP), commits must happen in order.
839        // We assign a sequence number to each batch and use a sequencer task to enforce order.
840        let (seq_tx, sequencer_handle) = spawn_sequencer(self.options.commit_concurrency_limit);
841
842        // --- Worker Pool ---
843        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                // Wait for all in-flight commits to complete
873                while commit_tasks.join_next().await.is_some() {}
874            });
875        }
876
877        let mut seq_counter = 0u64;
878        // Holds an error that caused the loop to break, to be returned after graceful shutdown.
879        let mut loop_error: Option<anyhow::Error> = None;
880        loop {
881            select! {
882                biased; // Prioritize checking for errors
883
884                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; // No messages, loop to select! again
915                            }
916                            (batch.messages, batch.commit)
917                        }
918                        Err(ConsumerError::EndOfStream) => {
919                            info!("Consumer for route '{}' reached end of stream. Shutting down.", name);
920                            break; // Graceful exit
921                        }
922                        Err(ConsumerError::Connection(e)) => {
923                            // Propagate error to trigger reconnect by the outer loop
924                            loop_error = Some(e);
925                            break;
926                        }
927                        Err(ConsumerError::Gap { requested, base }) => {
928                            // Propagate gap error to trigger reconnect by the outer loop
929                            loop_error = Some(ConsumerError::Gap { requested, base }.into());
930                            break;
931                        }
932                    };
933                    debug!("Received a batch of {} messages concurrently", messages.len());
934
935                    // Wrap the commit function to route it through the sequencer.
936                    // Only advance the sequence counter after we've successfully enqueued
937                    // the work item to avoid creating sequence gaps if the work channel
938                    // is closed while producing batches.
939                    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                            // Recover the moved tuple so we can invoke the wrapped commit
949                            // and resolve the batch with a NACK.
950                            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        // --- Graceful Shutdown ---
962        // Close the work channel so workers drain their current messages and exit the loop.
963        // This applies on both normal shutdown AND error paths, ensuring in-flight commits
964        // are not aborted mid-sequence.
965        drop(work_tx);
966        // Wait for all worker tasks to complete.
967        while join_set.join_next().await.is_some() {}
968
969        // Close sequencer
970        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        // Return true if shutdown was requested (channel is empty means it was closed/consumed),
984        // false if we reached end-of-stream naturally.
985        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    /// Registers a typed handler for the route.
1032    ///
1033    /// The handler can accept either:
1034    /// - `fn(T) -> Future<Output = Result<Handled, HandlerError>>`
1035    /// - `fn(T, MessageContext) -> Future<Output = Result<Handled, HandlerError>>`
1036    ///
1037    /// # Examples
1038    ///
1039    /// ```
1040    /// # use mq_bridge::{Route, models::Endpoint};
1041    /// # use serde::Deserialize;
1042    ///
1043    /// #[derive(Deserialize)]
1044    /// struct MyData { id: u32 }
1045    ///
1046    /// async fn my_handler(data: MyData) -> anyhow::Result<()> {
1047    ///     Ok(())
1048    /// }
1049    ///
1050    /// let route = Route::new(Endpoint::new_memory("in", 10), Endpoint::new_memory("out", 10))
1051    ///     .add_handler("my_type", my_handler);
1052    /// ```
1053    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        // Create the wrapper closure that handles deserialization and context extraction
1060        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 we have the next item in sequence, execute its commit directly.
1119            // Using a plain await (no select!) here is essential: if we raced a recv
1120            // against the commit future, a recv win would drop the commit future and
1121            // the notify sender, leaving the caller permanently blocked while next_seq
1122            // stays unadvanced — a deadlock.
1123            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                // Yield to allow other tasks to run, preventing busy-loop when buffer has many messages
1128                tokio::task::yield_now().await;
1129                continue;
1130            }
1131
1132            // Wait for the next item from any worker.
1133            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                    // seq_tx was dropped — drain and notify any remaining buffered commits.
1153                    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    // Fast path for single message batches (very common for high-concurrency low-batch setups)
1204    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    // Build failed_ids manually to avoid collect() overhead
1223    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    // Create a map from message_id to response message for efficient lookup.
1229    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            // If a response exists for this specific ID, use it.
1240            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            // Otherwise, it was a successful send that did not produce a response.
1246            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, // Corresponds to id 1
1266        resp4, // Corresponds to id 4
1267    ]);
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); // id 3 expects a reply but won't get one
1275    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(_))); // from responses
1279    assert!(matches!(dispositions[1], MessageDisposition::Nack)); // from failed
1280    assert!(matches!(dispositions[2], MessageDisposition::Nack)); // missing reply
1281    assert!(matches!(dispositions[3], MessageDisposition::Reply(_))); // from responses
1282}
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    // Helper function to run a fault injection test on the consumer side.
1535    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), // Panic on the first message
1548            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        // Start the route
1563        route
1564            .deploy(&route_name)
1565            .await
1566            .expect("Failed to deploy route");
1567        // Send a message. The consumer will inject a fault when it tries to receive it.
1568        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            // The route's worker will fail, then the supervisor will restart it.
1576            tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1577        } else {
1578            // Route doesn't restart, just wait a bit for the (faulty) message to pass through.
1579            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1580        }
1581
1582        // Verify the outcome.
1583        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        // Cleanup
1593        Route::stop(&route_name).await;
1594    }
1595
1596    // Helper function to run a fault injection test on the publisher side.
1597    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), // Trigger on the first message
1609            enabled: true,
1610            ..Default::default()
1611        };
1612
1613        let mut input = Endpoint::new_memory(&in_topic, 10);
1614        // Enable NACK on input so messages aren't lost when publisher crashes
1615        if let EndpointType::Memory(ref mut cfg) = input.endpoint_type {
1616            cfg.enable_nack = true;
1617        }
1618        // Apply fault middleware to output
1619        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    // No Docker. Ignored to keep default `cargo test` focused on the fast path.
1657    // Locally measured on 2026-06-21: this plus the sequential variant ran in ~2s.
1658    #[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        // Test with concurrency > 1
1664        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        // This fault replaces the message but does not restart the route.
1670        run_consumer_fault_test(FaultMode::JsonFormatError, "{invalid json}", false, 2).await;
1671    }
1672
1673    // No Docker. Run with `cargo test test_route_recovery_from_faults -- --ignored --nocapture`.
1674    #[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        // Test with concurrency = 1
1680        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    // No Docker. Locally measured on 2026-06-21 at ~1s.
1685    #[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        // Test publisher-side faults causing restart/retry.
1690        // `FaultMode::Panic` is not tested here because the `MemoryConsumer` used for input
1691        // does not support crash-safe at-least-once delivery. A panic in the publisher
1692        // worker would cause the in-flight message to be lost.
1693        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        // This test ensures that when a worker fails to send a batch (and thus drops the commit handle),
1700        // the sequencer doesn't deadlock waiting for that sequence number.
1701        // The fix ensures that even on failure, the commit function is called (with Nack) to fill the sequence gap.
1702
1703        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                // We want to fail one batch to trigger the error path in the worker.
1748                // We use compare_exchange to ensure only one failure happens.
1749                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                // Add a small delay for successful batches to ensure the failed one (if it created a gap)
1759                // would block the sequencer if the gap wasn't filled.
1760                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        // Concurrency > 1 is required to have multiple workers and potential out-of-order completion
1792        let route = Route::new(input.clone(), output.clone())
1793            .with_concurrency(2)
1794            .with_batch_size(1);
1795
1796        // Send messages
1797        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        // Run the route. It should fail eventually due to the simulated error,
1803        // but it MUST NOT deadlock.
1804        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        // If deadlock exists, this timeout will trigger.
1812        let result = tokio::time::timeout(std::time::Duration::from_secs(5), run_fut).await;
1813
1814        match result {
1815            Ok(res) => {
1816                // We expect an error because the publisher returns Err.
1817                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        // Send sequences out of order to ensure the sequencer enforces ordering.
1837        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                    // Simulate variable work durations
1847                    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        // Wait for all commits to complete (with timeout to catch deadlocks)
1860        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        // Close sender to allow sequencer task to exit and await it.
1869        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        // Prepare two pending items for sequences 1 and 2 while sequence 0 is missing.
1888        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                // Should not be executed because next_seq is missing (0)
1894                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        // Trigger shutdown of the sequencer by dropping the sender.
1918        drop(seq_tx);
1919
1920        // Sequencer should drain buffered items and reply with an error to the notifiers.
1921        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        // tokio::time::pause();
2215        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        // The route should fail to start because the input endpoint fails to create.
2236        // The run() method waits for a ready signal which never comes.
2237        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        // tokio::time::pause();
2245        let unique_id = fast_uuid_v7::gen_id().to_string();
2246        let factory_name = format!("reconnect_{}", unique_id);
2247
2248        // Shared state to track connection attempts
2249        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                        // First connection works for one batch, then fails
2266                        self.attempt = 999; // prevent infinite loop in this instance
2267                        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                        // Simulate connection drop
2273                        Err(ConsumerError::Connection(anyhow::anyhow!(
2274                            "Connection dropped"
2275                        )))
2276                    } else {
2277                        // Subsequent connections work
2278                        // Sleep a bit to prevent busy loop in test
2279                        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        // Wait for reconnection and messages
2311        let mut verifier = create_consumer_from_route("verifier", &output)
2312            .await
2313            .unwrap();
2314
2315        // Should receive msg1
2316        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        // Route encounters error, sleeps 5s (skipped by pause), reconnects.
2323        // Should receive msg2
2324        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); // Not used, but good practice
2339
2340        let input = Endpoint::new_memory(&in_topic, 10);
2341        let output = Endpoint::new_memory(&out_topic, 10);
2342
2343        // A handler that fails on specific input
2344        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        // 1. Send poison message
2359        input_ch.send_message("poison".into()).await.unwrap();
2360
2361        // 2. Send valid message
2362        input_ch.send_message("valid".into()).await.unwrap();
2363
2364        // 3. Verify the valid message was processed and published
2365        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        // Mock publisher that fails messages with even-numbered IDs
2387        #[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                        // Fail even numbers
2412                        failed.push((
2413                            msg,
2414                            PublisherError::Retryable(anyhow::anyhow!("simulated failure")),
2415                        ));
2416                    }
2417                    // Odd numbers succeed implicitly by not being in `failed`
2418                }
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        // 1. Setup
2441        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        // Inject the mock publisher into the route's output
2466        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        // We need a way to run the route with our mocked publisher.
2475        // The simplest way is to manually drive the core logic.
2476        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        // Spawn a worker to process one batch
2481        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                        // In a real route, we'd map responses, but here we just care about failure.
2490                        let dispositions = if failed.is_empty() {
2491                            vec![MessageDisposition::Ack; batch_len]
2492                        } else {
2493                            // This is a simplification for the test. A real implementation
2494                            // would map dispositions based on message IDs.
2495                            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        // 2. Send a batch of messages
2507        let mut messages = Vec::new();
2508        for i in 1..=4 {
2509            // 1 (ok), 2 (fail), 3 (ok), 4 (fail)
2510            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        // 3. Verify
2516        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        // Verify retry attempts
2543        let attempts = mock_publisher.attempts.lock().unwrap();
2544        // Messages 2 and 4 should be tried `max_attempts` times.
2545        assert_eq!(attempts.values().filter(|&&c| c == 2).count(), 2);
2546        // Messages 1 and 3 should be tried once.
2547        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        // Setup: Input -> [Panic(Disconnect) -> Retry -> DLQ] -> Output
2553        // Panic(Disconnect) simulates transient failure.
2554        // Retry handles it up to N times.
2555        // If max attempts reached, DLQ catches it.
2556        // Note: Middleware application order is [Panic, Retry, DLQ] in list to wrap as DLQ(Retry(Panic(Endpoint))).
2557
2558        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            // Inner-most: Fail always
2568            Middleware::RandomPanic(RandomPanicMiddleware {
2569                mode: FaultMode::Timeout, // Returns Retryable error, does NOT cause route restart
2570                trigger_on_message: None, // Fail always
2571                enabled: true,
2572                ..Default::default()
2573            }),
2574            // Middle: Retry
2575            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            // Outer-most: DLQ
2582            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        // Send message
2591        let input_ch = input.channel().unwrap();
2592        input_ch.send_message("fail_msg".into()).await.unwrap();
2593
2594        // Verify:
2595        // 1. Output channel is empty (msg failed to go there)
2596        // 2. DLQ channel has message
2597
2598        let dlq_ch = dlq_endpoint.channel().unwrap();
2599
2600        // Wait for DLQ
2601        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); // Small capacity
2630        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]; // 5MB
2636        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}