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