Skip to main content

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}