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}
526
527#[derive(Clone)]
528pub struct TransportQueueConsumer {
529 topic: String,
530 transport: Arc<dyn TransportChannel>,
531 buffer: Vec<CanonicalMessage>,
532 enable_nack: bool,
533}
534
535impl fmt::Debug for TransportQueueConsumer {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 f.debug_struct("TransportQueueConsumer")
538 .field("topic", &self.topic)
539 .field("enable_nack", &self.enable_nack)
540 .finish_non_exhaustive()
541 }
542}
543
544#[derive(Debug)]
546pub enum MemoryConsumer {
547 Queue(MemoryQueueConsumer),
548 Transport(TransportQueueConsumer),
549 Log {
550 consumer: EventStoreConsumer,
551 topic: String,
552 },
553}
554
555impl MemoryConsumer {
556 pub fn new(config: &MemoryConfig) -> anyhow::Result<Self> {
557 let config = normalized_memory_config(config)?;
558 let channel_exists = memory_channel_exists(&config.topic);
559 let store_exists = event_store_exists(&config.topic);
560
561 if config.subscribe_mode {
562 if channel_exists {
563 return Err(anyhow!("Topic '{}' is already active as a Queue (MemoryChannel), but Subscriber mode (EventStore) was requested.", config.topic));
564 }
565 let store = get_or_create_event_store(&config.topic);
566 let subscriber_id = format!("{}-consumer", config.topic);
570 info!(topic = %config.topic, subscriber_id = %subscriber_id, "Memory consumer (Log mode) connected");
571 let consumer = store.consumer(subscriber_id);
572 Ok(Self::Log {
573 consumer,
574 topic: config.topic.clone(),
575 })
576 } else {
577 if store_exists {
578 return Err(anyhow!("Topic '{}' is already active as a Subscriber Log (EventStore), but Queue mode (MemoryChannel) was requested.", config.topic));
583 }
584 let queue = MemoryQueueConsumer::new(&config)?;
585 Ok(Self::Queue(queue))
586 }
587 }
588
589 pub async fn new_async(config: &MemoryConfig) -> anyhow::Result<Self> {
590 let url = resolved_transport(config)?;
591 match &url {
592 TransportUrl::Memory { .. } => Self::new(config),
593 _ => {
594 if config.subscribe_mode {
595 return Err(anyhow!(
596 "IPC memory consumers do not support subscribe_mode"
597 ));
598 }
599 let config = config.clone().with_smart_defaults();
600 let capacity = config.capacity.unwrap_or(100);
601 let transport = create_transport_from_url(&url, capacity, true).await?;
602 Ok(Self::Transport(TransportQueueConsumer {
603 topic: url.display_name(),
604 transport,
605 buffer: Vec::new(),
606 enable_nack: config.enable_nack,
607 }))
608 }
609 }
610 }
611}
612
613impl Drop for MemoryQueueConsumer {
614 fn drop(&mut self) {
615 if !self.buffer.is_empty() {
616 let mut messages = std::mem::take(&mut self.buffer);
617 messages.reverse();
618
619 let channel = get_or_create_channel(&MemoryConfig {
620 topic: self.topic.clone(),
621 capacity: None,
622 ..Default::default()
623 });
624
625 match channel.sender.try_send(messages) {
626 Ok(_) => {
627 info!(topic = %self.topic, "Requeued buffered messages on consumer drop");
628 }
629 Err(e) => {
630 let msgs = match e {
631 async_channel::TrySendError::Full(m) => m,
632 async_channel::TrySendError::Closed(m) => m,
633 };
634 warn!(topic = %self.topic, "Channel full on drop, spawning async requeue");
635 let sender = channel.sender.clone();
636 if let Ok(handle) = tokio::runtime::Handle::try_current() {
637 handle.spawn(async move {
638 if let Err(e) = sender.send(msgs).await {
639 tracing::error!(
640 "Failed to requeue buffered messages in background: {}",
641 e
642 );
643 }
644 });
645 } else {
646 tracing::error!(topic = %self.topic, "No active runtime found, could not requeue buffered messages on consumer drop");
647 }
648 }
649 }
650 }
651 }
652}
653
654impl MemoryQueueConsumer {
655 pub fn new(config: &MemoryConfig) -> anyhow::Result<Self> {
656 let channel = get_or_create_channel(config);
657 let buffer = if let Some(capacity) = config.capacity {
658 Vec::with_capacity(capacity)
659 } else {
660 Vec::new()
661 };
662 Ok(Self {
663 topic: config.topic.clone(),
664 receiver: channel.receiver.clone(),
665 buffer,
666 enable_nack: config.enable_nack,
667 })
668 }
669
670 async fn get_buffered_msgs(
671 &mut self,
672 max_messages: usize,
673 ) -> Result<Vec<CanonicalMessage>, ConsumerError> {
674 if self.buffer.is_empty() {
676 self.buffer = match self.receiver.recv().await {
678 Ok(batch) => batch,
679 Err(_) => return Err(ConsumerError::EndOfStream),
680 };
681 self.buffer.reverse();
683 }
684
685 let num_to_take = self.buffer.len().min(max_messages);
687 let split_at = self.buffer.len() - num_to_take;
688
689 let mut messages = self.buffer.split_off(split_at);
692 messages.reverse(); Ok(messages)
694 }
695}
696
697struct RequeueGuard {
698 topic: String,
699 messages: Vec<CanonicalMessage>,
700}
701
702impl Drop for RequeueGuard {
703 fn drop(&mut self) {
704 if !self.messages.is_empty() {
705 let topic = self.topic.clone();
706 let count = self.messages.len();
707 let messages = std::mem::take(&mut self.messages);
708
709 let channel = get_or_create_channel(&MemoryConfig {
710 topic: topic.clone(),
711 capacity: None,
712 ..Default::default()
713 });
714
715 match channel.sender.try_send(messages) {
716 Ok(_) => {
717 tracing::info!(topic = %topic, count, "Requeued dropped batch via RequeueGuard");
718 }
719 Err(e) => {
720 let msgs = match e {
721 async_channel::TrySendError::Full(m) => m,
722 async_channel::TrySendError::Closed(m) => m,
723 };
724 tracing::warn!(topic = %topic, count, "Failed to requeue dropped batch (channel full/closed), spawning retry");
725 let sender = channel.sender.clone();
726 if let Ok(handle) = tokio::runtime::Handle::try_current() {
727 handle.spawn(async move {
728 if let Err(e) = sender.send(msgs).await {
729 tracing::error!(
730 "Failed to requeue dropped batch in background: {}",
731 e
732 );
733 }
734 });
735 } else {
736 tracing::error!(topic = %topic, count, "No active runtime found, could not requeue dropped batch via RequeueGuard");
737 }
738 }
739 }
740 }
741 }
742}
743
744#[async_trait]
745impl MessageConsumer for MemoryQueueConsumer {
746 async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
747 let mut messages = self.get_buffered_msgs(max_messages).await?;
750 while messages.len() < max_messages / 2 {
751 if let Ok(mut next_batch) = self.receiver.try_recv() {
752 if next_batch.len() + messages.len() > max_messages {
753 let needed = max_messages - messages.len();
754 let mut to_buffer = next_batch.split_off(needed);
755 messages.append(&mut next_batch);
756 self.buffer.append(&mut to_buffer);
757 self.buffer.reverse();
758 break;
759 } else {
760 messages.append(&mut next_batch);
761 }
762 } else {
763 break;
764 }
765 }
766 trace!(count = messages.len(), topic = %self.topic, message_ids = ?LazyMessageIds(&messages), "Received batch of memory messages");
767 if messages.is_empty() {
768 return Ok(ReceivedBatch {
769 messages: Vec::new(),
770 commit: Box::new(|_| {
771 Box::pin(async move { Ok(()) }) as BoxFuture<'static, anyhow::Result<()>>
772 }),
773 });
774 }
775
776 let topic = self.topic.clone();
777 let expected_count = messages.len();
778 let correlation_ids: Vec<Option<String>> = messages
779 .iter()
780 .map(|m| m.metadata.get("correlation_id").cloned())
781 .collect();
782
783 let mut guard = if self.enable_nack {
785 Some(RequeueGuard {
786 topic: self.topic.clone(),
787 messages: messages.clone(),
788 })
789 } else {
790 None
791 };
792
793 let commit = Box::new(move |dispositions: Vec<MessageDisposition>| {
794 Box::pin(async move {
795 if dispositions.len() != expected_count {
796 return Err(anyhow::anyhow!(
797 "Memory batch commit received mismatched disposition count: expected {}, got {}",
798 expected_count,
799 dispositions.len()
800 ));
801 }
802
803 let messages_for_retry = if let Some(g) = &guard {
805 g.messages.clone()
806 } else {
807 Vec::new()
808 };
809
810 let response_channel = get_or_create_response_channel(&topic);
811 let mut to_requeue = Vec::new();
812
813 for (i, disposition) in dispositions.into_iter().enumerate() {
814 match disposition {
815 MessageDisposition::Reply(resp) => {
816 handle_memory_reply(resp, i, &correlation_ids, &response_channel).await;
817 }
818 MessageDisposition::Nack => {
819 if let Some(msg) = messages_for_retry.get(i) {
820 warn!("Requeueing nacked message {}", i);
821 to_requeue.push(msg.clone());
822 } else {
823 warn!("Nack for index {} but no message in retry buffer!", i);
824 }
825 }
826 MessageDisposition::Ack => {}
827 }
828 }
829
830 if !to_requeue.is_empty() {
831 let main_channel = get_or_create_channel(&MemoryConfig {
832 topic: topic.to_string(),
833 capacity: None,
834 ..Default::default()
835 });
836 if main_channel.sender.send(to_requeue).await.is_err() {
837 tracing::error!("Failed to re-queue NACKed messages to memory channel as it was closed.");
838 }
839 }
840
841 if let Some(g) = &mut guard {
843 std::mem::take(&mut g.messages);
844 }
845
846 Ok(())
847 }) as BoxFuture<'static, anyhow::Result<()>>
848 }) as BatchCommitFunc;
849 Ok(ReceivedBatch { messages, commit })
850 }
851
852 async fn status(&self) -> EndpointStatus {
853 let pending = self.receiver.len();
854 let capacity = self.receiver.capacity().unwrap_or(0);
855 EndpointStatus {
856 healthy: !self.receiver.is_closed(),
857 target: self.topic.clone(),
858 pending: Some(pending),
859 capacity: Some(capacity),
860 ..Default::default()
861 }
862 }
863
864 fn as_any(&self) -> &dyn Any {
865 self
866 }
867}
868
869#[async_trait]
870impl MessageConsumer for TransportQueueConsumer {
871 async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
872 let mut messages = Vec::with_capacity(max_messages);
873 let buffered = self.buffer.len().min(max_messages);
874 if buffered > 0 {
875 messages.extend(self.buffer.drain(..buffered));
876 }
877
878 if messages.len() < max_messages {
879 let mut received = self.transport.recv_batch().await.map_err(|e| {
880 ConsumerError::Connection(anyhow!("Failed to receive via memory transport: {}", e))
881 })?;
882 messages.append(&mut received);
883 if messages.len() > max_messages {
884 self.buffer = messages.split_off(max_messages);
885 }
886 }
887
888 trace!(count = messages.len(), topic = %self.topic, message_ids = ?LazyMessageIds(&messages), "Received batch from memory transport");
889
890 let topic = self.topic.clone();
891 let transport = self.transport.clone();
892 let enable_nack = self.enable_nack;
893 let expected_count = messages.len();
894 let messages_for_retry = if enable_nack {
895 messages.clone()
896 } else {
897 Vec::new()
898 };
899
900 let commit = Box::new(move |dispositions: Vec<MessageDisposition>| {
901 let transport = transport.clone();
902 let topic = topic.clone();
903 let messages_for_retry = messages_for_retry.clone();
904 Box::pin(async move {
905 if dispositions.len() != expected_count {
906 return Err(anyhow::anyhow!(
907 "Memory transport batch commit received mismatched disposition count: expected {}, got {}",
908 expected_count,
909 dispositions.len()
910 ));
911 }
912
913 let mut to_requeue = Vec::new();
914 for (i, disposition) in dispositions.into_iter().enumerate() {
915 match disposition {
916 MessageDisposition::Nack if enable_nack => {
917 if let Some(msg) = messages_for_retry.get(i) {
918 to_requeue.push(msg.clone());
919 }
920 }
921 MessageDisposition::Reply(_) => {
922 tracing::warn!(topic = %topic, "IPC memory transport does not support reply dispositions");
923 }
924 MessageDisposition::Ack | MessageDisposition::Nack => {}
925 }
926 }
927
928 if !to_requeue.is_empty() {
929 transport.send_batch(to_requeue).await?;
930 }
931
932 Ok(())
933 }) as BoxFuture<'static, anyhow::Result<()>>
934 }) as BatchCommitFunc;
935
936 Ok(ReceivedBatch { messages, commit })
937 }
938
939 async fn status(&self) -> EndpointStatus {
940 EndpointStatus {
941 healthy: !self.transport.is_closed(),
942 target: self.topic.clone(),
943 pending: Some(self.transport.len()),
944 capacity: self.transport.capacity(),
945 details: serde_json::json!({
946 "mode": "transport"
947 }),
948 ..Default::default()
949 }
950 }
951
952 fn as_any(&self) -> &dyn Any {
953 self
954 }
955}
956
957async fn handle_memory_reply(
958 mut resp: CanonicalMessage,
959 index: usize,
960 correlation_ids: &[Option<String>],
961 response_channel: &MemoryResponseChannel,
962) {
963 if !resp.metadata.contains_key("correlation_id") {
964 if let Some(Some(cid)) = correlation_ids.get(index) {
965 resp.metadata
966 .insert("correlation_id".to_string(), cid.clone());
967 }
968 }
969
970 if let Some(cid) = resp.metadata.get("correlation_id") {
971 if let Some(tx) = response_channel.remove_waiter(cid).await {
972 let _ = tx.send(resp);
973 return;
974 }
975 }
976 let _ = response_channel.sender.send(resp).await;
977}
978
979#[async_trait]
980impl MessageConsumer for MemoryConsumer {
981 async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
982 match self {
983 Self::Queue(q) => q.receive_batch(max_messages).await,
984 Self::Transport(t) => t.receive_batch(max_messages).await,
985 Self::Log { consumer, .. } => consumer.receive_batch(max_messages).await,
986 }
987 }
988
989 async fn status(&self) -> EndpointStatus {
990 match self {
991 Self::Queue(q) => q.status().await,
992 Self::Transport(t) => t.status().await,
993 Self::Log { consumer, .. } => consumer.status().await,
994 }
995 }
996
997 fn as_any(&self) -> &dyn Any {
998 self
999 }
1000}
1001
1002impl MemoryConsumer {
1003 pub fn new_local(topic: &str, capacity: usize) -> Self {
1004 Self::new(&MemoryConfig {
1005 topic: topic.to_string(),
1006 capacity: Some(capacity),
1007 ..Default::default()
1008 })
1009 .expect("Failed to create local memory consumer")
1010 }
1011 pub fn channel(&self) -> MemoryChannel {
1012 let topic = match self {
1013 Self::Queue(q) => &q.topic,
1014 Self::Transport(t) => &t.topic,
1015 Self::Log { topic, .. } => topic,
1016 };
1017 get_or_create_channel(&MemoryConfig {
1018 topic: topic.clone(),
1019 ..Default::default()
1020 })
1021 }
1022}
1023
1024pub struct MemorySubscriber {
1025 consumer: MemoryConsumer,
1026}
1027
1028impl MemorySubscriber {
1029 pub fn new(config: &MemoryConfig, id: &str) -> anyhow::Result<Self> {
1030 let mut sub_config = config.clone();
1031 let consumer = if config.subscribe_mode {
1034 let store = get_or_create_event_store(&config.topic);
1035 MemoryConsumer::Log {
1036 consumer: store.consumer(id.to_string()),
1037 topic: config.topic.clone(),
1038 }
1039 } else {
1040 sub_config.topic = format!("{}-{}", config.topic, id);
1041 MemoryConsumer::new(&sub_config)?
1042 };
1043 Ok(Self { consumer })
1044 }
1045}
1046
1047#[async_trait]
1048impl MessageConsumer for MemorySubscriber {
1049 async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
1050 self.consumer.receive_batch(max_messages).await
1051 }
1052
1053 async fn receive(&mut self) -> Result<Received, ConsumerError> {
1054 self.consumer.receive().await
1055 }
1056
1057 fn as_any(&self) -> &dyn Any {
1058 self
1059 }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064 use super::*;
1065 use crate::models::{Endpoint, Route};
1066 use crate::traits::Handled;
1067 use crate::{msg, CanonicalMessage};
1068 use serde_json::json;
1069 use tokio::time::sleep;
1070
1071 #[tokio::test]
1072 async fn test_memory_channel_integration() {
1073 let mut consumer = MemoryConsumer::new_local("test-mem1", 10);
1074 let publisher = MemoryPublisher::new_local("test-mem1", 10);
1075
1076 let msg = msg!(json!({"hello": "memory"}));
1077
1078 publisher.send(msg.clone()).await.unwrap();
1080
1081 sleep(std::time::Duration::from_millis(10)).await;
1082 let received = consumer.receive().await.unwrap();
1084 let _ = (received.commit)(MessageDisposition::Ack).await;
1085 assert_eq!(received.message.payload, msg.payload);
1086 assert_eq!(consumer.channel().len(), 0);
1087 }
1088
1089 #[tokio::test]
1090 async fn test_memory_url_alias_uses_same_channel_as_legacy_topic() {
1091 let mut consumer = MemoryConsumer::new(&MemoryConfig::new("test-memory-url", Some(10)))
1092 .expect("legacy topic consumer should be in-process memory");
1093 let publisher = MemoryPublisher::new_async(&MemoryConfig::new_with_url(
1094 "memory://test-memory-url",
1095 Some(10),
1096 ))
1097 .await
1098 .expect("memory URL publisher should be in-process memory");
1099
1100 let msg = msg!(json!({"hello": "memory-url"}));
1101 publisher.send(msg.clone()).await.unwrap();
1102
1103 let received = consumer.receive().await.unwrap();
1104 let _ = (received.commit)(MessageDisposition::Ack).await;
1105 assert_eq!(received.message.payload, msg.payload);
1106 }
1107
1108 #[cfg(unix)]
1109 #[tokio::test]
1110 async fn test_unix_ipc_endpoint_constructors_roundtrip() {
1111 let temp_dir = tempfile::TempDir::new().unwrap();
1112 let socket_path = temp_dir.path().join("endpoint.sock");
1113 let url = format!("unix://{}", socket_path.display());
1114 let config = MemoryConfig::new_with_url(url, Some(10));
1115
1116 assert!(config.clone().with_smart_defaults().enable_nack);
1117
1118 let mut consumer = MemoryConsumer::new_async(&config)
1119 .await
1120 .expect("IPC consumer should create a Unix socket server");
1121 let publisher = MemoryPublisher::new_async(&config)
1122 .await
1123 .expect("IPC publisher should connect to the Unix socket server");
1124
1125 let msg = CanonicalMessage::from_vec(b"endpoint-ipc");
1126 publisher.send(msg.clone()).await.unwrap();
1127
1128 let received = consumer.receive().await.unwrap();
1129 (received.commit)(MessageDisposition::Ack).await.unwrap();
1130 assert_eq!(received.message.payload.as_ref(), b"endpoint-ipc");
1131 }
1132
1133 #[tokio::test]
1134 async fn test_memory_publisher_and_consumer_integration() {
1135 let mut consumer = MemoryConsumer::new_local("test-mem2", 10);
1136 let publisher = MemoryPublisher::new_local("test-mem2", 10);
1137
1138 let msg1 = msg!(json!({"message": "one"}));
1139 let msg2 = msg!(json!({"message": "two"}));
1140 let msg3 = msg!(json!({"message": "three"}));
1141
1142 publisher
1144 .send_batch(vec![msg1.clone(), msg2.clone()])
1145 .await
1146 .unwrap();
1147 publisher.send(msg3.clone()).await.unwrap();
1148
1149 assert_eq!(publisher.channel().len(), 2);
1151
1152 let received1 = consumer.receive().await.unwrap();
1154 let _ = (received1.commit)(MessageDisposition::Ack).await;
1155 assert_eq!(received1.message.payload, msg1.payload);
1156
1157 let batch2 = consumer.receive_batch(1).await.unwrap();
1158 let (received_msg2, commit2) = (batch2.messages, batch2.commit);
1159 let _ = commit2(vec![MessageDisposition::Ack; received_msg2.len()]).await;
1160 assert_eq!(received_msg2.len(), 1);
1161 assert_eq!(received_msg2.first().unwrap().payload, msg2.payload);
1162 let batch3 = consumer.receive_batch(2).await.unwrap();
1163 let (received_msg3, commit3) = (batch3.messages, batch3.commit);
1164 let _ = commit3(vec![MessageDisposition::Ack; received_msg3.len()]).await;
1165 assert_eq!(received_msg3.first().unwrap().payload, msg3.payload);
1166
1167 assert_eq!(publisher.channel().len(), 0);
1169
1170 }
1174
1175 #[tokio::test]
1176 async fn test_memory_subscriber_structure() {
1177 let cfg = MemoryConfig {
1178 topic: "base_topic".to_string(),
1179 capacity: Some(10),
1180 ..Default::default()
1181 };
1182 let subscriber_id = "sub1";
1183 let mut subscriber = MemorySubscriber::new(&cfg, subscriber_id).unwrap();
1184
1185 let pub_cfg = MemoryConfig {
1188 topic: format!("base_topic-{}", subscriber_id),
1189 capacity: Some(10),
1190 ..Default::default()
1191 };
1192 let publisher = MemoryPublisher::new(&pub_cfg).unwrap();
1193
1194 publisher.send("hello subscriber".into()).await.unwrap();
1195
1196 let received = subscriber.receive().await.unwrap();
1197 assert_eq!(received.message.get_payload_str(), "hello subscriber");
1198 }
1199
1200 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1201 async fn test_memory_request_reply_mode() {
1202 let topic = format!("mem_rr_topic_{}", fast_uuid_v7::gen_id_str());
1203 let input_endpoint = Endpoint::new_memory(&topic, 10);
1204 let output_endpoint = Endpoint::new_response();
1205 let handler = |mut msg: CanonicalMessage| async move {
1206 let request_payload = msg.get_payload_str();
1207 let response_payload = format!("reply to {}", request_payload);
1208 msg.set_payload_str(response_payload);
1209 Ok(Handled::Publish(msg))
1210 };
1211
1212 let route = Route::new(input_endpoint, output_endpoint).with_handler(handler);
1213 route.deploy("mem_rr_test").await.unwrap();
1214
1215 let publisher = MemoryPublisher::new(&MemoryConfig {
1217 topic: topic.clone(),
1218 capacity: Some(10),
1219 request_reply: true,
1220 request_timeout_ms: Some(2000),
1221 ..Default::default()
1222 })
1223 .unwrap();
1224
1225 let result = publisher.send("direct request".into()).await.unwrap();
1226
1227 if let Sent::Response(response_msg) = result {
1228 assert_eq!(response_msg.get_payload_str(), "reply to direct request");
1229 } else {
1230 panic!("Expected Sent::Response, got {:?}", result);
1231 }
1232
1233 Route::stop("mem_rr_test").await;
1235 }
1236
1237 #[tokio::test]
1238 async fn test_memory_request_reply_timeout_cleans_waiter() {
1239 let topic = format!("mem_rr_timeout_{}", fast_uuid_v7::gen_id_str());
1240 let correlation_id = fast_uuid_v7::gen_id_string();
1241 let publisher = MemoryPublisher::new(&MemoryConfig {
1242 topic: topic.clone(),
1243 capacity: Some(10),
1244 request_reply: true,
1245 request_timeout_ms: Some(25),
1246 ..Default::default()
1247 })
1248 .unwrap();
1249
1250 let mut message = CanonicalMessage::from("request with no responder");
1251 message
1252 .metadata
1253 .insert("correlation_id".to_string(), correlation_id.clone());
1254
1255 let err = publisher.send(message).await.unwrap_err();
1256 assert!(err
1257 .to_string()
1258 .contains("Request timed out waiting for response"));
1259
1260 let response_channel = get_or_create_response_channel(&topic);
1261 assert!(
1262 response_channel
1263 .remove_waiter(&correlation_id)
1264 .await
1265 .is_none(),
1266 "timed out request should clean up the registered waiter"
1267 );
1268 }
1269
1270 #[tokio::test]
1271 async fn test_memory_nack_requeue() {
1272 let topic = format!("test_nack_requeue_{}", fast_uuid_v7::gen_id_str());
1273 let config = MemoryConfig {
1274 topic: topic.clone(),
1275 capacity: Some(10),
1276 enable_nack: true,
1277 ..Default::default()
1278 };
1279 let mut consumer = MemoryConsumer::new(&config).unwrap();
1280 let publisher = MemoryPublisher::new_local(&topic, 10);
1281
1282 publisher.send("to_be_nacked".into()).await.unwrap();
1283
1284 let received1 = consumer.receive().await.unwrap();
1286 assert_eq!(received1.message.get_payload_str(), "to_be_nacked");
1287 (received1.commit)(crate::traits::MessageDisposition::Nack)
1288 .await
1289 .unwrap();
1290
1291 let received2 = tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive())
1293 .await
1294 .expect("Timed out waiting for re-queued message")
1295 .unwrap();
1296 assert_eq!(received2.message.get_payload_str(), "to_be_nacked");
1297
1298 (received2.commit)(crate::traits::MessageDisposition::Ack)
1300 .await
1301 .unwrap();
1302
1303 let result =
1305 tokio::time::timeout(std::time::Duration::from_millis(100), consumer.receive()).await;
1306 assert!(result.is_err(), "Channel should be empty");
1307 }
1308
1309 #[tokio::test]
1310 async fn test_memory_dropped_batch_requeues_messages() {
1311 let topic = format!("drop_requeue_{}", fast_uuid_v7::gen_id_str());
1312 let config = MemoryConfig {
1313 topic: topic.clone(),
1314 capacity: Some(10),
1315 enable_nack: true,
1316 ..Default::default()
1317 };
1318 let mut consumer = MemoryConsumer::new(&config).unwrap();
1319 let publisher = MemoryPublisher::new_local(&topic, 10);
1320
1321 publisher
1322 .send_batch(vec!["first".into(), "second".into()])
1323 .await
1324 .unwrap();
1325
1326 let batch = consumer.receive_batch(2).await.unwrap();
1327 assert_eq!(batch.messages.len(), 2);
1328 drop(batch);
1329
1330 let requeued =
1331 tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive_batch(2))
1332 .await
1333 .expect("Timed out waiting for dropped batch to be re-queued")
1334 .unwrap();
1335
1336 assert_eq!(
1337 requeued
1338 .messages
1339 .iter()
1340 .map(CanonicalMessage::get_payload_str)
1341 .collect::<Vec<_>>(),
1342 vec!["first".to_string(), "second".to_string()]
1343 );
1344
1345 (requeued.commit)(vec![MessageDisposition::Ack, MessageDisposition::Ack])
1346 .await
1347 .unwrap();
1348 }
1349
1350 #[tokio::test]
1351 async fn test_memory_batch_commit_rejects_mismatched_dispositions() {
1352 let topic = format!("commit_mismatch_{}", fast_uuid_v7::gen_id_str());
1353 let config = MemoryConfig {
1354 topic: topic.clone(),
1355 capacity: Some(10),
1356 enable_nack: true,
1357 ..Default::default()
1358 };
1359 let mut consumer = MemoryConsumer::new(&config).unwrap();
1360 let publisher = MemoryPublisher::new_local(&topic, 10);
1361
1362 publisher
1363 .send_batch(vec!["one".into(), "two".into()])
1364 .await
1365 .unwrap();
1366
1367 let batch = consumer.receive_batch(2).await.unwrap();
1368 let err = (batch.commit)(vec![MessageDisposition::Ack])
1369 .await
1370 .unwrap_err();
1371 assert!(err
1372 .to_string()
1373 .contains("Memory batch commit received mismatched disposition count"));
1374
1375 let retried =
1376 tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive_batch(2))
1377 .await
1378 .expect("Timed out waiting for mismatched commit batch to be re-queued")
1379 .unwrap();
1380 assert_eq!(retried.messages.len(), 2);
1381 (retried.commit)(vec![MessageDisposition::Ack, MessageDisposition::Ack])
1382 .await
1383 .unwrap();
1384 }
1385
1386 #[tokio::test]
1387 async fn test_memory_event_store_integration() {
1388 let topic = "event_store_test";
1389 let pub_config = MemoryConfig {
1391 topic: topic.to_string(),
1392 subscribe_mode: true,
1393 ..Default::default()
1394 };
1395 let publisher = MemoryPublisher::new(&pub_config).unwrap();
1396
1397 let mut sub1 = MemorySubscriber::new(&pub_config, "sub1").unwrap();
1399 let mut sub2 = MemorySubscriber::new(&pub_config, "sub2").unwrap();
1401
1402 publisher.send("event1".into()).await.unwrap();
1403
1404 let msg1 = sub1.receive().await.unwrap();
1405 assert_eq!(msg1.message.get_payload_str(), "event1");
1406 (msg1.commit)(MessageDisposition::Ack).await.unwrap();
1407
1408 let msg2 = sub2.receive().await.unwrap();
1409 assert_eq!(msg2.message.get_payload_str(), "event1");
1410 }
1411
1412 #[tokio::test]
1413 async fn test_memory_no_subscribers_persistence() {
1414 let topic = format!("no_subs_{}", fast_uuid_v7::gen_id_str());
1415 let pub_config = MemoryConfig {
1416 topic: topic.clone(),
1417 subscribe_mode: true,
1418 ..Default::default()
1419 };
1420
1421 let publisher = MemoryPublisher::new(&pub_config).unwrap();
1423
1424 publisher.send("msg1".into()).await.unwrap();
1426 publisher.send("msg2".into()).await.unwrap();
1427
1428 let sub_config = MemoryConfig {
1430 topic: topic.clone(),
1431 subscribe_mode: true,
1432 ..Default::default()
1433 };
1434 let mut subscriber = MemorySubscriber::new(&sub_config, "late_sub").unwrap();
1435
1436 let received1 = subscriber.receive().await.unwrap();
1438 assert_eq!(received1.message.get_payload_str(), "msg1");
1439 (received1.commit)(MessageDisposition::Ack).await.unwrap();
1440
1441 let received2 = subscriber.receive().await.unwrap();
1442 assert_eq!(received2.message.get_payload_str(), "msg2");
1443 (received2.commit)(MessageDisposition::Ack).await.unwrap();
1444 }
1445
1446 #[tokio::test]
1447 async fn test_memory_mixed_mode_error() {
1448 let topic_q = format!("mixed_q_{}", fast_uuid_v7::gen_id_str());
1449 let topic_l = format!("mixed_l_{}", fast_uuid_v7::gen_id_str());
1450
1451 let _pub_q = MemoryPublisher::new_local(&topic_q, 10); let log_conf = MemoryConfig {
1455 topic: topic_q.clone(),
1456 subscribe_mode: true,
1457 ..Default::default()
1458 };
1459 let err = MemoryConsumer::new(&log_conf);
1460 assert!(err.is_err());
1461 assert!(err
1462 .unwrap_err()
1463 .to_string()
1464 .contains("already active as a Queue"));
1465
1466 let log_pub_conf = MemoryConfig {
1468 topic: topic_l.clone(),
1469 subscribe_mode: true,
1470 ..Default::default()
1471 };
1472 let _pub_l = MemoryPublisher::new(&log_pub_conf).unwrap(); let queue_conf = MemoryConfig {
1475 topic: topic_l.clone(),
1476 subscribe_mode: false,
1477 ..Default::default()
1478 };
1479 let err = MemoryConsumer::new(&queue_conf);
1480 assert!(err.is_err());
1481 assert!(err
1482 .unwrap_err()
1483 .to_string()
1484 .contains("already active as a Subscriber Log"));
1485 }
1486
1487 #[tokio::test]
1488 async fn test_memory_publisher_mixed_mode_error() {
1489 let topic_q = format!("pub_mixed_q_{}", fast_uuid_v7::gen_id_str());
1490
1491 let _cons_q = MemoryConsumer::new_local(&topic_q, 10);
1493
1494 let log_conf = MemoryConfig {
1496 topic: topic_q.clone(),
1497 subscribe_mode: true,
1498 ..Default::default()
1499 };
1500 let err = MemoryPublisher::new(&log_conf);
1501 assert!(err.is_err());
1502 assert!(err
1503 .unwrap_err()
1504 .to_string()
1505 .contains("already active as a Queue"));
1506 }
1507
1508 #[tokio::test]
1509 async fn test_memory_publisher_adaptive_behavior() {
1510 let topic = format!("adaptive_{}", fast_uuid_v7::gen_id_str());
1511
1512 let sub_config = MemoryConfig {
1514 topic: topic.clone(),
1515 subscribe_mode: true,
1516 ..Default::default()
1517 };
1518 let mut subscriber = MemorySubscriber::new(&sub_config, "sub1").unwrap();
1519
1520 let pub_config = MemoryConfig {
1522 topic: topic.clone(),
1523 subscribe_mode: false, ..Default::default()
1525 };
1526 let publisher = MemoryPublisher::new(&pub_config).unwrap();
1528
1529 publisher.send("adaptive_msg".into()).await.unwrap();
1531
1532 let received = subscriber.receive().await.unwrap();
1533 assert_eq!(received.message.get_payload_str(), "adaptive_msg");
1534 }
1535}