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);