1use 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
33static RUNTIME_MEMORY_CHANNELS: Lazy<Mutex<HashMap<String, MemoryChannel>>> =
36 Lazy::new(|| Mutex::new(HashMap::new()));
37
38static RUNTIME_RESPONSE_CHANNELS: Lazy<Mutex<HashMap<String, MemoryResponseChannel>>> =
40 Lazy::new(|| Mutex::new(HashMap::new()));
41
42#[derive(Debug, Clone)]
47pub struct MemoryChannel {
48 pub sender: Sender<Vec<CanonicalMessage>>,
49 pub receiver: Receiver<Vec<CanonicalMessage>>,
50}
51
52impl MemoryChannel {
53 pub fn new(capacity: usize) -> Self {
55 let (sender, receiver) = bounded(capacity);
56 Self { sender, receiver }
57 }
58
59 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 pub async fn fill_messages(&self, messages: Vec<CanonicalMessage>) -> anyhow::Result<()> {
68 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 pub fn close(&self) {
78 self.sender.close();
79 }
80
81 pub fn drain_messages(&self) -> Vec<CanonicalMessage> {
83 let mut messages = Vec::new();
84 while let Ok(batch) = self.receiver.try_recv() {
86 messages.extend(batch);
87 }
88 messages
89 }
90
91 pub fn len(&self) -> usize {
93 self.receiver.len()
94 }
95
96 pub fn is_empty(&self) -> bool {
98 self.receiver.is_empty()
99 }
100}
101
102#[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
163pub 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()) .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
176pub 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#[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#[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 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 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 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 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 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 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#[derive(Debug)]
519pub struct MemoryQueueConsumer {
520 topic: String,
521 receiver: Receiver<Vec<CanonicalMessage>>,
522 buffer: Vec<CanonicalMessage>,
524 enable_nack: bool,
525 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 requeue: Arc<Mutex<Vec<CanonicalMessage>>>,
542 enable_nack: bool,
543 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#[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 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 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 self.buffer.is_empty() {
691 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 self.buffer.reverse();
704 }
705
706 let num_to_take = self.buffer.len().min(max_messages);
708 let split_at = self.buffer.len() - num_to_take;
709
710 let mut messages = self.buffer.split_off(split_at);
713 messages.reverse(); Ok(messages)
715 }
716}
717
718fn 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 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 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 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 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_messages(&topic, to_requeue);
869
870 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 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 {
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 if messages.is_empty() {
927 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 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 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 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 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 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 publisher.send(msg.clone()).await.unwrap();
1175
1176 sleep(std::time::Duration::from_millis(10)).await;
1177 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 #[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 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 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 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 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 #[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 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 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 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 assert_eq!(publisher.channel().len(), 2);
1341
1342 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 assert_eq!(publisher.channel().len(), 0);
1359
1360 }
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 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 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 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 let topic = format!("nack_requeue_{}", fast_uuid_v7::gen_id_str());
1579 let config = MemoryConfig {
1580 topic: topic.clone(),
1581 capacity: Some(1), 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 publisher.send_batch(vec!["B".into()]).await.unwrap();
1594
1595 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 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 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 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 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 let mut sub1 = MemorySubscriber::new(&pub_config, "sub1").unwrap();
1685 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 let _pub_q = MemoryPublisher::new_local(&topic_q, 10); 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 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(); 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 let _cons_q = MemoryConsumer::new_local(&topic_q, 10);
1775
1776 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 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 let pub_config = MemoryConfig {
1804 topic: topic.clone(),
1805 subscribe_mode: false, ..Default::default()
1807 };
1808 let publisher = MemoryPublisher::new(&pub_config).unwrap();
1810
1811 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}