Skip to main content

monocoque_zmtp/
push.rs

1//! PUSH socket implementation
2//!
3//! PUSH sockets are send-only endpoints in the pipeline pattern. They distribute
4//! messages in a round-robin fashion to connected PULL sockets.
5//!
6//! # Characteristics
7//!
8//! - **Send-only**: Cannot receive messages
9//! - **Load balancing**: Distributes work across PULL sockets
10//! - **Non-blocking**: Never blocks on slow receivers (drops if HWM reached)
11//! - **Pipeline pattern**: For distributing tasks to workers
12//!
13//! # Use Cases
14//!
15//! - Task distribution (ventilator pattern)
16//! - Parallel pipeline processing
17//! - Work queue distribution
18
19use crate::base::SocketBase;
20use crate::{handshake::perform_handshake_with_options, session::SocketType};
21use bytes::Bytes;
22use compio_io::{AsyncRead, AsyncWrite};
23use monocoque_core::options::SocketOptions;
24use monocoque_core::rt::TcpStream;
25use std::io;
26use tracing::{debug, trace};
27
28/// PUSH socket for distributing messages in a pipeline.
29///
30/// PUSH sockets send messages to connected PULL sockets in a round-robin
31/// fashion, providing load balancing for parallel processing.
32pub struct PushSocket<S = TcpStream>
33where
34    S: AsyncRead + AsyncWrite + Unpin,
35{
36    /// Base socket infrastructure (stream, buffers, options)
37    base: SocketBase<S>,
38}
39
40impl<S> PushSocket<S>
41where
42    S: AsyncRead + AsyncWrite + Unpin,
43{
44    /// Create a new PUSH socket from a stream with default buffer configuration.
45    pub async fn new(stream: S) -> io::Result<Self> {
46        Self::with_options(stream, SocketOptions::default()).await
47    }
48
49    /// Create a new PUSH socket with custom buffer configuration and socket options.
50    pub async fn with_options(mut stream: S, options: SocketOptions) -> io::Result<Self> {
51        debug!("[PUSH] Creating new PUSH socket");
52
53        // Perform ZMTP handshake
54        debug!("[PUSH] Performing ZMTP handshake...");
55        let handshake_result = perform_handshake_with_options(
56            &mut stream,
57            SocketType::Push,
58            options.routing_id.as_deref(),
59            Some(options.handshake_timeout),
60            &options,
61        )
62        .await
63        .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
64
65        debug!(
66            peer_identity = ?handshake_result.peer_identity,
67            peer_socket_type = ?handshake_result.peer_socket_type,
68            "[PUSH] Handshake complete"
69        );
70
71        debug!("[PUSH] Socket initialized");
72
73        let mut base = SocketBase::new(stream, SocketType::Push, options);
74        base.curve_cipher = handshake_result.curve_cipher;
75        Ok(Self { base })
76    }
77
78    /// Send a message to a connected PULL socket.
79    ///
80    /// Messages are distributed in a round-robin fashion when multiple
81    /// PULL sockets are connected (in a multi-connection scenario).
82    ///
83    /// By default each call writes to the kernel immediately (eager mode, one
84    /// io_uring operation per message). For throughput-bound pipelines, enable write
85    /// coalescing via [`SocketOptions::with_write_coalescing`] and call
86    /// [`flush`](Self::flush) after the last send in each burst. In coalesced mode,
87    /// bytes may remain in userspace until the 64 KB threshold fills or `flush()` is
88    /// called explicitly.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if the socket is poisoned, disconnected, or if the write fails.
93    pub async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
94        trace!("[PUSH] Sending {} frames", msg.len());
95        self.base.send_message(&msg).await?;
96
97        // Check heartbeat: send PING if the connection has been idle too long
98        if self.base.check_heartbeat()? {
99            self.base.flush_send_buffer().await?;
100        }
101
102        trace!("[PUSH] Message sent successfully");
103        Ok(())
104    }
105
106    /// Send a single-frame message without allocating a one-element `Vec`.
107    ///
108    /// This is equivalent to `send(vec![frame])`, but keeps the hot path for
109    /// single-frame PUSH/PULL pipelines from measuring the caller's multipart
110    /// container allocation.
111    pub async fn send_one(&mut self, frame: Bytes) -> io::Result<()> {
112        trace!("[PUSH] Sending 1 frame");
113
114        if self.base.options.write_coalescing {
115            if self.base.encode_one_coalesced(&frame)? {
116                self.base.flush_send_buffer().await?;
117            }
118        } else {
119            let msg = std::slice::from_ref(&frame);
120            if self.base.should_vectored_write(msg) {
121                self.base.send_vectored(msg).await?;
122            } else {
123                self.base.encode_message_to_write_buf(msg)?;
124                self.base.write_from_buf().await?;
125            }
126        }
127
128        if self.base.check_heartbeat()? {
129            self.base.flush_send_buffer().await?;
130        }
131
132        trace!("[PUSH] Message sent successfully");
133        Ok(())
134    }
135
136    /// Flush any messages still buffered by write coalescing.
137    ///
138    /// Call this after the last `send()` in a burst when `write_coalescing` is
139    /// enabled to ensure all pending data is written to the kernel.
140    pub async fn flush(&mut self) -> io::Result<()> {
141        self.base.flush_send_buffer().await
142    }
143
144    /// Encode and send a batch of messages in a single kernel write.
145    ///
146    /// Encodes every message in `msgs` into the send buffer, then flushes once.
147    /// This gives the same kernel-call efficiency as write coalescing but with
148    /// explicit batch boundaries - no threshold check and no `flush()` required.
149    ///
150    /// Works independently of the `write_coalescing` option and can be mixed
151    /// with `send()` calls freely.
152    ///
153    /// Returns the number of messages sent.
154    pub async fn send_batch<I>(&mut self, msgs: I) -> io::Result<usize>
155    where
156        I: IntoIterator<Item = Vec<Bytes>>,
157    {
158        let mut count = 0;
159        for msg in msgs {
160            trace!("[PUSH] Buffering batch message {}", count);
161            self.base.encode_message_to_send_buf(&msg)?;
162            count += 1;
163        }
164        if count > 0 {
165            self.base.flush_send_buffer().await?;
166        }
167        if self.base.check_heartbeat()? {
168            self.base.flush_send_buffer().await?;
169        }
170        trace!("[PUSH] Batch of {} messages sent", count);
171        Ok(count)
172    }
173
174    /// Close the socket gracefully by shutting down the underlying stream.
175    pub async fn close(mut self) -> io::Result<()> {
176        trace!("[PUSH] Closing socket");
177        self.base.close().await
178    }
179
180    /// Get a reference to the socket options.
181    #[inline]
182    pub const fn options(&self) -> &SocketOptions {
183        &self.base.options
184    }
185
186    /// Get a mutable reference to the socket options.
187    #[inline]
188    pub fn options_mut(&mut self) -> &mut SocketOptions {
189        &mut self.base.options
190    }
191
192    /// Set socket options (builder-style).
193    #[inline]
194    pub fn set_options(&mut self, options: SocketOptions) {
195        self.base.set_options(options);
196    }
197}
198
199// Specialized implementation for TCP streams to enable TCP_NODELAY
200impl PushSocket<TcpStream> {
201    /// Create a new PUSH socket from a TCP stream with TCP_NODELAY enabled.
202    pub async fn from_tcp(stream: TcpStream) -> io::Result<Self> {
203        Self::from_tcp_with_options(stream, SocketOptions::default()).await
204    }
205
206    /// Create a new PUSH socket from a TCP stream with TCP_NODELAY and custom options.
207    pub async fn from_tcp_with_options(
208        stream: TcpStream,
209        options: SocketOptions,
210    ) -> io::Result<Self> {
211        // Configure TCP optimizations including keepalive
212        crate::utils::configure_tcp_stream(&stream, &options, "PUSH")?;
213        Self::with_options(stream, options).await
214    }
215
216    /// Connect to a remote PUSH socket, storing the endpoint for automatic reconnection.
217    pub async fn connect(addr: impl monocoque_core::rt::ToSocketAddrs) -> io::Result<Self> {
218        Self::connect_with_options(addr, SocketOptions::default()).await
219    }
220
221    /// Connect with custom options, storing the endpoint for reconnection.
222    pub async fn connect_with_options(
223        addr: impl monocoque_core::rt::ToSocketAddrs,
224        options: SocketOptions,
225    ) -> io::Result<Self> {
226        let stream = TcpStream::connect(addr).await?;
227        let peer_addr = stream.peer_addr()?;
228        crate::utils::configure_tcp_stream(&stream, &options, "PUSH")?;
229
230        let mut stream = stream;
231        let handshake_result = perform_handshake_with_options(
232            &mut stream,
233            SocketType::Push,
234            options.routing_id.as_deref(),
235            Some(options.handshake_timeout),
236            &options,
237        )
238        .await
239        .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
240
241        debug!(
242            peer_identity = ?handshake_result.peer_identity,
243            peer_socket_type = ?handshake_result.peer_socket_type,
244            "[PUSH] Connected to {} (endpoint stored for reconnection)",
245            peer_addr
246        );
247
248        let endpoint = monocoque_core::endpoint::Endpoint::Tcp(peer_addr);
249        let mut base =
250            crate::base::SocketBase::with_endpoint(stream, SocketType::Push, endpoint, options);
251        base.curve_cipher = handshake_result.curve_cipher;
252        Ok(Self { base })
253    }
254
255    /// Check if the socket is currently connected.
256    #[inline]
257    pub fn is_connected(&self) -> bool {
258        self.base.is_connected()
259    }
260
261    /// Try to reconnect to the stored endpoint.
262    pub async fn try_reconnect(&mut self) -> io::Result<()> {
263        self.base.try_reconnect(SocketType::Push).await
264    }
265
266    /// Send a message with automatic reconnection on network error.
267    ///
268    /// On BrokenPipe / ConnectionReset, `write_from_buf()` already sets
269    /// `stream = None`, so the next loop iteration reconnects automatically.
270    ///
271    /// Respects `max_reconnect_attempts`  -  returns `NotConnected` when exhausted.
272    pub async fn send_with_reconnect(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
273        let max = self.base.options.max_reconnect_attempts;
274        let mut attempts = 0u32;
275
276        loop {
277            if self.base.stream.is_none() {
278                if let Some(limit) = max
279                    && attempts >= limit
280                {
281                    return Err(io::Error::new(
282                        io::ErrorKind::NotConnected,
283                        format!("Max {} reconnection attempts exceeded", limit),
284                    ));
285                }
286                attempts += 1;
287                trace!(
288                    "[PUSH] Stream disconnected, reconnecting (attempt {})",
289                    attempts
290                );
291                self.try_reconnect().await?;
292            }
293
294            // Borrow msg instead of cloning: it must survive a possible retry
295            // after reconnect, but the happy first-try path pays no Vec clone.
296            match self.base.send_message(&msg).await {
297                Ok(()) => {
298                    if self.base.check_heartbeat()? {
299                        self.base.flush_send_buffer().await?;
300                    }
301                    return Ok(());
302                }
303                Err(_) if self.base.stream.is_none() => {
304                    // send_message set stream = None → network error, retry
305                    debug!("[PUSH] Send failed (stream lost), will reconnect");
306                }
307                Err(e) => return Err(e),
308            }
309        }
310    }
311}
312
313crate::impl_socket_trait_send_only!(PushSocket<S>, SocketType::Push);
314
315#[cfg(all(test, unix))]
316mod tests {
317    use super::*;
318    use monocoque_core::options::SocketOptions;
319    use monocoque_core::rt::{LocalRuntime, TcpListener};
320    use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
321    use std::sync::mpsc;
322    use std::thread;
323
324    /// Read `TCP_NODELAY` from a live fd without taking ownership of it.
325    fn fd_nodelay(fd: RawFd) -> bool {
326        let sock = unsafe { socket2::Socket::from_raw_fd(fd) };
327        let nd = sock.tcp_nodelay().expect("query TCP_NODELAY");
328        std::mem::forget(sock); // borrowed fd - do not close it
329        nd
330    }
331
332    /// A reconnect opens a brand-new fd that starts at the kernel default
333    /// (Nagle on), so the socket must re-apply `TCP_NODELAY`; otherwise latency
334    /// silently degrades after any automatic reconnect. This drives a real
335    /// connect followed by a forced reconnect and checks the live socket fd.
336    #[test]
337    fn tcp_nodelay_survives_reconnect() {
338        let (port_tx, port_rx) = mpsc::channel::<u16>();
339        let (done_tx, done_rx) = mpsc::channel::<()>();
340
341        // Server accepts twice - the initial connection and the forced
342        // reconnect - completing the ZMTP handshake each time with a real PULL
343        // peer, and holds both open until the client has inspected its socket.
344        let server = thread::spawn(move || {
345            let rt = LocalRuntime::new().unwrap();
346            rt.block_on(async move {
347                let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
348                port_tx.send(listener.local_addr().unwrap().port()).unwrap();
349
350                let (s1, _) = listener.accept().await.unwrap();
351                let _peer1 = crate::pull::PullSocket::new(s1).await.unwrap();
352
353                let (s2, _) = listener.accept().await.unwrap();
354                let _peer2 = crate::pull::PullSocket::new(s2).await.unwrap();
355
356                done_rx.recv().unwrap();
357            });
358        });
359
360        let port = port_rx.recv().unwrap();
361        let client = thread::spawn(move || {
362            let rt = LocalRuntime::new().unwrap();
363            rt.block_on(async move {
364                let mut push =
365                    PushSocket::connect_with_options(("127.0.0.1", port), SocketOptions::default())
366                        .await
367                        .unwrap();
368
369                // The initial connection sets NODELAY (existing behavior).
370                let fd0 = push.base.stream.as_ref().unwrap().as_raw_fd();
371                assert!(fd_nodelay(fd0), "initial connect must set TCP_NODELAY");
372
373                // Force a reconnect: a fresh fd that defaults to Nagle-on.
374                push.try_reconnect().await.unwrap();
375
376                let fd1 = push.base.stream.as_ref().unwrap().as_raw_fd();
377                assert!(
378                    fd_nodelay(fd1),
379                    "TCP_NODELAY must be re-applied on the reconnected socket",
380                );
381
382                done_tx.send(()).unwrap();
383            });
384        });
385
386        client.join().unwrap();
387        server.join().unwrap();
388    }
389}