Skip to main content

mq_bridge/endpoints/memory/
endpoint.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
5use super::memory_transport::MemoryTransport;
6use super::transport::{TransportChannel, TransportUrl};
7use crate::canonical_message::tracing_support::LazyMessageIds;
8use crate::event_store::{
9    event_store_exists, get_or_create_event_store, EventStore, EventStoreConsumer,
10};
11use crate::models::MemoryConfig;
12use crate::traits::{
13    BatchCommitFunc, BoxFuture, ConsumerError, EndpointStatus, MessageConsumer, MessageDisposition,
14    MessagePublisher, PublisherError, Received, ReceivedBatch, Sent, SentBatch,
15};
16use crate::CanonicalMessage;
17use anyhow::anyhow;
18use async_channel::{bounded, Receiver, Sender};
19use async_trait::async_trait;
20use once_cell::sync::Lazy;
21use std::any::Any;
22use std::collections::HashMap;
23use std::fmt;
24use std::sync::{Arc, Mutex};
25use tokio::sync::oneshot;
26use tracing::{info, trace, warn};
27
28#[cfg(unix)]
29use super::ipc_unix::UnixIpcTransport;
30#[cfg(windows)]
31use super::ipc_windows::WindowsIpcTransport;
32
33/// A map to hold memory channels for the duration of the bridge setup.
34/// This allows a consumer and publisher in different routes to connect to the same in-memory topic.
35static RUNTIME_MEMORY_CHANNELS: Lazy<Mutex<HashMap<String, MemoryChannel>>> =
36    Lazy::new(|| Mutex::new(HashMap::new()));
37
38/// A map to hold memory response channels.
39static RUNTIME_RESPONSE_CHANNELS: Lazy<Mutex<HashMap<String, MemoryResponseChannel>>> =
40    Lazy::new(|| Mutex::new(HashMap::new()));
41
42/// A shareable, thread-safe, in-memory channel for testing.
43///
44/// This struct holds the sender and receiver for an in-memory queue.
45/// It can be cloned and shared between your test code and the bridge's endpoints. It transports batches of messages.
46#[derive(Debug, Clone)]
47pub struct MemoryChannel {
48    pub sender: Sender<Vec<CanonicalMessage>>,
49    pub receiver: Receiver<Vec<CanonicalMessage>>,
50}
51
52impl MemoryChannel {
53    /// Creates a new batch channel with a specified capacity.
54    pub fn new(capacity: usize) -> Self {
55        let (sender, receiver) = bounded(capacity);
56        Self { sender, receiver }
57    }
58
59    /// Helper function for tests to easily send a message to the channel.
60    pub async fn send_message(&self, message: CanonicalMessage) -> anyhow::Result<()> {
61        self.sender.send(vec![message]).await?;
62        tracing::debug!("Message sent to memory {} channel", self.sender.len());
63        Ok(())
64    }
65
66    /// Helper function for tests to easily fill in messages.
67    pub async fn fill_messages(&self, messages: Vec<CanonicalMessage>) -> anyhow::Result<()> {
68        // Send the entire vector as a single batch.
69        self.sender
70            .send(messages)
71            .await
72            .map_err(|e| anyhow!("Memory channel was closed while filling messages: {}", e))?;
73        Ok(())
74    }
75
76    /// Closes the sender part of the channel.
77    pub fn close(&self) {
78        self.sender.close();
79    }
80
81    /// Helper function for tests to drain all messages from the channel.
82    pub fn drain_messages(&self) -> Vec<CanonicalMessage> {
83        let mut messages = Vec::new();
84        // Drain all batches from the channel and flatten them into a single Vec.
85        while let Ok(batch) = self.receiver.try_recv() {
86            messages.extend(batch);
87        }
88        messages
89    }
90
91    /// Returns the number of bulk messages in the channel.
92    pub fn len(&self) -> usize {
93        self.receiver.len()
94    }
95
96    /// Returns the number of messages currently in the channel.
97    pub fn is_empty(&self) -> bool {
98        self.receiver.is_empty()
99    }
100}
101
102/// A shareable, thread-safe, in-memory channel for responses.
103#[derive(Debug, Clone)]
104pub struct MemoryResponseChannel {
105    pub sender: Sender<CanonicalMessage>,
106    pub receiver: Receiver<CanonicalMessage>,
107    waiters: Arc<tokio::sync::Mutex<HashMap<String, oneshot::Sender<CanonicalMessage>>>>,
108}
109
110impl MemoryResponseChannel {
111    pub fn new(capacity: usize) -> Self {
112        let (sender, receiver) = bounded(capacity);
113        Self {
114            sender,
115            receiver,
116            waiters: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
117        }
118    }
119
120    pub fn close(&self) {
121        self.sender.close();
122    }
123
124    pub fn len(&self) -> usize {
125        self.receiver.len()
126    }
127
128    pub fn is_empty(&self) -> bool {
129        self.receiver.is_empty()
130    }
131
132    pub async fn wait_for_response(&self) -> anyhow::Result<CanonicalMessage> {
133        self.receiver
134            .recv()
135            .await
136            .map_err(|e| anyhow!("Error receiving response: {}", e))
137    }
138
139    pub async fn register_waiter(
140        &self,
141        correlation_id: &str,
142        sender: oneshot::Sender<CanonicalMessage>,
143    ) -> anyhow::Result<()> {
144        let mut waiters = self.waiters.lock().await;
145        if waiters.contains_key(correlation_id) {
146            return Err(anyhow!(
147                "Correlation ID {} already registered",
148                correlation_id
149            ));
150        }
151        waiters.insert(correlation_id.to_string(), sender);
152        Ok(())
153    }
154
155    pub async fn remove_waiter(
156        &self,
157        correlation_id: &str,
158    ) -> Option<oneshot::Sender<CanonicalMessage>> {
159        self.waiters.lock().await.remove(correlation_id)
160    }
161}
162
163/// Gets a shared `MemoryChannel` for a given topic, creating it if it doesn't exist.
164pub fn get_or_create_channel(config: &MemoryConfig) -> MemoryChannel {
165    let topic = memory_namespace(config).unwrap_or_else(|_| config.topic.clone());
166    let mut channels = RUNTIME_MEMORY_CHANNELS.lock().unwrap();
167    channels
168        .entry(topic.clone()) // Use the HashMap's entry API
169        .or_insert_with(|| {
170            info!(topic = %topic, "Creating new runtime memory channel");
171            MemoryChannel::new(config.capacity.unwrap_or(100))
172        })
173        .clone()
174}
175
176/// Gets a shared `MemoryResponseChannel` for a given topic, creating it if it doesn't exist.
177pub fn get_or_create_response_channel(topic: &str) -> MemoryResponseChannel {
178    let mut channels = RUNTIME_RESPONSE_CHANNELS.lock().unwrap();
179    channels
180        .entry(topic.to_string())
181        .or_insert_with(|| {
182            info!(topic = %topic, "Creating new runtime memory response channel");
183            MemoryResponseChannel::new(100)
184        })
185        .clone()
186}
187
188fn memory_channel_exists(topic: &str) -> bool {
189    let channels = RUNTIME_MEMORY_CHANNELS.lock().unwrap();
190    channels.contains_key(topic)
191}
192
193fn resolved_transport(config: &MemoryConfig) -> anyhow::Result<TransportUrl> {
194    let identifier = config.get_transport_identifier()?;
195    TransportUrl::parse(&identifier)
196}
197
198fn memory_namespace(config: &MemoryConfig) -> anyhow::Result<String> {
199    match resolved_transport(config)? {
200        TransportUrl::Memory { namespace } => Ok(namespace),
201        other => Err(anyhow!(
202            "MemoryConfig uses IPC transport '{}', which requires async endpoint construction",
203            other.display_name()
204        )),
205    }
206}
207
208fn normalized_memory_config(config: &MemoryConfig) -> anyhow::Result<MemoryConfig> {
209    let mut normalized = config.clone();
210    normalized.topic = memory_namespace(config)?;
211    normalized.url = None;
212    Ok(normalized.with_smart_defaults())
213}
214
215/// Create a transport based on the URL scheme
216#[allow(dead_code)]
217async fn create_transport_from_url(
218    url: &TransportUrl,
219    capacity: usize,
220    is_server: bool,
221) -> anyhow::Result<Arc<dyn TransportChannel>> {
222    match url {
223        TransportUrl::Memory { namespace } => {
224            info!(namespace = %namespace, "Creating in-process memory transport");
225            Ok(Arc::new(MemoryTransport::new(capacity)))
226        }
227        #[cfg(unix)]
228        TransportUrl::Unix { path } => {
229            if is_server {
230                info!(path = %path, "Creating Unix IPC server transport");
231                let transport = UnixIpcTransport::new_server(path, capacity).await?;
232                Ok(Arc::new(transport))
233            } else {
234                info!(path = %path, "Creating Unix IPC client transport");
235                let transport = UnixIpcTransport::new_client(path, capacity).await?;
236                Ok(Arc::new(transport))
237            }
238        }
239        #[cfg(windows)]
240        TransportUrl::Pipe { name } => {
241            if is_server {
242                info!(pipe = %name, "Creating Windows Named Pipe server transport");
243                let transport = WindowsIpcTransport::new_server(name, capacity).await?;
244                Ok(Arc::new(transport))
245            } else {
246                info!(pipe = %name, "Creating Windows Named Pipe client transport");
247                let transport = WindowsIpcTransport::new_client(name, capacity).await?;
248                Ok(Arc::new(transport))
249            }
250        }
251        #[cfg(not(any(unix, windows)))]
252        _ => Err(anyhow!("IPC transport not supported on this platform")),
253    }
254}
255
256/// A sink that sends messages to an in-memory channel.
257#[derive(Debug, Clone)]
258pub struct MemoryPublisher {
259    topic: String,
260    backend: PublisherBackend,
261    request_reply: bool,
262    request_timeout: std::time::Duration,
263}
264
265#[derive(Clone)]
266enum PublisherBackend {
267    Queue(Sender<Vec<CanonicalMessage>>),
268    Log(Arc<EventStore>),
269    Transport(Arc<dyn TransportChannel>),
270}
271
272impl fmt::Debug for PublisherBackend {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        match self {
275            Self::Queue(_) => f.write_str("Queue(..)"),
276            Self::Log(_) => f.write_str("Log(..)"),
277            Self::Transport(_) => f.write_str("Transport(..)"),
278        }
279    }
280}
281
282impl MemoryPublisher {
283    pub fn new(config: &MemoryConfig) -> anyhow::Result<Self> {
284        let config = normalized_memory_config(config)?;
285        let channel_exists = memory_channel_exists(&config.topic);
286        let store_exists = event_store_exists(&config.topic);
287
288        let backend = if config.subscribe_mode {
289            if channel_exists {
290                return Err(anyhow!("Topic '{}' is already active as a Queue (MemoryChannel), but Subscriber mode (EventStore) was requested.", config.topic));
291            }
292            let store = get_or_create_event_store(&config.topic);
293            PublisherBackend::Log(store)
294        } else if store_exists {
295            // Adaptive behavior: If an EventStore already exists, we publish to it even if
296            // subscribe_mode wasn't explicitly set. This prevents split-brain scenarios.
297            tracing::debug!(topic = %config.topic, "Adapting publisher to Log mode due to existing EventStore");
298            let store = get_or_create_event_store(&config.topic);
299            PublisherBackend::Log(store)
300        } else {
301            let channel = get_or_create_channel(&config);
302            PublisherBackend::Queue(channel.sender)
303        };
304
305        Ok(Self {
306            topic: config.topic.clone(),
307            backend,
308            request_reply: config.request_reply,
309            request_timeout: std::time::Duration::from_millis(
310                config.request_timeout_ms.unwrap_or(30000),
311            ),
312        })
313    }
314
315    pub async fn new_async(config: &MemoryConfig) -> anyhow::Result<Self> {
316        let url = resolved_transport(config)?;
317        match &url {
318            TransportUrl::Memory { .. } => Self::new(config),
319            _ => {
320                if config.subscribe_mode {
321                    return Err(anyhow!(
322                        "IPC memory publishers do not support subscribe_mode"
323                    ));
324                }
325                if config.request_reply {
326                    return Err(anyhow!(
327                        "IPC memory publishers do not yet support request_reply"
328                    ));
329                }
330                let capacity = config.capacity.unwrap_or(100);
331                let transport = create_transport_from_url(&url, capacity, false).await?;
332                Ok(Self {
333                    topic: url.display_name(),
334                    backend: PublisherBackend::Transport(transport),
335                    request_reply: false,
336                    request_timeout: std::time::Duration::from_millis(
337                        config.request_timeout_ms.unwrap_or(30000),
338                    ),
339                })
340            }
341        }
342    }
343
344    /// Creates a new local memory publisher.
345    ///
346    /// This method creates a new in-memory publisher with the specified topic and capacity.
347    /// The publisher will send messages to the in-memory channel for the specified topic.
348    pub fn new_local(topic: &str, capacity: usize) -> Self {
349        Self::new(&MemoryConfig {
350            topic: topic.to_string(),
351            capacity: Some(capacity),
352            ..Default::default()
353        })
354        .expect("Failed to create local memory publisher")
355    }
356
357    /// Note: This helper is primarily for tests expecting a Queue.    
358    /// If used on a broadcast publisher, it will create a separate Queue channel.
359    pub fn channel(&self) -> MemoryChannel {
360        get_or_create_channel(&MemoryConfig {
361            topic: self.topic.clone(),
362            capacity: None,
363            ..Default::default()
364        })
365    }
366}
367
368#[async_trait]
369impl MessagePublisher for MemoryPublisher {
370    async fn send(&self, mut message: CanonicalMessage) -> Result<Sent, PublisherError> {
371        match &self.backend {
372            PublisherBackend::Log(store) => {
373                store.append(message).await;
374                Ok(Sent::Ack)
375            }
376            PublisherBackend::Queue(sender) => {
377                if self.request_reply {
378                    let cid = message
379                        .metadata
380                        .entry("correlation_id".to_string())
381                        .or_insert_with(fast_uuid_v7::gen_id_string)
382                        .clone();
383
384                    let (tx, rx) = oneshot::channel();
385
386                    // Register waiter before sending
387                    let response_channel = get_or_create_response_channel(&self.topic);
388                    response_channel
389                        .register_waiter(&cid, tx)
390                        .await
391                        .map_err(PublisherError::NonRetryable)?;
392
393                    // Send the message
394                    // We use the internal sender directly to avoid recursion or cloning issues
395                    if let Err(e) = sender.send(vec![message]).await {
396                        response_channel.remove_waiter(&cid).await;
397                        return Err(anyhow!("Failed to send to memory channel: {}", e).into());
398                    }
399
400                    // Wait for the response
401                    let response = match tokio::time::timeout(self.request_timeout, rx).await {
402                        Ok(Ok(resp)) => resp,
403                        Ok(Err(e)) => {
404                            response_channel.remove_waiter(&cid).await;
405                            return Err(anyhow!(
406                                "Failed to receive response for correlation_id {}: {}",
407                                cid,
408                                e
409                            )
410                            .into());
411                        }
412                        Err(_) => {
413                            response_channel.remove_waiter(&cid).await;
414                            return Err(PublisherError::Retryable(anyhow!(
415                                "Request timed out waiting for response for correlation_id {}",
416                                cid
417                            )));
418                        }
419                    };
420
421                    Ok(Sent::Response(response))
422                } else {
423                    sender
424                        .send(vec![message])
425                        .await
426                        .map_err(|e| anyhow!("Failed to send to memory channel: {}", e))?;
427                    Ok(Sent::Ack)
428                }
429            }
430            PublisherBackend::Transport(transport) => {
431                transport
432                    .send_batch(vec![message])
433                    .await
434                    .map_err(|e| anyhow!("Failed to send via memory transport: {}", e))?;
435                Ok(Sent::Ack)
436            }
437        }
438    }
439
440    async fn send_batch(
441        &self,
442        messages: Vec<CanonicalMessage>,
443    ) -> Result<SentBatch, PublisherError> {
444        match &self.backend {
445            PublisherBackend::Log(store) => {
446                trace!(
447                    topic = %self.topic,
448                    message_ids = ?LazyMessageIds(&messages),
449                    "Appending batch to event store"
450                );
451                store.append_batch(messages).await;
452                Ok(SentBatch::Ack)
453            }
454            PublisherBackend::Queue(sender) => {
455                trace!(
456                    topic = %self.topic,
457                    message_ids = ?LazyMessageIds(&messages),
458                    "Sending batch to memory channel. Current batch count: {}",
459                    sender.len()
460                );
461                sender
462                    .send(messages)
463                    .await
464                    .map_err(|e| anyhow!("Failed to send to memory channel: {}", e))?;
465                Ok(SentBatch::Ack)
466            }
467            PublisherBackend::Transport(transport) => {
468                trace!(
469                    topic = %self.topic,
470                    message_ids = ?LazyMessageIds(&messages),
471                    "Sending batch to memory transport"
472                );
473                transport
474                    .send_batch(messages)
475                    .await
476                    .map_err(|e| anyhow!("Failed to send batch via memory transport: {}", e))?;
477                Ok(SentBatch::Ack)
478            }
479        }
480    }
481
482    async fn status(&self) -> EndpointStatus {
483        match &self.backend {
484            PublisherBackend::Queue(sender) => EndpointStatus {
485                healthy: !sender.is_closed(),
486                target: self.topic.clone(),
487                pending: Some(sender.len()),
488                capacity: Some(sender.capacity().unwrap_or(0)),
489                ..Default::default()
490            },
491            PublisherBackend::Log(_store) => EndpointStatus {
492                healthy: true,
493                target: self.topic.clone(),
494                details: serde_json::json!({
495                    "mode": "event_store"
496                }),
497                ..Default::default()
498            },
499            PublisherBackend::Transport(transport) => EndpointStatus {
500                healthy: !transport.is_closed(),
501                target: self.topic.clone(),
502                pending: Some(transport.len()),
503                capacity: transport.capacity(),
504                details: serde_json::json!({
505                    "mode": "transport"
506                }),
507                ..Default::default()
508            },
509        }
510    }
511
512    fn as_any(&self) -> &dyn Any {
513        self
514    }
515}
516
517/// A queue-based consumer (legacy behavior).
518#[derive(Debug)]
519pub struct MemoryQueueConsumer {
520    topic: String,
521    receiver: Receiver<Vec<CanonicalMessage>>,
522    // Internal buffer to hold messages from a received batch.
523    buffer: Vec<CanonicalMessage>,
524    enable_nack: bool,
525    /// Drain mode: only then does an idle recv time out into an empty batch.
526    exit_on_empty: bool,
527}
528
529#[derive(Clone)]
530pub struct TransportQueueConsumer {
531    topic: String,
532    transport: Arc<dyn TransportChannel>,
533    buffer: Vec<CanonicalMessage>,
534    /// Nacked messages awaiting redelivery.
535    ///
536    /// IPC transports are unidirectional (publisher -> consumer), so a requeue
537    /// cannot go back down the socket: the publisher never reads, so those bytes
538    /// would strand and eventually block the commit on a full socket buffer.
539    /// Redelivery is therefore consumer-local, and does not survive a consumer
540    /// crash. Shared with the commit closure, which has no access to `&mut self`.
541    requeue: Arc<Mutex<Vec<CanonicalMessage>>>,
542    enable_nack: bool,
543    /// Drain mode: only then does an idle recv time out into an empty batch.
544    exit_on_empty: bool,
545}
546
547impl fmt::Debug for TransportQueueConsumer {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        f.debug_struct("TransportQueueConsumer")
550            .field("topic", &self.topic)
551            .field("enable_nack", &self.enable_nack)
552            .finish_non_exhaustive()
553    }
554}
555
556/// A source that reads messages from an in-memory channel or event store.
557#[derive(Debug)]
558pub enum MemoryConsumer {
559    Queue(MemoryQueueConsumer),
560    Transport(TransportQueueConsumer),
561    Log {
562        consumer: EventStoreConsumer,
563        topic: String,
564    },
565}
566
567impl MemoryConsumer {
568    pub fn new(config: &MemoryConfig) -> anyhow::Result<Self> {
569        let config = normalized_memory_config(config)?;
570        let channel_exists = memory_channel_exists(&config.topic);
571        let store_exists = event_store_exists(&config.topic);
572
573        if config.subscribe_mode {
574            if channel_exists {
575                return Err(anyhow!("Topic '{}' is already active as a Queue (MemoryChannel), but Subscriber mode (EventStore) was requested.", config.topic));
576            }
577            let store = get_or_create_event_store(&config.topic);
578            // For subscriber mode, we generate a unique ID if one isn't implicit in the usage.
579            // However, MemorySubscriber struct usually handles the ID.
580            // If MemoryConsumer is used directly with subscribe_mode=true, we assume a default ID or ephemeral.
581            let subscriber_id = format!("{}-consumer", config.topic);
582            info!(topic = %config.topic, subscriber_id = %subscriber_id, "Memory consumer (Log mode) connected");
583            let consumer = store.consumer(subscriber_id);
584            Ok(Self::Log {
585                consumer,
586                topic: config.topic.clone(),
587            })
588        } else {
589            if store_exists {
590                // Unlike the Publisher, we cannot silently adapt to Log mode here.
591                // The EventStore implementation currently supports Pub/Sub (broadcast) only.
592                // Adapting would result in this consumer receiving all messages, violating
593                // the expected Queue (competing consumer) semantics requested by `subscribe_mode: false`.
594                return Err(anyhow!("Topic '{}' is already active as a Subscriber Log (EventStore), but Queue mode (MemoryChannel) was requested.", config.topic));
595            }
596            let queue = MemoryQueueConsumer::new(&config)?;
597            Ok(Self::Queue(queue))
598        }
599    }
600
601    pub async fn new_async(config: &MemoryConfig) -> anyhow::Result<Self> {
602        let url = resolved_transport(config)?;
603        match &url {
604            TransportUrl::Memory { .. } => Self::new(config),
605            _ => {
606                if config.subscribe_mode {
607                    return Err(anyhow!(
608                        "IPC memory consumers do not support subscribe_mode"
609                    ));
610                }
611                let config = config.clone().with_smart_defaults();
612                let capacity = config.capacity.unwrap_or(100);
613                let transport = create_transport_from_url(&url, capacity, true).await?;
614                Ok(Self::Transport(TransportQueueConsumer {
615                    topic: url.display_name(),
616                    transport,
617                    buffer: Vec::new(),
618                    requeue: Arc::new(Mutex::new(Vec::new())),
619                    enable_nack: config.enable_nack,
620                    exit_on_empty: false,
621                }))
622            }
623        }
624    }
625}
626
627impl Drop for MemoryQueueConsumer {
628    fn drop(&mut self) {
629        if !self.buffer.is_empty() {
630            let mut messages = std::mem::take(&mut self.buffer);
631            messages.reverse();
632
633            let channel = get_or_create_channel(&MemoryConfig {
634                topic: self.topic.clone(),
635                capacity: None,
636                ..Default::default()
637            });
638
639            match channel.sender.try_send(messages) {
640                Ok(_) => {
641                    info!(topic = %self.topic, "Requeued buffered messages on consumer drop");
642                }
643                Err(e) => {
644                    let msgs = match e {
645                        async_channel::TrySendError::Full(m) => m,
646                        async_channel::TrySendError::Closed(m) => m,
647                    };
648                    warn!(topic = %self.topic, "Channel full on drop, spawning async requeue");
649                    let sender = channel.sender.clone();
650                    if let Ok(handle) = tokio::runtime::Handle::try_current() {
651                        handle.spawn(async move {
652                            if let Err(e) = sender.send(msgs).await {
653                                tracing::error!(
654                                    "Failed to requeue buffered messages in background: {}",
655                                    e
656                                );
657                            }
658                        });
659                    } else {
660                        tracing::error!(topic = %self.topic, "No active runtime found, could not requeue buffered messages on consumer drop");
661                    }
662                }
663            }
664        }
665    }
666}
667
668impl MemoryQueueConsumer {
669    pub fn new(config: &MemoryConfig) -> anyhow::Result<Self> {
670        let channel = get_or_create_channel(config);
671        let buffer = if let Some(capacity) = config.capacity {
672            Vec::with_capacity(capacity)
673        } else {
674            Vec::new()
675        };
676        Ok(Self {
677            topic: config.topic.clone(),
678            receiver: channel.receiver.clone(),
679            buffer,
680            enable_nack: config.enable_nack,
681            exit_on_empty: false,
682        })
683    }
684
685    async fn get_buffered_msgs(
686        &mut self,
687        max_messages: usize,
688    ) -> Result<Vec<CanonicalMessage>, ConsumerError> {
689        // If the internal buffer has messages, return them first.
690        if self.buffer.is_empty() {
691            // Buffer is empty. Wait for a new batch from the channel.
692            // Drain mode: a brief idle timeout returns empty so --drain can fire.
693            let Some(recv) =
694                crate::traits::drain_gated(self.exit_on_empty, self.receiver.recv()).await
695            else {
696                return Ok(Vec::new());
697            };
698            self.buffer = match recv {
699                Ok(batch) => batch,
700                Err(_) => return Err(ConsumerError::EndOfStream),
701            };
702            // Reverse the buffer so we can efficiently pop from the end.
703            self.buffer.reverse();
704        }
705
706        // Determine the number of messages to take from the buffer.
707        let num_to_take = self.buffer.len().min(max_messages);
708        let split_at = self.buffer.len() - num_to_take;
709
710        // `split_off` is highly efficient. It splits the Vec in two at the given
711        // index and returns the part after the index, leaving the first part.
712        let mut messages = self.buffer.split_off(split_at);
713        messages.reverse(); // Reverse back to original order.
714        Ok(messages)
715    }
716}
717
718/// Requeues messages onto a topic's channel without ever blocking the caller.
719///
720/// Tries a non-blocking send first; if the channel is momentarily full, the
721/// blocking send is finished on a detached task. This matters when called from a
722/// commit, which holds a route dispatch permit: a blocking `send().await` there
723/// would stall the whole commit dispatcher (and thus the consumer that drains
724/// this very channel) into a deadlock. Messages are never dropped on a full
725/// channel — they are requeued in the background. The only loss is a *closed*
726/// channel (or no runtime), which is logged as an error.
727fn requeue_messages(topic: &str, messages: Vec<CanonicalMessage>) {
728    if messages.is_empty() {
729        return;
730    }
731    let count = messages.len();
732    let channel = get_or_create_channel(&MemoryConfig {
733        topic: topic.to_string(),
734        capacity: None,
735        ..Default::default()
736    });
737    match channel.sender.try_send(messages) {
738        Ok(_) => {}
739        Err(async_channel::TrySendError::Closed(_)) => {
740            tracing::error!(topic = %topic, count, "Dropped messages: memory channel closed during requeue");
741        }
742        Err(async_channel::TrySendError::Full(msgs)) => match tokio::runtime::Handle::try_current()
743        {
744            Ok(handle) => {
745                let sender = channel.sender.clone();
746                let topic = topic.to_string();
747                handle.spawn(async move {
748                    if let Err(e) = sender.send(msgs).await {
749                        tracing::error!(topic = %topic, count, "Dropped messages: background requeue failed: {}", e);
750                    }
751                });
752            }
753            Err(_) => {
754                tracing::error!(topic = %topic, count, "Dropped messages: no runtime to complete requeue");
755            }
756        },
757    }
758}
759
760struct RequeueGuard {
761    topic: String,
762    messages: Vec<CanonicalMessage>,
763}
764
765impl Drop for RequeueGuard {
766    fn drop(&mut self) {
767        requeue_messages(&self.topic, std::mem::take(&mut self.messages));
768    }
769}
770
771#[async_trait]
772impl MessageConsumer for MemoryQueueConsumer {
773    // Channel-backed: commit only requeues this batch's own nacks (no cursor),
774    // so commits are order-independent.
775    fn commit_requires_order(&self) -> bool {
776        false
777    }
778    fn set_exit_on_empty(&mut self, exit_on_empty: bool) {
779        self.exit_on_empty = exit_on_empty;
780    }
781    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
782        // If the internal buffer has messages, return them first.
783
784        let mut messages = self.get_buffered_msgs(max_messages).await?;
785        while messages.len() < max_messages / 2 {
786            if let Ok(mut next_batch) = self.receiver.try_recv() {
787                if next_batch.len() + messages.len() > max_messages {
788                    let needed = max_messages - messages.len();
789                    let mut to_buffer = next_batch.split_off(needed);
790                    messages.append(&mut next_batch);
791                    self.buffer.append(&mut to_buffer);
792                    self.buffer.reverse();
793                    break;
794                } else {
795                    messages.append(&mut next_batch);
796                }
797            } else {
798                break;
799            }
800        }
801        trace!(count = messages.len(), topic = %self.topic, message_ids = ?LazyMessageIds(&messages), "Received batch of memory messages");
802        if messages.is_empty() {
803            return Ok(ReceivedBatch {
804                messages: Vec::new(),
805                commit: Box::new(|_| {
806                    Box::pin(async move { Ok(()) }) as BoxFuture<'static, anyhow::Result<()>>
807                }),
808            });
809        }
810
811        let topic = self.topic.clone();
812        let expected_count = messages.len();
813        let correlation_ids: Vec<Option<String>> = messages
814            .iter()
815            .map(|m| m.metadata.get("correlation_id").cloned())
816            .collect();
817
818        // Guard to requeue messages if the batch is dropped without commit/nack.
819        let mut guard = if self.enable_nack {
820            Some(RequeueGuard {
821                topic: self.topic.clone(),
822                messages: messages.clone(),
823            })
824        } else {
825            None
826        };
827
828        let commit = Box::new(move |dispositions: Vec<MessageDisposition>| {
829            Box::pin(async move {
830                if dispositions.len() != expected_count {
831                    return Err(anyhow::anyhow!(
832                        "Memory batch commit received mismatched disposition count: expected {}, got {}",
833                        expected_count,
834                        dispositions.len()
835                    ));
836                }
837
838                // Clone messages from guard to keep it armed during async operations
839                let messages_for_retry = if let Some(g) = &guard {
840                    g.messages.clone()
841                } else {
842                    Vec::new()
843                };
844
845                let response_channel = get_or_create_response_channel(&topic);
846                let mut to_requeue = Vec::new();
847
848                for (i, disposition) in dispositions.into_iter().enumerate() {
849                    match disposition {
850                        MessageDisposition::Reply(resp) => {
851                            handle_memory_reply(resp, i, &correlation_ids, &response_channel).await;
852                        }
853                        MessageDisposition::Nack => {
854                            if let Some(msg) = messages_for_retry.get(i) {
855                                warn!("Requeueing nacked message {}", i);
856                                to_requeue.push(msg.clone());
857                            } else {
858                                warn!("Nack for index {} but no message in retry buffer!", i);
859                            }
860                        }
861                        MessageDisposition::Ack => {}
862                    }
863                }
864
865                // Requeue nacked messages without blocking the commit: this runs
866                // while holding a dispatch permit, so a blocking send into a full
867                // channel would deadlock the route. Messages are not dropped.
868                requeue_messages(&topic, to_requeue);
869
870                // Disarm the guard after all awaits are finished.
871                if let Some(g) = &mut guard {
872                    std::mem::take(&mut g.messages);
873                }
874
875                Ok(())
876            }) as BoxFuture<'static, anyhow::Result<()>>
877        }) as BatchCommitFunc;
878        Ok(ReceivedBatch { messages, commit })
879    }
880
881    async fn status(&self) -> EndpointStatus {
882        let pending = self.receiver.len();
883        let capacity = self.receiver.capacity().unwrap_or(0);
884        EndpointStatus {
885            healthy: !self.receiver.is_closed(),
886            target: self.topic.clone(),
887            pending: Some(pending),
888            capacity: Some(capacity),
889            ..Default::default()
890        }
891    }
892
893    fn as_any(&self) -> &dyn Any {
894        self
895    }
896}
897
898#[async_trait]
899impl MessageConsumer for TransportQueueConsumer {
900    // Channel-backed: no cursor, commits are order-independent.
901    fn commit_requires_order(&self) -> bool {
902        false
903    }
904    fn set_exit_on_empty(&mut self, exit_on_empty: bool) {
905        self.exit_on_empty = exit_on_empty;
906    }
907    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
908        let mut messages = Vec::with_capacity(max_messages);
909
910        // Nacked messages get redelivered ahead of anything new.
911        {
912            let mut requeue = self.requeue.lock().unwrap();
913            let take = requeue.len().min(max_messages);
914            if take > 0 {
915                messages.extend(requeue.drain(..take));
916            }
917        }
918
919        if messages.len() < max_messages && !self.buffer.is_empty() {
920            let buffered = self.buffer.len().min(max_messages - messages.len());
921            messages.extend(self.buffer.drain(..buffered));
922        }
923
924        // Only block on the transport when nothing was already pending, so a
925        // redelivery is never held up waiting for a new frame to arrive.
926        if messages.is_empty() {
927            // Drain mode: a brief idle timeout leaves the batch empty so --drain can fire.
928            if let Some(r) =
929                crate::traits::drain_gated(self.exit_on_empty, self.transport.recv_batch()).await
930            {
931                let mut received = r.map_err(|e| {
932                    ConsumerError::Connection(anyhow!(
933                        "Failed to receive via memory transport: {}",
934                        e
935                    ))
936                })?;
937                messages.append(&mut received);
938                if messages.len() > max_messages {
939                    self.buffer = messages.split_off(max_messages);
940                }
941            }
942        }
943
944        trace!(count = messages.len(), topic = %self.topic, message_ids = ?LazyMessageIds(&messages), "Received batch from memory transport");
945
946        let topic = self.topic.clone();
947        let requeue = self.requeue.clone();
948        let enable_nack = self.enable_nack;
949        let expected_count = messages.len();
950        let messages_for_retry = if enable_nack {
951            messages.clone()
952        } else {
953            Vec::new()
954        };
955
956        let commit = Box::new(move |dispositions: Vec<MessageDisposition>| {
957            let requeue = requeue.clone();
958            let topic = topic.clone();
959            let messages_for_retry = messages_for_retry.clone();
960            Box::pin(async move {
961                if dispositions.len() != expected_count {
962                    return Err(anyhow::anyhow!(
963                        "Memory transport batch commit received mismatched disposition count: expected {}, got {}",
964                        expected_count,
965                        dispositions.len()
966                    ));
967                }
968
969                let mut to_requeue = Vec::new();
970                for (i, disposition) in dispositions.into_iter().enumerate() {
971                    match disposition {
972                        MessageDisposition::Nack if enable_nack => {
973                            if let Some(msg) = messages_for_retry.get(i) {
974                                to_requeue.push(msg.clone());
975                            }
976                        }
977                        MessageDisposition::Reply(_) => {
978                            tracing::warn!(topic = %topic, "IPC memory transport does not support reply dispositions");
979                        }
980                        MessageDisposition::Ack | MessageDisposition::Nack => {}
981                    }
982                }
983
984                if !to_requeue.is_empty() {
985                    // Redeliver locally. Sending back down the socket would push
986                    // these at a publisher that never reads.
987                    let count = to_requeue.len();
988                    requeue.lock().unwrap().extend(to_requeue);
989                    tracing::debug!(topic = %topic, count, "Requeued nacked IPC messages for local redelivery");
990                }
991
992                Ok(())
993            }) as BoxFuture<'static, anyhow::Result<()>>
994        }) as BatchCommitFunc;
995
996        Ok(ReceivedBatch { messages, commit })
997    }
998
999    async fn status(&self) -> EndpointStatus {
1000        EndpointStatus {
1001            healthy: !self.transport.is_closed(),
1002            target: self.topic.clone(),
1003            // Everything readable without waiting on the peer: messages held
1004            // locally plus whole frames already buffered by the transport.
1005            pending: Some(
1006                self.buffer.len()
1007                    + self.requeue.lock().map(|q| q.len()).unwrap_or(0)
1008                    + self.transport.len(),
1009            ),
1010            capacity: self.transport.capacity(),
1011            details: serde_json::json!({
1012                "mode": "transport"
1013            }),
1014            ..Default::default()
1015        }
1016    }
1017
1018    fn as_any(&self) -> &dyn Any {
1019        self
1020    }
1021}
1022
1023async fn handle_memory_reply(
1024    mut resp: CanonicalMessage,
1025    index: usize,
1026    correlation_ids: &[Option<String>],
1027    response_channel: &MemoryResponseChannel,
1028) {
1029    if !resp.metadata.contains_key("correlation_id") {
1030        if let Some(Some(cid)) = correlation_ids.get(index) {
1031            resp.metadata
1032                .insert("correlation_id".to_string(), cid.clone());
1033        }
1034    }
1035
1036    if let Some(cid) = resp.metadata.get("correlation_id") {
1037        if let Some(tx) = response_channel.remove_waiter(cid).await {
1038            let _ = tx.send(resp);
1039            return;
1040        }
1041    }
1042    // No waiter: deliver best-effort. A forward route (no `reply_to`) still gets a
1043    // publisher response per message but nothing drains this channel; a blocking
1044    // send would fill the buffer and, since this runs inside a commit holding a
1045    // dispatch permit, deadlock the route. Drop on overflow instead.
1046    if let Err(async_channel::TrySendError::Full(_)) = response_channel.sender.try_send(resp) {
1047        trace!("Dropping unconsumed memory response (response channel full, no waiter)");
1048    }
1049}
1050
1051#[async_trait]
1052impl MessageConsumer for MemoryConsumer {
1053    // Delegate to the active backend: channel backends commit order-independently;
1054    // the Log (event-store) backend keeps the conservative default because its
1055    // per-subscriber cursor is position-based.
1056    fn commit_requires_order(&self) -> bool {
1057        match self {
1058            Self::Queue(q) => q.commit_requires_order(),
1059            Self::Transport(t) => t.commit_requires_order(),
1060            Self::Log { consumer, .. } => consumer.commit_requires_order(),
1061        }
1062    }
1063    fn set_exit_on_empty(&mut self, exit_on_empty: bool) {
1064        match self {
1065            Self::Queue(q) => q.set_exit_on_empty(exit_on_empty),
1066            Self::Transport(t) => t.set_exit_on_empty(exit_on_empty),
1067            Self::Log { consumer, .. } => consumer.set_exit_on_empty(exit_on_empty),
1068        }
1069    }
1070    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
1071        match self {
1072            Self::Queue(q) => q.receive_batch(max_messages).await,
1073            Self::Transport(t) => t.receive_batch(max_messages).await,
1074            Self::Log { consumer, .. } => consumer.receive_batch(max_messages).await,
1075        }
1076    }
1077
1078    async fn status(&self) -> EndpointStatus {
1079        match self {
1080            Self::Queue(q) => q.status().await,
1081            Self::Transport(t) => t.status().await,
1082            Self::Log { consumer, .. } => consumer.status().await,
1083        }
1084    }
1085
1086    fn as_any(&self) -> &dyn Any {
1087        self
1088    }
1089}
1090
1091impl MemoryConsumer {
1092    pub fn new_local(topic: &str, capacity: usize) -> Self {
1093        Self::new(&MemoryConfig {
1094            topic: topic.to_string(),
1095            capacity: Some(capacity),
1096            ..Default::default()
1097        })
1098        .expect("Failed to create local memory consumer")
1099    }
1100    pub fn channel(&self) -> MemoryChannel {
1101        let topic = match self {
1102            Self::Queue(q) => &q.topic,
1103            Self::Transport(t) => &t.topic,
1104            Self::Log { topic, .. } => topic,
1105        };
1106        get_or_create_channel(&MemoryConfig {
1107            topic: topic.clone(),
1108            ..Default::default()
1109        })
1110    }
1111}
1112
1113pub struct MemorySubscriber {
1114    consumer: MemoryConsumer,
1115}
1116
1117impl MemorySubscriber {
1118    pub fn new(config: &MemoryConfig, id: &str) -> anyhow::Result<Self> {
1119        let mut sub_config = config.clone();
1120        // If subscribe_mode is true, we use EventStore with the original topic but unique subscriber ID.
1121        // If false (legacy), we use the suffixed topic queue.
1122        let consumer = if config.subscribe_mode {
1123            let store = get_or_create_event_store(&config.topic);
1124            MemoryConsumer::Log {
1125                consumer: store.consumer(id.to_string()),
1126                topic: config.topic.clone(),
1127            }
1128        } else {
1129            sub_config.topic = format!("{}-{}", config.topic, id);
1130            MemoryConsumer::new(&sub_config)?
1131        };
1132        Ok(Self { consumer })
1133    }
1134}
1135
1136#[async_trait]
1137impl MessageConsumer for MemorySubscriber {
1138    fn commit_requires_order(&self) -> bool {
1139        self.consumer.commit_requires_order()
1140    }
1141    fn set_exit_on_empty(&mut self, exit_on_empty: bool) {
1142        self.consumer.set_exit_on_empty(exit_on_empty);
1143    }
1144    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
1145        self.consumer.receive_batch(max_messages).await
1146    }
1147
1148    async fn receive(&mut self) -> Result<Received, ConsumerError> {
1149        self.consumer.receive().await
1150    }
1151
1152    fn as_any(&self) -> &dyn Any {
1153        self
1154    }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159    use super::*;
1160    use crate::models::{Endpoint, Route};
1161    use crate::traits::Handled;
1162    use crate::{msg, CanonicalMessage};
1163    use serde_json::json;
1164    use tokio::time::sleep;
1165
1166    #[tokio::test]
1167    async fn test_memory_channel_integration() {
1168        let mut consumer = MemoryConsumer::new_local("test-mem1", 10);
1169        let publisher = MemoryPublisher::new_local("test-mem1", 10);
1170
1171        let msg = msg!(json!({"hello": "memory"}));
1172
1173        // Send a message via the publisher
1174        publisher.send(msg.clone()).await.unwrap();
1175
1176        sleep(std::time::Duration::from_millis(10)).await;
1177        // Receive it with the consumer
1178        let received = consumer.receive().await.unwrap();
1179        let _ = (received.commit)(MessageDisposition::Ack).await;
1180        assert_eq!(received.message.payload, msg.payload);
1181        assert_eq!(consumer.channel().len(), 0);
1182    }
1183
1184    #[tokio::test]
1185    async fn test_memory_url_alias_uses_same_channel_as_legacy_topic() {
1186        let mut consumer = MemoryConsumer::new(&MemoryConfig::new("test-memory-url", Some(10)))
1187            .expect("legacy topic consumer should be in-process memory");
1188        let publisher = MemoryPublisher::new_async(&MemoryConfig::new_with_url(
1189            "memory://test-memory-url",
1190            Some(10),
1191        ))
1192        .await
1193        .expect("memory URL publisher should be in-process memory");
1194
1195        let msg = msg!(json!({"hello": "memory-url"}));
1196        publisher.send(msg.clone()).await.unwrap();
1197
1198        let received = consumer.receive().await.unwrap();
1199        let _ = (received.commit)(MessageDisposition::Ack).await;
1200        assert_eq!(received.message.payload, msg.payload);
1201    }
1202
1203    #[cfg(unix)]
1204    #[tokio::test]
1205    async fn test_unix_ipc_endpoint_constructors_roundtrip() {
1206        let temp_dir = tempfile::TempDir::new().unwrap();
1207        let socket_path = temp_dir.path().join("endpoint.sock");
1208        let url = format!("unix://{}", socket_path.display());
1209        let config = MemoryConfig::new_with_url(url, Some(10));
1210
1211        assert!(config.clone().with_smart_defaults().enable_nack);
1212
1213        let mut consumer = MemoryConsumer::new_async(&config)
1214            .await
1215            .expect("IPC consumer should create a Unix socket server");
1216        let publisher = MemoryPublisher::new_async(&config)
1217            .await
1218            .expect("IPC publisher should connect to the Unix socket server");
1219
1220        let msg = CanonicalMessage::from_vec(b"endpoint-ipc");
1221        publisher.send(msg.clone()).await.unwrap();
1222
1223        let received = consumer.receive().await.unwrap();
1224        (received.commit)(MessageDisposition::Ack).await.unwrap();
1225        assert_eq!(received.message.payload.as_ref(), b"endpoint-ipc");
1226    }
1227
1228    /// Regression: nacking over IPC used to call `send_batch` on the consumer's
1229    /// own transport, writing the messages back down the socket at a publisher
1230    /// that never reads. They were never redelivered, and once the peer's
1231    /// receive buffer filled the commit blocked while holding a dispatch permit.
1232    /// `enable_nack` defaults to true for IPC, so this was the default path.
1233    #[cfg(unix)]
1234    #[tokio::test]
1235    async fn test_unix_ipc_nack_redelivers_locally() {
1236        let temp_dir = tempfile::TempDir::new().unwrap();
1237        let socket_path = temp_dir.path().join("nack.sock");
1238        let url = format!("unix://{}", socket_path.display());
1239        let config = MemoryConfig::new_with_url(url, Some(10));
1240
1241        // Nack support is on by default for IPC transports.
1242        assert!(config.clone().with_smart_defaults().enable_nack);
1243
1244        let mut consumer = MemoryConsumer::new_async(&config).await.unwrap();
1245        let publisher = MemoryPublisher::new_async(&config).await.unwrap();
1246
1247        publisher
1248            .send(CanonicalMessage::from_vec(b"to_be_nacked"))
1249            .await
1250            .unwrap();
1251
1252        // Receive and nack.
1253        let first = consumer.receive().await.unwrap();
1254        assert_eq!(first.message.get_payload_str(), "to_be_nacked");
1255        (first.commit)(MessageDisposition::Nack).await.unwrap();
1256
1257        // Must come back without the publisher resending anything.
1258        let second = tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive())
1259            .await
1260            .expect("nacked message should be redelivered")
1261            .unwrap();
1262        assert_eq!(second.message.get_payload_str(), "to_be_nacked");
1263
1264        (second.commit)(MessageDisposition::Ack).await.unwrap();
1265
1266        // After the ack it must not come back again.
1267        let result =
1268            tokio::time::timeout(std::time::Duration::from_millis(200), consumer.receive()).await;
1269        assert!(result.is_err(), "acked message must not be redelivered");
1270    }
1271
1272    /// A nack must not block even when nothing is draining the socket, and the
1273    /// commit must not wedge the route.
1274    #[cfg(unix)]
1275    #[tokio::test]
1276    async fn test_unix_ipc_nack_commit_does_not_block() {
1277        let temp_dir = tempfile::TempDir::new().unwrap();
1278        let socket_path = temp_dir.path().join("nack_block.sock");
1279        let url = format!("unix://{}", socket_path.display());
1280        let config = MemoryConfig::new_with_url(url, Some(1));
1281
1282        let mut consumer = MemoryConsumer::new_async(&config).await.unwrap();
1283        let publisher = MemoryPublisher::new_async(&config).await.unwrap();
1284
1285        // Large enough to exceed the socket buffer (8 KiB on macOS), so the send
1286        // only completes once the consumer drains it. It has to run concurrently
1287        // with the receive: the consumer accepts the connection inside
1288        // `receive_batch`, so a blocking send here would deadlock the test.
1289        let total = 200usize;
1290        let msgs: Vec<CanonicalMessage> = (0..total)
1291            .map(|i| CanonicalMessage::from_vec(format!("m{i}").as_bytes()))
1292            .collect();
1293        let send_task = tokio::spawn(async move { publisher.send_batch(msgs).await });
1294
1295        let batch = consumer.receive_batch(total).await.unwrap();
1296        let n = batch.messages.len();
1297        assert_eq!(n, total);
1298        send_task.await.unwrap().unwrap();
1299
1300        // Nack the whole batch; this must return promptly rather than blocking
1301        // on a socket write.
1302        tokio::time::timeout(
1303            std::time::Duration::from_secs(5),
1304            (batch.commit)(vec![MessageDisposition::Nack; n]),
1305        )
1306        .await
1307        .expect("nack commit must not block")
1308        .unwrap();
1309
1310        // All of them are available again.
1311        let requeued = tokio::time::timeout(
1312            std::time::Duration::from_secs(1),
1313            consumer.receive_batch(total),
1314        )
1315        .await
1316        .expect("requeued messages should be readable")
1317        .unwrap();
1318        assert_eq!(requeued.messages.len(), n);
1319        (requeued.commit)(vec![MessageDisposition::Ack; n])
1320            .await
1321            .unwrap();
1322    }
1323
1324    #[tokio::test]
1325    async fn test_memory_publisher_and_consumer_integration() {
1326        let mut consumer = MemoryConsumer::new_local("test-mem2", 10);
1327        let publisher = MemoryPublisher::new_local("test-mem2", 10);
1328
1329        let msg1 = msg!(json!({"message": "one"}));
1330        let msg2 = msg!(json!({"message": "two"}));
1331        let msg3 = msg!(json!({"message": "three"}));
1332
1333        publisher
1334            .send_batch(vec![msg1.clone(), msg2.clone()])
1335            .await
1336            .unwrap();
1337        publisher.send(msg3.clone()).await.unwrap();
1338
1339        // Verify the channel has the messages
1340        assert_eq!(publisher.channel().len(), 2);
1341
1342        // Receive the messages and verify them
1343        let received1 = consumer.receive().await.unwrap();
1344        let _ = (received1.commit)(MessageDisposition::Ack).await;
1345        assert_eq!(received1.message.payload, msg1.payload);
1346
1347        let batch2 = consumer.receive_batch(1).await.unwrap();
1348        let (received_msg2, commit2) = (batch2.messages, batch2.commit);
1349        let _ = commit2(vec![MessageDisposition::Ack; received_msg2.len()]).await;
1350        assert_eq!(received_msg2.len(), 1);
1351        assert_eq!(received_msg2.first().unwrap().payload, msg2.payload);
1352        let batch3 = consumer.receive_batch(2).await.unwrap();
1353        let (received_msg3, commit3) = (batch3.messages, batch3.commit);
1354        let _ = commit3(vec![MessageDisposition::Ack; received_msg3.len()]).await;
1355        assert_eq!(received_msg3.first().unwrap().payload, msg3.payload);
1356
1357        // Verify the channel is empty
1358        assert_eq!(publisher.channel().len(), 0);
1359
1360        // Verify that reading again results in an error because the channel is empty and we are not closing it
1361        // In a real scenario with a closed channel, this would error out. Here we can just check it's empty.
1362        // A `receive` call would just hang, waiting for a message.
1363    }
1364
1365    #[tokio::test]
1366    async fn test_memory_subscriber_structure() {
1367        let cfg = MemoryConfig {
1368            topic: "base_topic".to_string(),
1369            capacity: Some(10),
1370            ..Default::default()
1371        };
1372        let subscriber_id = "sub1";
1373        let mut subscriber = MemorySubscriber::new(&cfg, subscriber_id).unwrap();
1374
1375        // The subscriber should be listening on "base_topic-sub1"
1376        // We can verify this by creating a publisher for that specific topic.
1377        let pub_cfg = MemoryConfig {
1378            topic: format!("base_topic-{}", subscriber_id),
1379            capacity: Some(10),
1380            ..Default::default()
1381        };
1382        let publisher = MemoryPublisher::new(&pub_cfg).unwrap();
1383
1384        publisher.send("hello subscriber".into()).await.unwrap();
1385
1386        let received = subscriber.receive().await.unwrap();
1387        assert_eq!(received.message.get_payload_str(), "hello subscriber");
1388    }
1389
1390    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1391    async fn test_memory_request_reply_mode() {
1392        let topic = format!("mem_rr_topic_{}", fast_uuid_v7::gen_id_str());
1393        let input_endpoint = Endpoint::new_memory(&topic, 10);
1394        let output_endpoint = Endpoint::new_response();
1395        let handler = |mut msg: CanonicalMessage| async move {
1396            let request_payload = msg.get_payload_str();
1397            let response_payload = format!("reply to {}", request_payload);
1398            msg.set_payload_str(response_payload);
1399            Ok(Handled::Publish(msg))
1400        };
1401
1402        let route = Route::new(input_endpoint, output_endpoint).with_handler(handler);
1403        route.deploy("mem_rr_test").await.unwrap();
1404
1405        // Create a publisher with request_reply = true
1406        let publisher = MemoryPublisher::new(&MemoryConfig {
1407            topic: topic.clone(),
1408            capacity: Some(10),
1409            request_reply: true,
1410            request_timeout_ms: Some(2000),
1411            ..Default::default()
1412        })
1413        .unwrap();
1414
1415        let result = publisher.send("direct request".into()).await.unwrap();
1416
1417        if let Sent::Response(response_msg) = result {
1418            assert_eq!(response_msg.get_payload_str(), "reply to direct request");
1419        } else {
1420            panic!("Expected Sent::Response, got {:?}", result);
1421        }
1422
1423        // Clean up
1424        Route::stop("mem_rr_test").await;
1425    }
1426
1427    #[tokio::test]
1428    async fn test_memory_request_reply_timeout_cleans_waiter() {
1429        let topic = format!("mem_rr_timeout_{}", fast_uuid_v7::gen_id_str());
1430        let correlation_id = fast_uuid_v7::gen_id_string();
1431        let publisher = MemoryPublisher::new(&MemoryConfig {
1432            topic: topic.clone(),
1433            capacity: Some(10),
1434            request_reply: true,
1435            request_timeout_ms: Some(25),
1436            ..Default::default()
1437        })
1438        .unwrap();
1439
1440        let mut message = CanonicalMessage::from("request with no responder");
1441        message
1442            .metadata
1443            .insert("correlation_id".to_string(), correlation_id.clone());
1444
1445        let err = publisher.send(message).await.unwrap_err();
1446        assert!(err
1447            .to_string()
1448            .contains("Request timed out waiting for response"));
1449
1450        let response_channel = get_or_create_response_channel(&topic);
1451        assert!(
1452            response_channel
1453                .remove_waiter(&correlation_id)
1454                .await
1455                .is_none(),
1456            "timed out request should clean up the registered waiter"
1457        );
1458    }
1459
1460    #[tokio::test]
1461    async fn test_memory_nack_requeue() {
1462        let topic = format!("test_nack_requeue_{}", fast_uuid_v7::gen_id_str());
1463        let config = MemoryConfig {
1464            topic: topic.clone(),
1465            capacity: Some(10),
1466            enable_nack: true,
1467            ..Default::default()
1468        };
1469        let mut consumer = MemoryConsumer::new(&config).unwrap();
1470        let publisher = MemoryPublisher::new_local(&topic, 10);
1471
1472        publisher.send("to_be_nacked".into()).await.unwrap();
1473
1474        let received1 = consumer.receive().await.unwrap();
1475        assert_eq!(received1.message.get_payload_str(), "to_be_nacked");
1476        (received1.commit)(crate::traits::MessageDisposition::Nack)
1477            .await
1478            .unwrap();
1479
1480        let received2 = tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive())
1481            .await
1482            .expect("Timed out waiting for re-queued message")
1483            .unwrap();
1484        assert_eq!(received2.message.get_payload_str(), "to_be_nacked");
1485
1486        (received2.commit)(crate::traits::MessageDisposition::Ack)
1487            .await
1488            .unwrap();
1489
1490        let result =
1491            tokio::time::timeout(std::time::Duration::from_millis(100), consumer.receive()).await;
1492        assert!(result.is_err(), "Channel should be empty");
1493    }
1494
1495    #[tokio::test]
1496    async fn test_memory_dropped_batch_requeues_messages() {
1497        let topic = format!("drop_requeue_{}", fast_uuid_v7::gen_id_str());
1498        let config = MemoryConfig {
1499            topic: topic.clone(),
1500            capacity: Some(10),
1501            enable_nack: true,
1502            ..Default::default()
1503        };
1504        let mut consumer = MemoryConsumer::new(&config).unwrap();
1505        let publisher = MemoryPublisher::new_local(&topic, 10);
1506
1507        publisher
1508            .send_batch(vec!["first".into(), "second".into()])
1509            .await
1510            .unwrap();
1511
1512        let batch = consumer.receive_batch(2).await.unwrap();
1513        assert_eq!(batch.messages.len(), 2);
1514        drop(batch);
1515
1516        let requeued =
1517            tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive_batch(2))
1518                .await
1519                .expect("Timed out waiting for dropped batch to be re-queued")
1520                .unwrap();
1521
1522        assert_eq!(
1523            requeued
1524                .messages
1525                .iter()
1526                .map(CanonicalMessage::get_payload_str)
1527                .collect::<Vec<_>>(),
1528            vec!["first".to_string(), "second".to_string()]
1529        );
1530
1531        (requeued.commit)(vec![MessageDisposition::Ack, MessageDisposition::Ack])
1532            .await
1533            .unwrap();
1534    }
1535
1536    #[tokio::test]
1537    async fn test_memory_batch_commit_rejects_mismatched_dispositions() {
1538        let topic = format!("commit_mismatch_{}", fast_uuid_v7::gen_id_str());
1539        let config = MemoryConfig {
1540            topic: topic.clone(),
1541            capacity: Some(10),
1542            enable_nack: true,
1543            ..Default::default()
1544        };
1545        let mut consumer = MemoryConsumer::new(&config).unwrap();
1546        let publisher = MemoryPublisher::new_local(&topic, 10);
1547
1548        publisher
1549            .send_batch(vec!["one".into(), "two".into()])
1550            .await
1551            .unwrap();
1552
1553        let batch = consumer.receive_batch(2).await.unwrap();
1554        let err = (batch.commit)(vec![MessageDisposition::Ack])
1555            .await
1556            .unwrap_err();
1557        assert!(err
1558            .to_string()
1559            .contains("Memory batch commit received mismatched disposition count"));
1560
1561        let retried =
1562            tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive_batch(2))
1563                .await
1564                .expect("Timed out waiting for mismatched commit batch to be re-queued")
1565                .unwrap();
1566        assert_eq!(retried.messages.len(), 2);
1567        (retried.commit)(vec![MessageDisposition::Ack, MessageDisposition::Ack])
1568            .await
1569            .unwrap();
1570    }
1571
1572    #[tokio::test]
1573    async fn test_memory_nack_requeue_does_not_block_and_loses_nothing() {
1574        // Regression: nacked messages are requeued from inside a commit (which holds
1575        // a route dispatch permit). If the input channel is full, a blocking send
1576        // there would deadlock the route. The requeue must return immediately AND
1577        // not drop the nacked messages — they get requeued in the background.
1578        let topic = format!("nack_requeue_{}", fast_uuid_v7::gen_id_str());
1579        let config = MemoryConfig {
1580            topic: topic.clone(),
1581            capacity: Some(1), // one batch slot, so the channel is easily full
1582            enable_nack: true,
1583            ..Default::default()
1584        };
1585        let mut consumer = MemoryConsumer::new(&config).unwrap();
1586        let publisher = MemoryPublisher::new_local(&topic, 1);
1587
1588        publisher.send_batch(vec!["A".into()]).await.unwrap();
1589        let batch_a = consumer.receive_batch(4).await.unwrap();
1590        assert_eq!(batch_a.messages.len(), 1);
1591
1592        // Fill the (capacity-1) channel so A's nack-requeue cannot fit immediately.
1593        publisher.send_batch(vec!["B".into()]).await.unwrap();
1594
1595        // Commit A with a Nack. The channel is full, so the requeue must defer to a
1596        // background task rather than block the commit.
1597        tokio::time::timeout(
1598            std::time::Duration::from_secs(5),
1599            (batch_a.commit)(vec![MessageDisposition::Nack]),
1600        )
1601        .await
1602        .expect("nack commit blocked requeuing into a full input channel")
1603        .unwrap();
1604
1605        // Both A (requeued) and B must still be delivered — nothing dropped.
1606        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1607        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
1608        while seen.len() < 2 && std::time::Instant::now() < deadline {
1609            if let Ok(Ok(batch)) = tokio::time::timeout(
1610                std::time::Duration::from_millis(200),
1611                consumer.receive_batch(4),
1612            )
1613            .await
1614            {
1615                for m in &batch.messages {
1616                    seen.insert(m.get_payload_str().into_owned());
1617                }
1618                let n = batch.messages.len();
1619                (batch.commit)(vec![MessageDisposition::Ack; n])
1620                    .await
1621                    .unwrap();
1622            }
1623        }
1624        assert!(seen.contains("A"), "nacked message A was lost");
1625        assert!(seen.contains("B"), "message B was lost");
1626    }
1627
1628    #[tokio::test]
1629    async fn test_memory_reply_overflow_does_not_block_commit() {
1630        // Regression: a forward route (memory -> a publisher that returns responses,
1631        // e.g. HTTP) still produces a publisher response per message, mapped to a
1632        // Reply disposition. Nothing drains the per-topic response channel and no
1633        // waiter is registered, so committing must not block once that bounded
1634        // channel fills — otherwise the commit wedges while holding a dispatch
1635        // permit and deadlocks the whole route. See `handle_memory_reply`.
1636        let topic = format!("reply_overflow_{}", fast_uuid_v7::gen_id_str());
1637        let config = MemoryConfig {
1638            topic: topic.clone(),
1639            capacity: Some(1000),
1640            ..Default::default()
1641        };
1642        let mut consumer = MemoryConsumer::new(&config).unwrap();
1643        let publisher = MemoryPublisher::new_local(&topic, 1000);
1644
1645        // Far more replies than the response channel capacity (100), nothing draining.
1646        let total = 500usize;
1647        let msgs: Vec<CanonicalMessage> = (0..total).map(|i| format!("m{i}").into()).collect();
1648        publisher.send_batch(msgs).await.unwrap();
1649
1650        let mut handled = 0usize;
1651        while handled < total {
1652            let batch = consumer.receive_batch(16).await.unwrap();
1653            let n = batch.messages.len();
1654            if n == 0 {
1655                break;
1656            }
1657            let dispositions: Vec<MessageDisposition> = (0..n)
1658                .map(|i| MessageDisposition::Reply(format!("r{i}").into()))
1659                .collect();
1660            tokio::time::timeout(
1661                std::time::Duration::from_secs(5),
1662                (batch.commit)(dispositions),
1663            )
1664            .await
1665            .expect("commit blocked delivering replies to a full, undrained response channel")
1666            .unwrap();
1667            handled += n;
1668        }
1669        assert_eq!(handled, total);
1670    }
1671
1672    #[tokio::test]
1673    async fn test_memory_event_store_integration() {
1674        let topic = "event_store_test";
1675        // Publisher with subscribe_mode=true enables EventStore writing
1676        let pub_config = MemoryConfig {
1677            topic: topic.to_string(),
1678            subscribe_mode: true,
1679            ..Default::default()
1680        };
1681        let publisher = MemoryPublisher::new(&pub_config).unwrap();
1682
1683        // Subscriber 1
1684        let mut sub1 = MemorySubscriber::new(&pub_config, "sub1").unwrap();
1685        // Subscriber 2
1686        let mut sub2 = MemorySubscriber::new(&pub_config, "sub2").unwrap();
1687
1688        publisher.send("event1".into()).await.unwrap();
1689
1690        let msg1 = sub1.receive().await.unwrap();
1691        assert_eq!(msg1.message.get_payload_str(), "event1");
1692        (msg1.commit)(MessageDisposition::Ack).await.unwrap();
1693
1694        let msg2 = sub2.receive().await.unwrap();
1695        assert_eq!(msg2.message.get_payload_str(), "event1");
1696    }
1697
1698    #[tokio::test]
1699    async fn test_memory_no_subscribers_persistence() {
1700        let topic = format!("no_subs_{}", fast_uuid_v7::gen_id_str());
1701        let pub_config = MemoryConfig {
1702            topic: topic.clone(),
1703            subscribe_mode: true,
1704            ..Default::default()
1705        };
1706
1707        let publisher = MemoryPublisher::new(&pub_config).unwrap();
1708
1709        publisher.send("msg1".into()).await.unwrap();
1710        publisher.send("msg2".into()).await.unwrap();
1711
1712        let sub_config = MemoryConfig {
1713            topic: topic.clone(),
1714            subscribe_mode: true,
1715            ..Default::default()
1716        };
1717        let mut subscriber = MemorySubscriber::new(&sub_config, "late_sub").unwrap();
1718
1719        let received1 = subscriber.receive().await.unwrap();
1720        assert_eq!(received1.message.get_payload_str(), "msg1");
1721        (received1.commit)(MessageDisposition::Ack).await.unwrap();
1722
1723        let received2 = subscriber.receive().await.unwrap();
1724        assert_eq!(received2.message.get_payload_str(), "msg2");
1725        (received2.commit)(MessageDisposition::Ack).await.unwrap();
1726    }
1727
1728    #[tokio::test]
1729    async fn test_memory_mixed_mode_error() {
1730        let topic_q = format!("mixed_q_{}", fast_uuid_v7::gen_id_str());
1731        let topic_l = format!("mixed_l_{}", fast_uuid_v7::gen_id_str());
1732
1733        // Case 1: Active Queue, try to create Log Consumer
1734        let _pub_q = MemoryPublisher::new_local(&topic_q, 10); // Creates Queue backend
1735
1736        let log_conf = MemoryConfig {
1737            topic: topic_q.clone(),
1738            subscribe_mode: true,
1739            ..Default::default()
1740        };
1741        let err = MemoryConsumer::new(&log_conf);
1742        assert!(err.is_err());
1743        assert!(err
1744            .unwrap_err()
1745            .to_string()
1746            .contains("already active as a Queue"));
1747
1748        // Case 2: Active Log, try to create Queue Consumer
1749        let log_pub_conf = MemoryConfig {
1750            topic: topic_l.clone(),
1751            subscribe_mode: true,
1752            ..Default::default()
1753        };
1754        let _pub_l = MemoryPublisher::new(&log_pub_conf).unwrap(); // Creates Log backend
1755
1756        let queue_conf = MemoryConfig {
1757            topic: topic_l.clone(),
1758            subscribe_mode: false,
1759            ..Default::default()
1760        };
1761        let err = MemoryConsumer::new(&queue_conf);
1762        assert!(err.is_err());
1763        assert!(err
1764            .unwrap_err()
1765            .to_string()
1766            .contains("already active as a Subscriber Log"));
1767    }
1768
1769    #[tokio::test]
1770    async fn test_memory_publisher_mixed_mode_error() {
1771        let topic_q = format!("pub_mixed_q_{}", fast_uuid_v7::gen_id_str());
1772
1773        // Create a Queue Consumer to establish the channel
1774        let _cons_q = MemoryConsumer::new_local(&topic_q, 10);
1775
1776        // Try to create a Log Publisher on the same topic
1777        let log_conf = MemoryConfig {
1778            topic: topic_q.clone(),
1779            subscribe_mode: true,
1780            ..Default::default()
1781        };
1782        let err = MemoryPublisher::new(&log_conf);
1783        assert!(err.is_err());
1784        assert!(err
1785            .unwrap_err()
1786            .to_string()
1787            .contains("already active as a Queue"));
1788    }
1789
1790    #[tokio::test]
1791    async fn test_memory_publisher_adaptive_behavior() {
1792        let topic = format!("adaptive_{}", fast_uuid_v7::gen_id_str());
1793
1794        // Create a Log Consumer (Subscriber) to establish the EventStore
1795        let sub_config = MemoryConfig {
1796            topic: topic.clone(),
1797            subscribe_mode: true,
1798            ..Default::default()
1799        };
1800        let mut subscriber = MemorySubscriber::new(&sub_config, "sub1").unwrap();
1801
1802        // Create a Publisher WITHOUT subscribe_mode explicitly set
1803        let pub_config = MemoryConfig {
1804            topic: topic.clone(),
1805            subscribe_mode: false, // Default is false
1806            ..Default::default()
1807        };
1808        // This should succeed and adapt to Log mode because the store exists
1809        let publisher = MemoryPublisher::new(&pub_config).unwrap();
1810
1811        // Verify it publishes to the store (subscriber receives it)
1812        publisher.send("adaptive_msg".into()).await.unwrap();
1813
1814        let received = subscriber.receive().await.unwrap();
1815        assert_eq!(received.message.get_payload_str(), "adaptive_msg");
1816    }
1817}