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 // Coalesce / vector / copy-and-write as appropriate.
95 self.base.send_message(&msg).await?;
96
97 trace!("[PAIR] Message sent successfully");
98 Ok(())
99 }
100
101 /// Receive a message from the paired socket.
102 ///
103 /// Returns `Ok(Some(msg))` if a message was received, `Ok(None)` if the
104 /// connection was closed, or an error.
105 pub async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
106 trace!("[PAIR] Waiting for message");
107
108 // Read from stream until we have a complete message
109 loop {
110 // Try to decode frames from buffer
111 loop {
112 match self.base.process_frame()? {
113 crate::base::FrameResult::NeedMore => break,
114 crate::base::FrameResult::CommandHandled => {
115 if !self.base.send_buffer.is_empty() {
116 self.base.flush_send_buffer().await?;
117 }
118 }
119 crate::base::FrameResult::Data(more, payload) => {
120 self.frames.push(payload);
121 if !more {
122 let msg: Vec<Bytes> = self.frames.drain(..).collect();
123 trace!("[PAIR] Received {} frames", msg.len());
124 return Ok(Some(msg));
125 }
126 }
127 }
128 }
129
130 // Need more data - read raw bytes from stream
131 let n = self.base.read_raw().await?;
132 if n == 0 {
133 // EOF - connection closed
134 trace!("[PAIR] Connection closed");
135 return Ok(None);
136 }
137 if self.base.check_heartbeat()? {
138 self.base.flush_send_buffer().await?;
139 }
140 // Continue decoding with new data
141 }
142 }
143
144 /// Receive a message into a caller-provided buffer, reusing its allocation.
145 ///
146 /// Allocation-free counterpart to [`recv`](Self::recv): frames go into `out`
147 /// (cleared on entry) instead of a fresh `Vec`. Returns `Ok(true)` on a
148 /// complete message, `Ok(false)` on EOF.
149 pub async fn recv_into(&mut self, out: &mut Vec<Bytes>) -> io::Result<bool> {
150 out.clear();
151 loop {
152 loop {
153 match self.base.process_frame()? {
154 crate::base::FrameResult::NeedMore => break,
155 crate::base::FrameResult::CommandHandled => {
156 if !self.base.send_buffer.is_empty() {
157 self.base.flush_send_buffer().await?;
158 }
159 }
160 crate::base::FrameResult::Data(more, payload) => {
161 if !more && self.frames.is_empty() {
162 out.push(payload);
163 return Ok(true);
164 }
165 self.frames.push(payload);
166 if !more {
167 out.extend(self.frames.drain(..));
168 return Ok(true);
169 }
170 }
171 }
172 }
173
174 let n = self.base.read_raw().await?;
175 if n == 0 {
176 return Ok(false);
177 }
178 if self.base.check_heartbeat()? {
179 self.base.flush_send_buffer().await?;
180 }
181 }
182 }
183
184 /// Try to receive a message into `out` without a kernel read.
185 ///
186 /// Returns `Ok(true)` with a complete message moved into `out`, or
187 /// `Ok(false)` leaving `out` untouched when nothing complete is buffered.
188 pub fn try_recv_into(&mut self, out: &mut Vec<Bytes>) -> io::Result<bool> {
189 loop {
190 match self.base.process_frame()? {
191 crate::base::FrameResult::NeedMore => return Ok(false),
192 crate::base::FrameResult::CommandHandled => {}
193 crate::base::FrameResult::Data(more, payload) => {
194 if !more && self.frames.is_empty() {
195 out.clear();
196 out.push(payload);
197 return Ok(true);
198 }
199 self.frames.push(payload);
200 if !more {
201 out.clear();
202 out.extend(self.frames.drain(..));
203 return Ok(true);
204 }
205 }
206 }
207 }
208 }
209
210 /// Receive a single-frame message, returning just its frame.
211 ///
212 /// Returns `Ok(None)` on EOF, and an error if the message is multipart.
213 pub async fn recv_one(&mut self) -> io::Result<Option<Bytes>> {
214 loop {
215 loop {
216 match self.base.process_frame()? {
217 crate::base::FrameResult::NeedMore => break,
218 crate::base::FrameResult::CommandHandled => {
219 if !self.base.send_buffer.is_empty() {
220 self.base.flush_send_buffer().await?;
221 }
222 }
223 crate::base::FrameResult::Data(more, payload) => {
224 if more || !self.frames.is_empty() {
225 return Err(io::Error::new(
226 io::ErrorKind::InvalidData,
227 "recv_one received a multipart message",
228 ));
229 }
230 return Ok(Some(payload));
231 }
232 }
233 }
234
235 let n = self.base.read_raw().await?;
236 if n == 0 {
237 return Ok(None);
238 }
239 if self.base.check_heartbeat()? {
240 self.base.flush_send_buffer().await?;
241 }
242 }
243 }
244
245 /// Close the socket gracefully by shutting down the underlying stream.
246 pub async fn close(mut self) -> io::Result<()> {
247 trace!("[PAIR] Closing socket");
248 self.base.close().await
249 }
250
251 /// Get a reference to the socket options.
252 #[inline]
253 pub const fn options(&self) -> &SocketOptions {
254 &self.base.options
255 }
256
257 /// Get a mutable reference to the socket options.
258 #[inline]
259 pub fn options_mut(&mut self) -> &mut SocketOptions {
260 &mut self.base.options
261 }
262
263 /// Set socket options (builder-style).
264 #[inline]
265 pub fn set_options(&mut self, options: SocketOptions) {
266 self.base.set_options(options);
267 }
268
269 /// Get the socket type.
270 ///
271 /// # ZeroMQ Compatibility
272 ///
273 /// Corresponds to `ZMQ_TYPE` (16) option.
274 #[inline]
275 pub const fn socket_type(&self) -> SocketType {
276 SocketType::Pair
277 }
278
279 /// Get the endpoint this socket is connected/bound to, if available.
280 ///
281 /// Returns `None` if the socket was created from a raw stream.
282 ///
283 /// # ZeroMQ Compatibility
284 ///
285 /// Corresponds to `ZMQ_LAST_ENDPOINT` (32) option.
286 #[inline]
287 pub fn last_endpoint(&self) -> Option<&Endpoint> {
288 self.base.last_endpoint()
289 }
290
291 /// Check if the last received message has more frames coming.
292 ///
293 /// Returns `true` if there are more frames in the current multipart message.
294 ///
295 /// # ZeroMQ Compatibility
296 ///
297 /// Corresponds to `ZMQ_RCVMORE` (13) option.
298 #[inline]
299 pub fn has_more(&self) -> bool {
300 self.base.has_more()
301 }
302
303 /// Get the event state of the socket.
304 ///
305 /// Returns a bitmask indicating ready-to-receive and ready-to-send states.
306 ///
307 /// # Returns
308 ///
309 /// - `1` (POLLIN) - Socket is ready to receive
310 /// - `2` (POLLOUT) - Socket is ready to send
311 /// - `3` (POLLIN | POLLOUT) - Socket is ready for both
312 ///
313 /// # ZeroMQ Compatibility
314 ///
315 /// Corresponds to `ZMQ_EVENTS` (15) option.
316 #[inline]
317 pub fn events(&self) -> u32 {
318 self.base.events()
319 }
320}
321
322// Specialized implementation for TCP streams to enable TCP_NODELAY
323impl PairSocket<TcpStream> {
324 /// Bind to an address and accept the first connection.
325 ///
326 /// PAIR sockets form an exclusive pair with exactly one peer.
327 ///
328 /// # Returns
329 ///
330 /// A tuple of `(listener, socket)` where:
331 /// - `listener` can be used to accept additional connections if needed
332 /// - `socket` is ready to send/receive with the first peer
333 ///
334 /// # Example
335 ///
336 /// ```no_run
337 /// use monocoque_zmtp::pair::PairSocket;
338 ///
339 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
340 /// let (listener, mut socket) = PairSocket::bind("127.0.0.1:5555").await?;
341 /// # Ok(())
342 /// # }
343 /// ```
344 pub async fn bind(
345 addr: impl monocoque_core::rt::ToSocketAddrs,
346 ) -> io::Result<(monocoque_core::rt::TcpListener, Self)> {
347 let listener = monocoque_core::rt::TcpListener::bind(addr).await?;
348 let (stream, _) = listener.accept().await?;
349 let socket = Self::from_tcp(stream).await?;
350 Ok((listener, socket))
351 }
352
353 /// Connect to a remote PAIR socket, storing the endpoint for automatic reconnection.
354 ///
355 /// # Example
356 ///
357 /// ```no_run
358 /// use monocoque_zmtp::pair::PairSocket;
359 ///
360 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
361 /// let mut socket = PairSocket::connect("127.0.0.1:5555").await?;
362 /// # Ok(())
363 /// # }
364 /// ```
365 pub async fn connect(addr: impl monocoque_core::rt::ToSocketAddrs) -> io::Result<Self> {
366 Self::connect_with_options(addr, SocketOptions::default()).await
367 }
368
369 /// Connect with custom options, storing the endpoint for reconnection.
370 pub async fn connect_with_options(
371 addr: impl monocoque_core::rt::ToSocketAddrs,
372 options: SocketOptions,
373 ) -> io::Result<Self> {
374 let stream = TcpStream::connect(addr).await?;
375 let peer_addr = stream.peer_addr()?;
376 crate::utils::configure_tcp_stream(&stream, &options, "PAIR")?;
377
378 let mut stream = stream;
379 let handshake_result = perform_handshake_with_options(
380 &mut stream,
381 SocketType::Pair,
382 options.routing_id.as_deref(),
383 Some(options.handshake_timeout),
384 &options,
385 )
386 .await
387 .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
388
389 debug!(
390 peer_identity = ?handshake_result.peer_identity,
391 peer_socket_type = ?handshake_result.peer_socket_type,
392 "[PAIR] Connected to {} (endpoint stored for reconnection)",
393 peer_addr
394 );
395
396 let endpoint = monocoque_core::endpoint::Endpoint::Tcp(peer_addr);
397 let mut base =
398 crate::base::SocketBase::with_endpoint(stream, SocketType::Pair, endpoint, options);
399 base.curve_cipher = handshake_result.curve_cipher;
400 Ok(Self {
401 base,
402 frames: SmallVec::new(),
403 })
404 }
405
406 /// Create a new PAIR socket from a TCP stream with TCP_NODELAY enabled.
407 pub async fn from_tcp(stream: TcpStream) -> io::Result<Self> {
408 Self::from_tcp_with_options(stream, SocketOptions::default()).await
409 }
410
411 /// Create a new PAIR socket from a TCP stream with TCP_NODELAY and custom options.
412 pub async fn from_tcp_with_options(
413 stream: TcpStream,
414 options: SocketOptions,
415 ) -> io::Result<Self> {
416 // Configure TCP optimizations including keepalive
417 crate::utils::configure_tcp_stream(&stream, &options, "PAIR")?;
418 Self::with_options(stream, options).await
419 }
420
421 /// Check if the socket is currently connected.
422 #[inline]
423 pub fn is_connected(&self) -> bool {
424 self.base.is_connected()
425 }
426
427 /// Try to reconnect to the stored endpoint.
428 pub async fn try_reconnect(&mut self) -> io::Result<()> {
429 self.base.try_reconnect(SocketType::Pair).await
430 }
431
432 /// Receive a message with automatic reconnection on EOF or network error.
433 ///
434 /// If the socket was created with `connect()` and stores an endpoint, this
435 /// method loops: on EOF or broken-pipe it clears the stream and calls
436 /// `try_reconnect()` (which applies exponential backoff), then retries `recv()`.
437 ///
438 /// Respects `max_reconnect_attempts` - returns `NotConnected` when exhausted.
439 pub async fn recv_with_reconnect(&mut self) -> io::Result<Option<Vec<Bytes>>> {
440 let max = self.base.options.max_reconnect_attempts;
441 let mut attempts = 0u32;
442
443 loop {
444 if self.base.stream.is_none() {
445 if let Some(limit) = max
446 && attempts >= limit
447 {
448 return Err(io::Error::new(
449 io::ErrorKind::NotConnected,
450 format!("Max {} reconnection attempts exceeded", limit),
451 ));
452 }
453 attempts += 1;
454 trace!(
455 "[PAIR] Stream disconnected, reconnecting (attempt {})",
456 attempts
457 );
458 self.try_reconnect().await?;
459 }
460
461 match self.recv().await {
462 Ok(Some(msg)) => return Ok(Some(msg)),
463 // EOF: read_raw() already set stream = None
464 Ok(None) => {
465 debug!("[PAIR] EOF on recv, will reconnect");
466 }
467 Err(e) => {
468 if self.base.stream.is_none()
469 || matches!(
470 e.kind(),
471 io::ErrorKind::ConnectionReset
472 | io::ErrorKind::ConnectionAborted
473 | io::ErrorKind::BrokenPipe
474 | io::ErrorKind::UnexpectedEof
475 )
476 {
477 debug!("[PAIR] Connection error on recv ({}), will reconnect", e);
478 self.base.stream = None;
479 } else {
480 return Err(e);
481 }
482 }
483 }
484 }
485 }
486
487 /// Send a message with automatic reconnection on network error.
488 ///
489 /// On BrokenPipe / ConnectionReset, `write_from_buf()` already sets
490 /// `stream = None`, so the next loop iteration reconnects automatically.
491 ///
492 /// Respects `max_reconnect_attempts` - returns `NotConnected` when exhausted.
493 pub async fn send_with_reconnect(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
494 let max = self.base.options.max_reconnect_attempts;
495 let mut attempts = 0u32;
496
497 loop {
498 if self.base.stream.is_none() {
499 if let Some(limit) = max
500 && attempts >= limit
501 {
502 return Err(io::Error::new(
503 io::ErrorKind::NotConnected,
504 format!("Max {} reconnection attempts exceeded", limit),
505 ));
506 }
507 attempts += 1;
508 trace!(
509 "[PAIR] Stream disconnected, reconnecting (attempt {})",
510 attempts
511 );
512 self.try_reconnect().await?;
513 }
514
515 match self.send(msg.clone()).await {
516 Ok(()) => return Ok(()),
517 Err(_) if self.base.stream.is_none() => {
518 // write_from_buf set stream = None → network error, retry
519 debug!("[PAIR] Send failed (stream lost), will reconnect");
520 }
521 Err(e) => return Err(e),
522 }
523 }
524 }
525}
526
527// Specialized implementation for Inproc streams
528impl PairSocket<InprocStream> {
529 /// Bind to an inproc endpoint.
530 ///
531 /// Creates a new inproc endpoint that other sockets can connect to.
532 /// Inproc endpoints must be bound before they can be connected to.
533 ///
534 /// # Arguments
535 ///
536 /// * `endpoint` - Inproc URI (e.g., "inproc://my-endpoint")
537 ///
538 /// # Example
539 ///
540 /// ```no_run
541 /// use monocoque_zmtp::pair::PairSocket;
542 ///
543 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
544 /// let socket = PairSocket::bind_inproc("inproc://my-pair")?;
545 /// # Ok(())
546 /// # }
547 /// ```
548 pub fn bind_inproc(endpoint: &str) -> io::Result<Self> {
549 Self::bind_inproc_with_options(endpoint, SocketOptions::default())
550 }
551
552 /// Bind to an inproc endpoint with custom configuration and options.
553 pub fn bind_inproc_with_options(endpoint: &str, options: SocketOptions) -> io::Result<Self> {
554 debug!("[PAIR] Binding to inproc endpoint: {}", endpoint);
555
556 // Bind to inproc endpoint
557 let (tx, rx) = monocoque_core::inproc::bind_inproc(endpoint)?;
558 let stream = InprocStream::new(tx, rx);
559
560 // Parse endpoint for storage
561 let parsed_endpoint = Endpoint::parse(endpoint)
562 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
563
564 debug!("[PAIR] Bound to {}", endpoint);
565
566 // For inproc, no handshake needed (same process)
567 Ok(Self {
568 base: SocketBase::with_endpoint(stream, SocketType::Pair, parsed_endpoint, options),
569 frames: SmallVec::new(),
570 })
571 }
572
573 /// Connect to an inproc endpoint.
574 ///
575 /// Connects to a previously bound inproc endpoint.
576 ///
577 /// # Arguments
578 ///
579 /// * `endpoint` - Inproc URI (e.g., "inproc://my-endpoint")
580 ///
581 /// # Example
582 ///
583 /// ```no_run
584 /// use monocoque_zmtp::pair::PairSocket;
585 ///
586 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
587 /// let socket = PairSocket::connect_inproc("inproc://my-pair")?;
588 /// # Ok(())
589 /// # }
590 /// ```
591 pub fn connect_inproc(endpoint: &str) -> io::Result<Self> {
592 Self::connect_inproc_with_options(endpoint, SocketOptions::default())
593 }
594
595 /// Connect to an inproc endpoint with custom configuration and options.
596 pub fn connect_inproc_with_options(endpoint: &str, options: SocketOptions) -> io::Result<Self> {
597 debug!("[PAIR] Connecting to inproc endpoint: {}", endpoint);
598
599 // connect_inproc_bidi returns (to_server_tx, from_server_rx) so we can
600 // both send to the server and receive replies from it. The server must
601 // have been bound with bind_inproc_bidi.
602 let (tx, rx) = monocoque_core::inproc::connect_inproc_bidi(endpoint)?;
603 let stream = InprocStream::new(tx, rx);
604
605 // Parse endpoint for storage
606 let parsed_endpoint = Endpoint::parse(endpoint)
607 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
608
609 debug!("[PAIR] Connected to {}", endpoint);
610
611 // For inproc, no handshake needed (same process)
612 Ok(Self {
613 base: SocketBase::with_endpoint(stream, SocketType::Pair, parsed_endpoint, options),
614 frames: SmallVec::new(),
615 })
616 }
617}
618
619crate::impl_socket_trait!(PairSocket<S>, SocketType::Pair);