Skip to main content

monocoque_zmtp/
pair.rs

1//! PAIR socket implementation
2//!
3//! PAIR sockets are exclusive peer-to-peer sockets that connect exactly two endpoints.
4//! They provide bidirectional communication without routing or filtering.
5//!
6//! # Characteristics
7//!
8//! - **Exclusive**: Only connects to one peer at a time
9//! - **Bidirectional**: Can both send and receive messages
10//! - **No routing**: Messages go directly between the pair
11//! - **No filtering**: All messages are delivered
12//!
13//! # Use Cases
14//!
15//! - Connecting two threads in a process
16//! - Exclusive communication between two services
17//! - Testing and prototyping
18
19use crate::base::SocketBase;
20use crate::inproc_stream::InprocStream;
21use crate::{handshake::perform_handshake_with_options, session::SocketType};
22use bytes::Bytes;
23use compio_io::{AsyncRead, AsyncWrite};
24use monocoque_core::endpoint::Endpoint;
25use monocoque_core::options::SocketOptions;
26use monocoque_core::rt::TcpStream;
27use smallvec::SmallVec;
28use std::io;
29use tracing::{debug, trace};
30
31/// PAIR socket for exclusive peer-to-peer communication.
32///
33/// PAIR sockets connect exactly two endpoints and provide bidirectional
34/// message passing without any routing or filtering logic.
35pub struct PairSocket<S = TcpStream>
36where
37    S: AsyncRead + AsyncWrite + Unpin,
38{
39    /// Base socket infrastructure (stream, buffers, options)
40    base: SocketBase<S>,
41    /// Accumulated frames for current multipart message
42    frames: SmallVec<[Bytes; 4]>,
43}
44
45impl<S> PairSocket<S>
46where
47    S: AsyncRead + AsyncWrite + Unpin,
48{
49    /// Create a new PAIR socket from a stream with default buffer configuration.
50    pub async fn new(stream: S) -> io::Result<Self> {
51        Self::with_options(stream, SocketOptions::default()).await
52    }
53
54    /// Create a new PAIR socket with custom buffer configuration and socket options.
55    pub async fn with_options(mut stream: S, options: SocketOptions) -> io::Result<Self> {
56        debug!("[PAIR] Creating new PAIR socket");
57
58        // Perform ZMTP handshake
59        debug!("[PAIR] Performing ZMTP handshake...");
60        let handshake_result = perform_handshake_with_options(
61            &mut stream,
62            SocketType::Pair,
63            options.routing_id.as_deref(),
64            Some(options.handshake_timeout),
65            &options,
66        )
67        .await
68        .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
69
70        debug!(
71            peer_identity = ?handshake_result.peer_identity,
72            peer_socket_type = ?handshake_result.peer_socket_type,
73            "[PAIR] Handshake complete"
74        );
75
76        debug!("[PAIR] Socket initialized");
77
78        let mut base = SocketBase::new(stream, SocketType::Pair, options);
79        base.curve_cipher = handshake_result.curve_cipher;
80        Ok(Self {
81            base,
82            frames: SmallVec::new(),
83        })
84    }
85
86    /// Send a message to the paired socket.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if the socket is poisoned, disconnected, or if the write fails.
91    pub async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
92        trace!("[PAIR] Sending {} frames", msg.len());
93
94        // Encode message into write_buf (with CURVE encryption if active)
95        self.base.encode_message_to_write_buf(&msg)?;
96
97        // Delegate to base for writing
98        self.base.write_from_buf().await?;
99
100        trace!("[PAIR] Message sent successfully");
101        Ok(())
102    }
103
104    /// Receive a message from the paired socket.
105    ///
106    /// Returns `Ok(Some(msg))` if a message was received, `Ok(None)` if the
107    /// connection was closed, or an error.
108    pub async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
109        trace!("[PAIR] Waiting for message");
110
111        // Read from stream until we have a complete message
112        loop {
113            // Try to decode frames from buffer
114            loop {
115                match self.base.process_frame()? {
116                    crate::base::FrameResult::NeedMore => break,
117                    crate::base::FrameResult::CommandHandled => {
118                        if !self.base.send_buffer.is_empty() {
119                            self.base.flush_send_buffer().await?;
120                        }
121                    }
122                    crate::base::FrameResult::Data(more, payload) => {
123                        self.frames.push(payload);
124                        if !more {
125                            let msg: Vec<Bytes> = self.frames.drain(..).collect();
126                            trace!("[PAIR] Received {} frames", msg.len());
127                            return Ok(Some(msg));
128                        }
129                    }
130                }
131            }
132
133            // Need more data - read raw bytes from stream
134            let n = self.base.read_raw().await?;
135            if n == 0 {
136                // EOF - connection closed
137                trace!("[PAIR] Connection closed");
138                return Ok(None);
139            }
140            if self.base.check_heartbeat()? {
141                self.base.flush_send_buffer().await?;
142            }
143            // Continue decoding with new data
144        }
145    }
146
147    /// Close the socket gracefully by shutting down the underlying stream.
148    pub async fn close(mut self) -> io::Result<()> {
149        trace!("[PAIR] Closing socket");
150        self.base.close().await
151    }
152
153    /// Get a reference to the socket options.
154    #[inline]
155    pub const fn options(&self) -> &SocketOptions {
156        &self.base.options
157    }
158
159    /// Get a mutable reference to the socket options.
160    #[inline]
161    pub fn options_mut(&mut self) -> &mut SocketOptions {
162        &mut self.base.options
163    }
164
165    /// Set socket options (builder-style).
166    #[inline]
167    pub fn set_options(&mut self, options: SocketOptions) {
168        self.base.set_options(options);
169    }
170
171    /// Get the socket type.
172    ///
173    /// # ZeroMQ Compatibility
174    ///
175    /// Corresponds to `ZMQ_TYPE` (16) option.
176    #[inline]
177    pub const fn socket_type(&self) -> SocketType {
178        SocketType::Pair
179    }
180
181    /// Get the endpoint this socket is connected/bound to, if available.
182    ///
183    /// Returns `None` if the socket was created from a raw stream.
184    ///
185    /// # ZeroMQ Compatibility
186    ///
187    /// Corresponds to `ZMQ_LAST_ENDPOINT` (32) option.
188    #[inline]
189    pub fn last_endpoint(&self) -> Option<&Endpoint> {
190        self.base.last_endpoint()
191    }
192
193    /// Check if the last received message has more frames coming.
194    ///
195    /// Returns `true` if there are more frames in the current multipart message.
196    ///
197    /// # ZeroMQ Compatibility
198    ///
199    /// Corresponds to `ZMQ_RCVMORE` (13) option.
200    #[inline]
201    pub fn has_more(&self) -> bool {
202        self.base.has_more()
203    }
204
205    /// Get the event state of the socket.
206    ///
207    /// Returns a bitmask indicating ready-to-receive and ready-to-send states.
208    ///
209    /// # Returns
210    ///
211    /// - `1` (POLLIN) - Socket is ready to receive
212    /// - `2` (POLLOUT) - Socket is ready to send
213    /// - `3` (POLLIN | POLLOUT) - Socket is ready for both
214    ///
215    /// # ZeroMQ Compatibility
216    ///
217    /// Corresponds to `ZMQ_EVENTS` (15) option.
218    #[inline]
219    pub fn events(&self) -> u32 {
220        self.base.events()
221    }
222}
223
224// Specialized implementation for TCP streams to enable TCP_NODELAY
225impl PairSocket<TcpStream> {
226    /// Bind to an address and accept the first connection.
227    ///
228    /// PAIR sockets form an exclusive pair with exactly one peer.
229    ///
230    /// # Returns
231    ///
232    /// A tuple of `(listener, socket)` where:
233    /// - `listener` can be used to accept additional connections if needed
234    /// - `socket` is ready to send/receive with the first peer
235    ///
236    /// # Example
237    ///
238    /// ```no_run
239    /// use monocoque_zmtp::pair::PairSocket;
240    ///
241    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
242    /// let (listener, mut socket) = PairSocket::bind("127.0.0.1:5555").await?;
243    /// # Ok(())
244    /// # }
245    /// ```
246    pub async fn bind(
247        addr: impl monocoque_core::rt::ToSocketAddrs,
248    ) -> io::Result<(monocoque_core::rt::TcpListener, Self)> {
249        let listener = monocoque_core::rt::TcpListener::bind(addr).await?;
250        let (stream, _) = listener.accept().await?;
251        let socket = Self::from_tcp(stream).await?;
252        Ok((listener, socket))
253    }
254
255    /// Connect to a remote PAIR socket, storing the endpoint for automatic reconnection.
256    ///
257    /// # Example
258    ///
259    /// ```no_run
260    /// use monocoque_zmtp::pair::PairSocket;
261    ///
262    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
263    /// let mut socket = PairSocket::connect("127.0.0.1:5555").await?;
264    /// # Ok(())
265    /// # }
266    /// ```
267    pub async fn connect(addr: impl monocoque_core::rt::ToSocketAddrs) -> io::Result<Self> {
268        Self::connect_with_options(addr, SocketOptions::default()).await
269    }
270
271    /// Connect with custom options, storing the endpoint for reconnection.
272    pub async fn connect_with_options(
273        addr: impl monocoque_core::rt::ToSocketAddrs,
274        options: SocketOptions,
275    ) -> io::Result<Self> {
276        let stream = TcpStream::connect(addr).await?;
277        let peer_addr = stream.peer_addr()?;
278        crate::utils::configure_tcp_stream(&stream, &options, "PAIR")?;
279
280        let mut stream = stream;
281        let handshake_result = perform_handshake_with_options(
282            &mut stream,
283            SocketType::Pair,
284            options.routing_id.as_deref(),
285            Some(options.handshake_timeout),
286            &options,
287        )
288        .await
289        .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
290
291        debug!(
292            peer_identity = ?handshake_result.peer_identity,
293            peer_socket_type = ?handshake_result.peer_socket_type,
294            "[PAIR] Connected to {} (endpoint stored for reconnection)",
295            peer_addr
296        );
297
298        let endpoint = monocoque_core::endpoint::Endpoint::Tcp(peer_addr);
299        let mut base =
300            crate::base::SocketBase::with_endpoint(stream, SocketType::Pair, endpoint, options);
301        base.curve_cipher = handshake_result.curve_cipher;
302        Ok(Self {
303            base,
304            frames: SmallVec::new(),
305        })
306    }
307
308    /// Create a new PAIR socket from a TCP stream with TCP_NODELAY enabled.
309    pub async fn from_tcp(stream: TcpStream) -> io::Result<Self> {
310        Self::from_tcp_with_options(stream, SocketOptions::default()).await
311    }
312
313    /// Create a new PAIR socket from a TCP stream with TCP_NODELAY and custom options.
314    pub async fn from_tcp_with_options(
315        stream: TcpStream,
316        options: SocketOptions,
317    ) -> io::Result<Self> {
318        // Configure TCP optimizations including keepalive
319        crate::utils::configure_tcp_stream(&stream, &options, "PAIR")?;
320        Self::with_options(stream, options).await
321    }
322
323    /// Check if the socket is currently connected.
324    #[inline]
325    pub fn is_connected(&self) -> bool {
326        self.base.is_connected()
327    }
328
329    /// Try to reconnect to the stored endpoint.
330    pub async fn try_reconnect(&mut self) -> io::Result<()> {
331        self.base.try_reconnect(SocketType::Pair).await
332    }
333
334    /// Receive a message with automatic reconnection on EOF or network error.
335    ///
336    /// If the socket was created with `connect()` and stores an endpoint, this
337    /// method loops: on EOF or broken-pipe it clears the stream and calls
338    /// `try_reconnect()` (which applies exponential backoff), then retries `recv()`.
339    ///
340    /// Respects `max_reconnect_attempts`  -  returns `NotConnected` when exhausted.
341    pub async fn recv_with_reconnect(&mut self) -> io::Result<Option<Vec<Bytes>>> {
342        let max = self.base.options.max_reconnect_attempts;
343        let mut attempts = 0u32;
344
345        loop {
346            if self.base.stream.is_none() {
347                if let Some(limit) = max
348                    && attempts >= limit
349                {
350                    return Err(io::Error::new(
351                        io::ErrorKind::NotConnected,
352                        format!("Max {} reconnection attempts exceeded", limit),
353                    ));
354                }
355                attempts += 1;
356                trace!(
357                    "[PAIR] Stream disconnected, reconnecting (attempt {})",
358                    attempts
359                );
360                self.try_reconnect().await?;
361            }
362
363            match self.recv().await {
364                Ok(Some(msg)) => return Ok(Some(msg)),
365                // EOF: read_raw() already set stream = None
366                Ok(None) => {
367                    debug!("[PAIR] EOF on recv, will reconnect");
368                }
369                Err(e) => {
370                    if self.base.stream.is_none()
371                        || matches!(
372                            e.kind(),
373                            io::ErrorKind::ConnectionReset
374                                | io::ErrorKind::ConnectionAborted
375                                | io::ErrorKind::BrokenPipe
376                                | io::ErrorKind::UnexpectedEof
377                        )
378                    {
379                        debug!("[PAIR] Connection error on recv ({}), will reconnect", e);
380                        self.base.stream = None;
381                    } else {
382                        return Err(e);
383                    }
384                }
385            }
386        }
387    }
388
389    /// Send a message with automatic reconnection on network error.
390    ///
391    /// On BrokenPipe / ConnectionReset, `write_from_buf()` already sets
392    /// `stream = None`, so the next loop iteration reconnects automatically.
393    ///
394    /// Respects `max_reconnect_attempts`  -  returns `NotConnected` when exhausted.
395    pub async fn send_with_reconnect(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
396        let max = self.base.options.max_reconnect_attempts;
397        let mut attempts = 0u32;
398
399        loop {
400            if self.base.stream.is_none() {
401                if let Some(limit) = max
402                    && attempts >= limit
403                {
404                    return Err(io::Error::new(
405                        io::ErrorKind::NotConnected,
406                        format!("Max {} reconnection attempts exceeded", limit),
407                    ));
408                }
409                attempts += 1;
410                trace!(
411                    "[PAIR] Stream disconnected, reconnecting (attempt {})",
412                    attempts
413                );
414                self.try_reconnect().await?;
415            }
416
417            match self.send(msg.clone()).await {
418                Ok(()) => return Ok(()),
419                Err(_) if self.base.stream.is_none() => {
420                    // write_from_buf set stream = None → network error, retry
421                    debug!("[PAIR] Send failed (stream lost), will reconnect");
422                }
423                Err(e) => return Err(e),
424            }
425        }
426    }
427}
428
429// Specialized implementation for Inproc streams
430impl PairSocket<InprocStream> {
431    /// Bind to an inproc endpoint.
432    ///
433    /// Creates a new inproc endpoint that other sockets can connect to.
434    /// Inproc endpoints must be bound before they can be connected to.
435    ///
436    /// # Arguments
437    ///
438    /// * `endpoint` - Inproc URI (e.g., "inproc://my-endpoint")
439    ///
440    /// # Example
441    ///
442    /// ```no_run
443    /// use monocoque_zmtp::pair::PairSocket;
444    ///
445    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
446    /// let socket = PairSocket::bind_inproc("inproc://my-pair")?;
447    /// # Ok(())
448    /// # }
449    /// ```
450    pub fn bind_inproc(endpoint: &str) -> io::Result<Self> {
451        Self::bind_inproc_with_options(endpoint, SocketOptions::default())
452    }
453
454    /// Bind to an inproc endpoint with custom configuration and options.
455    pub fn bind_inproc_with_options(endpoint: &str, options: SocketOptions) -> io::Result<Self> {
456        debug!("[PAIR] Binding to inproc endpoint: {}", endpoint);
457
458        // Bind to inproc endpoint
459        let (tx, rx) = monocoque_core::inproc::bind_inproc(endpoint)?;
460        let stream = InprocStream::new(tx, rx);
461
462        // Parse endpoint for storage
463        let parsed_endpoint = Endpoint::parse(endpoint)
464            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
465
466        debug!("[PAIR] Bound to {}", endpoint);
467
468        // For inproc, no handshake needed (same process)
469        Ok(Self {
470            base: SocketBase::with_endpoint(stream, SocketType::Pair, parsed_endpoint, options),
471            frames: SmallVec::new(),
472        })
473    }
474
475    /// Connect to an inproc endpoint.
476    ///
477    /// Connects to a previously bound inproc endpoint.
478    ///
479    /// # Arguments
480    ///
481    /// * `endpoint` - Inproc URI (e.g., "inproc://my-endpoint")
482    ///
483    /// # Example
484    ///
485    /// ```no_run
486    /// use monocoque_zmtp::pair::PairSocket;
487    ///
488    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
489    /// let socket = PairSocket::connect_inproc("inproc://my-pair")?;
490    /// # Ok(())
491    /// # }
492    /// ```
493    pub fn connect_inproc(endpoint: &str) -> io::Result<Self> {
494        Self::connect_inproc_with_options(endpoint, SocketOptions::default())
495    }
496
497    /// Connect to an inproc endpoint with custom configuration and options.
498    pub fn connect_inproc_with_options(endpoint: &str, options: SocketOptions) -> io::Result<Self> {
499        debug!("[PAIR] Connecting to inproc endpoint: {}", endpoint);
500
501        // connect_inproc_bidi returns (to_server_tx, from_server_rx) so we can
502        // both send to the server and receive replies from it. The server must
503        // have been bound with bind_inproc_bidi.
504        let (tx, rx) = monocoque_core::inproc::connect_inproc_bidi(endpoint)?;
505        let stream = InprocStream::new(tx, rx);
506
507        // Parse endpoint for storage
508        let parsed_endpoint = Endpoint::parse(endpoint)
509            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
510
511        debug!("[PAIR] Connected to {}", endpoint);
512
513        // For inproc, no handshake needed (same process)
514        Ok(Self {
515            base: SocketBase::with_endpoint(stream, SocketType::Pair, parsed_endpoint, options),
516            frames: SmallVec::new(),
517        })
518    }
519}
520
521crate::impl_socket_trait!(PairSocket<S>, SocketType::Pair);