monocoque_zmtp/proxy.rs
1//! Message proxy (broker) implementation for ZeroMQ patterns.
2//!
3//! A proxy connects frontend and backend sockets, forwarding messages
4//! bidirectionally. This enables common patterns like message brokers,
5//! load balancers, and forwarders without application logic.
6//!
7//! # Supported Patterns
8//!
9//! - **PUB-SUB broker**: XSUB frontend ←→ XPUB backend
10//! - **REQ-REP load balancer**: ROUTER frontend ←→ DEALER backend
11//! - **PUSH-PULL forwarder**: PULL frontend ←→ PUSH backend
12//!
13//! # Message Flow
14//!
15//! ```text
16//! Publishers → XSUB (frontend) → XPUB (backend) → Subscribers
17//! Clients → ROUTER (frontend) → DEALER (backend) → Workers
18//! ```
19//!
20//! # Example: PUB-SUB Broker
21//!
22//! ```rust,ignore
23//! use monocoque_zmtp::proxy::{proxy, ProxySocket};
24//! use monocoque_zmtp::xsub::XSubSocket;
25//! use monocoque_zmtp::xpub::XPubSocket;
26//!
27//! #[compio::main]
28//! async fn main() -> std::io::Result<()> {
29//! // Publishers connect to 5555
30//! let mut frontend = XSubSocket::bind("127.0.0.1:5555").await?;
31//!
32//! // Subscribers connect to 5556
33//! let mut backend = XPubSocket::bind("127.0.0.1:5556").await?;
34//!
35//! // Forward messages and subscriptions bidirectionally
36//! proxy(&mut frontend, &mut backend, None).await?;
37//! Ok(())
38//! }
39//! ```
40//!
41//! # Example: REQ-REP Load Balancer
42//!
43//! ```rust,ignore
44//! use monocoque_zmtp::proxy::{proxy, ProxySocket};
45//! use monocoque_zmtp::router::RouterSocket;
46//! use monocoque_zmtp::dealer::DealerSocket;
47//!
48//! #[compio::main]
49//! async fn main() -> std::io::Result<()> {
50//! // Clients connect to 5555
51//! let mut frontend = RouterSocket::bind("127.0.0.1:5555").await?;
52//!
53//! // Workers connect to 5556
54//! let mut backend = DealerSocket::bind("127.0.0.1:5556").await?;
55//!
56//! // Load balance requests across workers
57//! proxy(&mut frontend, &mut backend, None).await?;
58//! Ok(())
59//! }
60//! ```
61
62use bytes::Bytes;
63use std::io;
64use tracing::debug;
65
66// Import socket types
67use crate::dealer::DealerSocket;
68use crate::pair::PairSocket;
69use crate::publisher::PubSocket;
70use crate::pull::PullSocket;
71use crate::push::PushSocket;
72use crate::rep::RepSocket;
73use crate::req::ReqSocket;
74use crate::router::RouterSocket;
75use crate::subscriber::SubSocket;
76use crate::xpub::XPubSocket;
77use crate::xsub::XSubSocket;
78
79/// Whether a forward-side send error is transient (the frame can be dropped and
80/// the proxy kept running) or fatal (the peer is gone and the loop should stop).
81///
82/// Transient: `WouldBlock` (HWM/EAGAIN), `Interrupted`, `TimedOut`. A single
83/// such hiccup must not tear down the whole proxy. Everything else (broken pipe,
84/// reset, not connected) is treated as fatal and propagates, so a permanently
85/// dead peer cannot spin the loop forwarding-and-dropping forever.
86fn is_transient_send_error(err: &io::Error) -> bool {
87 matches!(
88 err.kind(),
89 io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted | io::ErrorKind::TimedOut
90 )
91}
92
93/// Socket types that can participate in a proxy.
94///
95/// Sockets must implement multipart message send/receive operations
96/// to be used in a proxy pattern.
97///
98/// Note: This trait is designed for single-threaded async runtimes like compio
99/// and does not require `Send`.
100///
101/// Uses native async-fn-in-trait rather than `#[async_trait]`: the proxy loop
102/// forwards every message through `recv_multipart`/`send_multipart`, and boxing
103/// each of those futures per message was pure overhead on this thread-per-core
104/// runtime. AFIT removes the per-message heap allocation. `ProxySocket` is only
105/// ever used behind generics (`proxy<F, B, C>`), never as `dyn ProxySocket`, so
106/// object safety does not apply; the public-AFIT Send-bound lint is irrelevant
107/// to this deliberately `!Send` API.
108#[allow(async_fn_in_trait)]
109pub trait ProxySocket {
110 /// Receive a multipart message from the socket.
111 ///
112 /// Returns `None` if no message is available or connection closed.
113 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>>;
114
115 /// Send a multipart message to the socket.
116 ///
117 /// # Errors
118 ///
119 /// Returns an error if the send operation fails.
120 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()>;
121
122 /// Get a description of the socket for logging.
123 fn socket_desc(&self) -> &'static str;
124}
125
126/// Run a bidirectional message proxy between frontend and backend sockets.
127///
128/// Messages are forwarded in both directions:
129/// - Frontend → Backend
130/// - Backend → Frontend
131///
132/// An optional capture socket receives copies of all messages for monitoring.
133///
134/// # Parameters
135///
136/// - `frontend`: Socket facing clients/publishers
137/// - `backend`: Socket facing workers/subscribers
138/// - `capture`: Optional socket to receive message copies
139///
140/// # Patterns
141///
142/// - **PUB-SUB**: `XSUB` (frontend) ←→ `XPUB` (backend)
143/// - **REQ-REP**: `ROUTER` (frontend) ←→ `DEALER` (backend)
144/// - **PUSH-PULL**: `PULL` (frontend) ←→ `PUSH` (backend)
145///
146/// # Blocking
147///
148/// This function runs forever, forwarding messages until an error occurs.
149///
150/// # Errors
151///
152/// Returns an error if a socket operation fails.
153///
154/// # Example
155///
156/// ```rust,ignore
157/// use monocoque_zmtp::proxy::{proxy, ProxySocket};
158/// use monocoque_zmtp::xsub::XSubSocket;
159/// use monocoque_zmtp::xpub::XPubSocket;
160///
161/// #[compio::main]
162/// async fn main() -> std::io::Result<()> {
163/// let mut frontend = XSubSocket::bind("127.0.0.1:5555").await?;
164/// let mut backend = XPubSocket::bind("127.0.0.1:5556").await?;
165///
166/// proxy(&mut frontend, &mut backend, None).await
167/// }
168/// ```
169pub async fn proxy<F, B, C>(
170 frontend: &mut F,
171 backend: &mut B,
172 mut capture: Option<&mut C>,
173) -> io::Result<()>
174where
175 F: ProxySocket,
176 B: ProxySocket,
177 C: ProxySocket,
178{
179 use futures::{FutureExt, select};
180
181 debug!(
182 "Starting proxy: {} ←→ {}",
183 frontend.socket_desc(),
184 backend.socket_desc()
185 );
186
187 loop {
188 // Use select! to multiplex between frontend and backend in single-threaded runtime
189 select! {
190 // Forward frontend → backend
191 msg_result = frontend.recv_multipart().fuse() => {
192 if let Some(msg) = msg_result? {
193 debug!("Proxy: {} → {}: {} frames",
194 frontend.socket_desc(),
195 backend.socket_desc(),
196 msg.len());
197
198 // Send copy to capture if present
199 if let Some(ref mut cap) = capture
200 && let Err(e) = cap.send_multipart(msg.clone()).await
201 {
202 debug!("Capture socket send failed: {}", e);
203 }
204
205 // Forward to backend. A transient error (HWM/EAGAIN) drops
206 // this frame but keeps the proxy alive; a fatal error tears
207 // the loop down.
208 if let Err(e) = backend.send_multipart(msg).await {
209 if is_transient_send_error(&e) {
210 debug!("Proxy: transient send to {}, dropping frame: {}",
211 backend.socket_desc(), e);
212 } else {
213 return Err(e);
214 }
215 }
216 }
217 }
218
219 // Forward backend → frontend
220 msg_result = backend.recv_multipart().fuse() => {
221 if let Some(msg) = msg_result? {
222 debug!("Proxy: {} → {}: {} frames",
223 backend.socket_desc(),
224 frontend.socket_desc(),
225 msg.len());
226
227 // Send copy to capture if present
228 if let Some(ref mut cap) = capture
229 && let Err(e) = cap.send_multipart(msg.clone()).await
230 {
231 debug!("Capture socket send failed: {}", e);
232 }
233
234 // Forward to frontend (transient errors keep the proxy up).
235 if let Err(e) = frontend.send_multipart(msg).await {
236 if is_transient_send_error(&e) {
237 debug!("Proxy: transient send to {}, dropping frame: {}",
238 frontend.socket_desc(), e);
239 } else {
240 return Err(e);
241 }
242 }
243 }
244 }
245 }
246 }
247}
248
249/// Control commands for steerable proxy.
250///
251/// Sent as single-frame messages to the control socket.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum ProxyCommand {
254 /// Pause message forwarding (buffering continues)
255 Pause,
256 /// Resume message forwarding
257 Resume,
258 /// Terminate the proxy loop
259 Terminate,
260 /// Report statistics - replies with `"messages_forwarded=N"` on the control socket.
261 Statistics,
262}
263
264impl ProxyCommand {
265 /// Parse command from bytes.
266 pub const fn from_bytes(data: &[u8]) -> Option<Self> {
267 match data {
268 b"PAUSE" => Some(Self::Pause),
269 b"RESUME" => Some(Self::Resume),
270 b"TERMINATE" => Some(Self::Terminate),
271 b"STATISTICS" => Some(Self::Statistics),
272 _ => None,
273 }
274 }
275
276 /// Convert command to bytes.
277 pub const fn as_bytes(&self) -> &'static [u8] {
278 match self {
279 Self::Pause => b"PAUSE",
280 Self::Resume => b"RESUME",
281 Self::Terminate => b"TERMINATE",
282 Self::Statistics => b"STATISTICS",
283 }
284 }
285}
286
287/// Run a steerable bidirectional message proxy with control socket.
288///
289/// Like [`proxy()`] but can be controlled via a control socket that receives commands:
290/// - `PAUSE` - Stop forwarding messages (buffering continues)
291/// - `RESUME` - Resume forwarding messages
292/// - `TERMINATE` - Stop the proxy and return
293/// - `STATISTICS` - Future: report proxy statistics
294///
295/// # Parameters
296///
297/// - `frontend`: Socket facing clients/publishers
298/// - `backend`: Socket facing workers/subscribers
299/// - `capture`: Optional socket to receive message copies
300/// - `control`: Socket that receives control commands
301///
302/// # Control Socket Protocol
303///
304/// Send single-frame messages with command text:
305/// ```text
306/// PAUSE - Pause forwarding
307/// RESUME - Resume forwarding
308/// TERMINATE - Stop proxy
309/// STATISTICS - Get stats (future)
310/// ```
311///
312/// # Example
313///
314/// ```rust,ignore
315/// use monocoque_zmtp::proxy::{proxy_steerable, ProxySocket, ProxyCommand};
316/// use monocoque_zmtp::router::RouterSocket;
317/// use monocoque_zmtp::dealer::DealerSocket;
318/// use monocoque_zmtp::pair::PairSocket;
319///
320/// #[compio::main]
321/// async fn main() -> std::io::Result<()> {
322/// // Broker sockets
323/// let (_, mut frontend) = RouterSocket::bind("127.0.0.1:5555").await?;
324/// let (_, mut backend) = DealerSocket::bind("127.0.0.1:5556").await?;
325///
326/// // Control socket
327/// let (_, mut control) = PairSocket::bind("127.0.0.1:5557").await?;
328///
329/// // Run steerable proxy
330/// proxy_steerable(&mut frontend, &mut backend, None, &mut control).await?;
331/// Ok(())
332/// }
333/// ```
334///
335/// Send control commands from another socket:
336/// ```no_run
337/// use monocoque_zmtp::pair::PairSocket;
338/// use bytes::Bytes;
339///
340/// # async fn send_control() -> std::io::Result<()> {
341/// let mut control_client = PairSocket::connect("127.0.0.1:5557").await?;
342///
343/// // Pause proxy
344/// control_client.send(vec![Bytes::from("PAUSE")]).await?;
345///
346/// // Resume proxy
347/// control_client.send(vec![Bytes::from("RESUME")]).await?;
348///
349/// // Terminate proxy
350/// control_client.send(vec![Bytes::from("TERMINATE")]).await?;
351/// # Ok(())
352/// # }
353/// ```
354pub async fn proxy_steerable<F, B, C, Ctrl>(
355 frontend: &mut F,
356 backend: &mut B,
357 mut capture: Option<&mut C>,
358 control: &mut Ctrl,
359) -> io::Result<()>
360where
361 F: ProxySocket,
362 B: ProxySocket,
363 C: ProxySocket,
364 Ctrl: ProxySocket,
365{
366 use futures::{FutureExt, select};
367
368 debug!(
369 "Starting steerable proxy: {} ←→ {} (control enabled)",
370 frontend.socket_desc(),
371 backend.socket_desc()
372 );
373
374 let mut paused = false;
375 let mut message_count = 0u64;
376
377 loop {
378 select! {
379 // Check for control commands
380 cmd_result = control.recv_multipart().fuse() => {
381 if let Some(cmd_msg) = cmd_result?
382 && let Some(cmd_frame) = cmd_msg.first()
383 && let Some(cmd) = ProxyCommand::from_bytes(cmd_frame)
384 {
385 debug!("Proxy control command: {:?}", cmd);
386
387 match cmd {
388 ProxyCommand::Pause => {
389 debug!("Proxy PAUSED");
390 paused = true;
391 }
392 ProxyCommand::Resume => {
393 debug!("Proxy RESUMED");
394 paused = false;
395 }
396 ProxyCommand::Terminate => {
397 debug!("Proxy TERMINATING (forwarded {} messages)", message_count);
398 return Ok(());
399 }
400 ProxyCommand::Statistics => {
401 debug!("Proxy statistics: {} messages forwarded", message_count);
402 let stats = format!("messages_forwarded={}", message_count);
403 let _ = control.send_multipart(vec![bytes::Bytes::from(stats)]).await;
404 }
405 }
406 }
407 }
408
409 // Forward frontend → backend (if not paused)
410 msg_result = frontend.recv_multipart().fuse() => {
411 if let Some(msg) = msg_result? {
412 if paused {
413 debug!("Proxy: dropped message (paused)");
414 } else {
415 debug!("Proxy: {} → {}: {} frames",
416 frontend.socket_desc(),
417 backend.socket_desc(),
418 msg.len());
419
420 // Send copy to capture if present
421 if let Some(ref mut cap) = capture
422 && let Err(e) = cap.send_multipart(msg.clone()).await
423 {
424 debug!("Capture socket send failed: {}", e);
425 }
426
427 // Forward to backend (transient errors keep the proxy up).
428 match backend.send_multipart(msg).await {
429 Ok(()) => message_count += 1,
430 Err(e) if is_transient_send_error(&e) => {
431 debug!("Proxy: transient send to {}, dropping frame: {}",
432 backend.socket_desc(), e);
433 }
434 Err(e) => return Err(e),
435 }
436 }
437 }
438 }
439
440 // Forward backend → frontend (if not paused)
441 msg_result = backend.recv_multipart().fuse() => {
442 if let Some(msg) = msg_result? {
443 if paused {
444 debug!("Proxy: dropped message (paused)");
445 } else {
446 debug!("Proxy: {} → {}: {} frames",
447 backend.socket_desc(),
448 frontend.socket_desc(),
449 msg.len());
450
451 // Send copy to capture if present
452 if let Some(ref mut cap) = capture
453 && let Err(e) = cap.send_multipart(msg.clone()).await
454 {
455 debug!("Capture socket send failed: {}", e);
456 }
457
458 // Forward to frontend (transient errors keep the proxy up).
459 match frontend.send_multipart(msg).await {
460 Ok(()) => message_count += 1,
461 Err(e) if is_transient_send_error(&e) => {
462 debug!("Proxy: transient send to {}, dropping frame: {}",
463 frontend.socket_desc(), e);
464 }
465 Err(e) => return Err(e),
466 }
467 }
468 }
469 }
470 }
471 }
472}
473
474// ===== ProxySocket Implementations =====
475
476// XSUB socket (frontend in PUB-SUB broker)
477impl ProxySocket for XSubSocket {
478 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
479 self.recv().await
480 }
481
482 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
483 // In a PUB-SUB broker the proxy receives subscription events from XPUB
484 // (backend) and must forward them upstream via XSUB so the publisher stops
485 // or starts sending the relevant topics.
486 //
487 // The message format produced by XPubSocket::recv_multipart is:
488 // [b"\x01", topic] - subscribe
489 // [b"\x00", topic] - unsubscribe
490 //
491 // We reconstruct the raw ZMTP subscription frame and dispatch it.
492 if msg.is_empty() {
493 return Ok(());
494 }
495
496 let cmd_frame = &msg[0];
497 if cmd_frame.is_empty() {
498 return Ok(());
499 }
500
501 let cmd_byte = cmd_frame[0];
502 // Topic is either in a second frame or appended after the command byte
503 // in the same frame, depending on how the message was encoded.
504 let topic: Bytes = if msg.len() >= 2 {
505 msg[1].clone()
506 } else if cmd_frame.len() > 1 {
507 cmd_frame.slice(1..)
508 } else {
509 Bytes::new()
510 };
511
512 let event = if cmd_byte == 0x01 {
513 monocoque_core::subscription::SubscriptionEvent::Subscribe(topic)
514 } else if cmd_byte == 0x00 {
515 monocoque_core::subscription::SubscriptionEvent::Unsubscribe(topic)
516 } else {
517 // Unknown command - ignore
518 return Ok(());
519 };
520
521 self.send_subscription_event(event).await
522 }
523
524 fn socket_desc(&self) -> &'static str {
525 "XSUB"
526 }
527}
528
529// XPUB socket (backend in PUB-SUB broker)
530impl ProxySocket for XPubSocket {
531 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
532 // XPUB receives subscription events, not data
533 // Map subscription events to message format
534 if let Some(event) = self.recv_subscription().await? {
535 let msg = match event {
536 monocoque_core::subscription::SubscriptionEvent::Subscribe(topic) => {
537 vec![Bytes::from(&b"\x01"[..]), topic]
538 }
539 monocoque_core::subscription::SubscriptionEvent::Unsubscribe(topic) => {
540 vec![Bytes::from(&b"\x00"[..]), topic]
541 }
542 };
543 Ok(Some(msg))
544 } else {
545 Ok(None)
546 }
547 }
548
549 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
550 self.send(msg).await
551 }
552
553 fn socket_desc(&self) -> &'static str {
554 "XPUB"
555 }
556}
557
558// DEALER socket (backend in REQ-REP load balancer)
559impl ProxySocket for DealerSocket {
560 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
561 self.recv().await
562 }
563
564 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
565 self.send(msg).await
566 }
567
568 fn socket_desc(&self) -> &'static str {
569 "DEALER"
570 }
571}
572
573// ROUTER socket (frontend in REQ-REP load balancer)
574impl ProxySocket for RouterSocket {
575 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
576 self.recv().await
577 }
578
579 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
580 self.send(msg).await
581 }
582
583 fn socket_desc(&self) -> &'static str {
584 "ROUTER"
585 }
586}
587
588// PULL socket (frontend in PUSH-PULL forwarder)
589impl ProxySocket for PullSocket {
590 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
591 self.recv().await
592 }
593
594 async fn send_multipart(&mut self, _msg: Vec<Bytes>) -> io::Result<()> {
595 // PULL doesn't send
596 Ok(())
597 }
598
599 fn socket_desc(&self) -> &'static str {
600 "PULL"
601 }
602}
603
604// PUSH socket (backend in PUSH-PULL forwarder)
605impl ProxySocket for PushSocket {
606 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
607 // PUSH doesn't receive
608 Ok(None)
609 }
610
611 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
612 self.send(msg).await
613 }
614
615 fn socket_desc(&self) -> &'static str {
616 "PUSH"
617 }
618}
619
620// REQ socket
621impl ProxySocket for ReqSocket {
622 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
623 self.recv().await
624 }
625
626 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
627 self.send(msg).await
628 }
629
630 fn socket_desc(&self) -> &'static str {
631 "REQ"
632 }
633}
634
635// REP socket
636impl ProxySocket for RepSocket {
637 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
638 self.recv().await
639 }
640
641 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
642 self.send(msg).await
643 }
644
645 fn socket_desc(&self) -> &'static str {
646 "REP"
647 }
648}
649
650// PAIR socket
651impl ProxySocket for PairSocket {
652 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
653 self.recv().await
654 }
655
656 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
657 self.send(msg).await
658 }
659
660 fn socket_desc(&self) -> &'static str {
661 "PAIR"
662 }
663}
664
665// PUB socket (typically not used in proxy, but included for completeness)
666impl ProxySocket for PubSocket {
667 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
668 // PUB doesn't receive
669 Ok(None)
670 }
671
672 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
673 self.send(msg).await
674 }
675
676 fn socket_desc(&self) -> &'static str {
677 "PUB"
678 }
679}
680
681// SUB socket (typically not used directly in proxy, XSUB is preferred)
682impl ProxySocket for SubSocket {
683 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
684 self.recv().await
685 }
686
687 async fn send_multipart(&mut self, _msg: Vec<Bytes>) -> io::Result<()> {
688 // SUB doesn't send data
689 Ok(())
690 }
691
692 fn socket_desc(&self) -> &'static str {
693 "SUB"
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn transient_send_errors_are_classified() {
703 // Transient: dropped frame, proxy stays up.
704 for kind in [
705 io::ErrorKind::WouldBlock,
706 io::ErrorKind::Interrupted,
707 io::ErrorKind::TimedOut,
708 ] {
709 assert!(is_transient_send_error(&io::Error::new(kind, "x")));
710 }
711 // Fatal: proxy tears down.
712 for kind in [
713 io::ErrorKind::BrokenPipe,
714 io::ErrorKind::ConnectionReset,
715 io::ErrorKind::NotConnected,
716 ] {
717 assert!(!is_transient_send_error(&io::Error::new(kind, "x")));
718 }
719 }
720
721 /// Mock socket for testing proxy logic
722 struct MockSocket {
723 name: &'static str,
724 recv_queue: Vec<Vec<Bytes>>,
725 send_queue: Vec<Vec<Bytes>>,
726 }
727
728 impl MockSocket {
729 fn new(name: &'static str) -> Self {
730 Self {
731 name,
732 recv_queue: Vec::new(),
733 send_queue: Vec::new(),
734 }
735 }
736
737 fn enqueue(&mut self, msg: Vec<Bytes>) {
738 self.recv_queue.push(msg);
739 }
740 }
741
742 impl ProxySocket for MockSocket {
743 async fn recv_multipart(&mut self) -> io::Result<Option<Vec<Bytes>>> {
744 Ok(self.recv_queue.pop())
745 }
746
747 async fn send_multipart(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
748 self.send_queue.push(msg);
749 Ok(())
750 }
751
752 fn socket_desc(&self) -> &'static str {
753 self.name
754 }
755 }
756
757 #[test]
758 fn test_mock_socket() {
759 let mut sock = MockSocket::new("test");
760 sock.enqueue(vec![Bytes::from("hello")]);
761 assert_eq!(sock.recv_queue.len(), 1);
762 }
763
764 // TODO: Add integration tests with real sockets
765 // - Test XSUB-XPUB broker pattern
766 // - Test ROUTER-DEALER load balancer
767 // - Test capture socket monitoring
768 // - Test error handling when socket fails
769}