Skip to main content

monocoque_zmtp/
pull.rs

1//! PULL socket implementation
2//!
3//! PULL sockets are receive-only endpoints in the pipeline pattern. They receive
4//! messages from connected PUSH sockets in a fair-queued manner.
5//!
6//! # Characteristics
7//!
8//! - **Receive-only**: Cannot send messages
9//! - **Fair-queued**: Receives from all PUSH sockets fairly
10//! - **Pipeline pattern**: For receiving tasks from distributors
11//! - **No filtering**: All messages are delivered
12//!
13//! # Use Cases
14//!
15//! - Task receiver (worker pattern)
16//! - Parallel pipeline processing
17//! - Work queue consumption
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 smallvec::SmallVec;
26use std::io;
27use tracing::{debug, trace};
28
29/// PULL socket for receiving messages in a pipeline.
30///
31/// PULL sockets receive messages from connected PUSH sockets, providing
32/// the worker side of the pipeline pattern.
33pub struct PullSocket<S = TcpStream>
34where
35    S: AsyncRead + AsyncWrite + Unpin,
36{
37    /// Base socket infrastructure (stream, buffers, options)
38    base: SocketBase<S>,
39    /// Accumulated frames for current multipart message
40    frames: SmallVec<[Bytes; 4]>,
41}
42
43impl<S> PullSocket<S>
44where
45    S: AsyncRead + AsyncWrite + Unpin,
46{
47    /// Create a new PULL socket from a stream with default buffer configuration.
48    pub async fn new(stream: S) -> io::Result<Self> {
49        Self::with_options(stream, SocketOptions::default()).await
50    }
51
52    /// Create a new PULL socket with custom buffer configuration and socket options.
53    pub async fn with_options(mut stream: S, options: SocketOptions) -> io::Result<Self> {
54        debug!("[PULL] Creating new PULL socket");
55
56        // Perform ZMTP handshake
57        debug!("[PULL] Performing ZMTP handshake...");
58        let handshake_result = perform_handshake_with_options(
59            &mut stream,
60            SocketType::Pull,
61            options.routing_id.as_deref(),
62            Some(options.handshake_timeout),
63            &options,
64        )
65        .await
66        .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
67
68        debug!(
69            peer_identity = ?handshake_result.peer_identity,
70            peer_socket_type = ?handshake_result.peer_socket_type,
71            "[PULL] Handshake complete"
72        );
73
74        debug!("[PULL] Socket initialized");
75
76        let mut base = SocketBase::new(stream, SocketType::Pull, options);
77        base.curve_cipher = handshake_result.curve_cipher;
78        Ok(Self {
79            base,
80            frames: SmallVec::new(),
81        })
82    }
83
84    /// Try to receive a message from the already-buffered input without doing a
85    /// kernel read.
86    ///
87    /// Decodes from bytes already present in the receive buffer. Returns
88    /// `Ok(None)` immediately when the buffer is empty rather than suspending.
89    /// Use this after `recv()` to drain all messages from a single read batch
90    /// before returning to the io_uring submission loop:
91    ///
92    /// ```rust,no_run
93    /// # async fn example(pull: &mut monocoque_zmtp::PullSocket) -> std::io::Result<()> {
94    /// // One kernel read may deliver many messages - drain them all before
95    /// // going back to the event loop.
96    /// if let Some(first) = pull.recv().await? {
97    ///     process(first);
98    ///     while let Some(msg) = pull.try_recv()? {
99    ///         process(msg);
100    ///     }
101    /// }
102    /// # fn process(_: Vec<bytes::Bytes>) {}
103    /// # Ok(())
104    /// # }
105    /// ```
106    ///
107    /// When a PING heartbeat command is decoded the corresponding PONG is
108    /// queued in the send buffer; the next `recv()` call flushes it. For
109    /// pure pipeline throughput benchmarks (where heartbeats are inactive) this
110    /// is never triggered.
111    pub fn try_recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
112        loop {
113            match self.base.process_frame()? {
114                crate::base::FrameResult::NeedMore => return Ok(None),
115                crate::base::FrameResult::CommandHandled => {
116                    // PONG or other response is already in send_buffer;
117                    // the next recv() call will flush it.
118                }
119                crate::base::FrameResult::Data(more, payload) => {
120                    if !more && self.frames.is_empty() {
121                        return Ok(Some(vec![payload]));
122                    }
123                    self.frames.push(payload);
124                    if !more {
125                        let msg: Vec<Bytes> = self.frames.drain(..).collect();
126                        return Ok(Some(msg));
127                    }
128                }
129            }
130        }
131    }
132
133    /// Try to receive a message into a caller-provided buffer, without a kernel read.
134    ///
135    /// The allocation-free counterpart to [`try_recv`](Self::try_recv): on a
136    /// complete message the frames are moved into `out` (reusing its capacity) and
137    /// `Ok(true)` is returned; when no complete message is buffered it returns
138    /// `Ok(false)` and leaves `out` untouched. Partial frames stay in the socket's
139    /// accumulator, so it interleaves correctly with [`recv_into`](Self::recv_into)
140    /// for multipart messages split across reads.
141    pub fn try_recv_into(&mut self, out: &mut Vec<Bytes>) -> io::Result<bool> {
142        loop {
143            match self.base.process_frame()? {
144                crate::base::FrameResult::NeedMore => return Ok(false),
145                crate::base::FrameResult::CommandHandled => {}
146                crate::base::FrameResult::Data(more, payload) => {
147                    if !more && self.frames.is_empty() {
148                        out.clear();
149                        out.push(payload);
150                        return Ok(true);
151                    }
152                    self.frames.push(payload);
153                    if !more {
154                        out.clear();
155                        out.extend(self.frames.drain(..));
156                        return Ok(true);
157                    }
158                }
159            }
160        }
161    }
162
163    /// Receive a message from a connected PUSH socket.
164    ///
165    /// When multiple PUSH sockets are connected, messages are received
166    /// in a fair-queued manner (in a multi-connection scenario).
167    ///
168    /// Returns `Ok(Some(msg))` if a message was received, `Ok(None)` if the
169    /// connection was closed, or an error.
170    pub async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
171        trace!("[PULL] Waiting for message");
172
173        // Read from stream until we have a complete message
174        loop {
175            // Try to decode frames from buffer
176            loop {
177                match self.base.process_frame()? {
178                    crate::base::FrameResult::NeedMore => break,
179                    crate::base::FrameResult::CommandHandled => {
180                        if !self.base.send_buffer.is_empty() {
181                            self.base.flush_send_buffer().await?;
182                        }
183                    }
184                    crate::base::FrameResult::Data(more, payload) => {
185                        if !more && self.frames.is_empty() {
186                            trace!("[PULL] Received 1 frame");
187                            return Ok(Some(vec![payload]));
188                        }
189                        self.frames.push(payload);
190                        if !more {
191                            let msg: Vec<Bytes> = self.frames.drain(..).collect();
192                            trace!("[PULL] Received {} frames", msg.len());
193                            return Ok(Some(msg));
194                        }
195                    }
196                }
197            }
198
199            // Need more data - read raw bytes from stream
200            let n = self.base.read_raw().await?;
201            if n == 0 {
202                // EOF - connection closed
203                trace!("[PULL] Connection closed");
204                return Ok(None);
205            }
206            if self.base.check_heartbeat()? {
207                self.base.flush_send_buffer().await?;
208            }
209            // Continue decoding with new data
210        }
211    }
212
213    /// Receive a message into a caller-provided buffer, reusing its allocation.
214    ///
215    /// Identical to [`recv`](Self::recv) except the message frames are pushed
216    /// straight into `out` instead of a freshly allocated `Vec`. The caller keeps
217    /// one `Vec` and passes it on every call, so a steady recv loop performs no
218    /// per-message heap allocation (the dominant per-message cost at small message
219    /// sizes). `out` is cleared on entry.
220    ///
221    /// Returns `Ok(true)` when a complete message was read into `out`, `Ok(false)`
222    /// when the connection was closed.
223    pub async fn recv_into(&mut self, out: &mut Vec<Bytes>) -> io::Result<bool> {
224        out.clear();
225        loop {
226            loop {
227                match self.base.process_frame()? {
228                    crate::base::FrameResult::NeedMore => break,
229                    crate::base::FrameResult::CommandHandled => {
230                        if !self.base.send_buffer.is_empty() {
231                            self.base.flush_send_buffer().await?;
232                        }
233                    }
234                    crate::base::FrameResult::Data(more, payload) => {
235                        if !more && self.frames.is_empty() {
236                            out.push(payload);
237                            return Ok(true);
238                        }
239                        // Accumulate in the shared frame buffer so a multipart
240                        // message split across reads (or across a try_recv_into)
241                        // is reassembled correctly, then move it into `out`,
242                        // reusing the caller's allocation.
243                        self.frames.push(payload);
244                        if !more {
245                            out.extend(self.frames.drain(..));
246                            return Ok(true);
247                        }
248                    }
249                }
250            }
251
252            let n = self.base.read_raw().await?;
253            if n == 0 {
254                return Ok(false);
255            }
256            if self.base.check_heartbeat()? {
257                self.base.flush_send_buffer().await?;
258            }
259        }
260    }
261
262    /// Receive a batch of messages with a single `.await`.
263    ///
264    /// Blocks until at least one message is available (like [`recv`](Self::recv)),
265    /// then drains every further message already decoded from the same kernel
266    /// read(s) without suspending again. One `read` frequently delivers many
267    /// small messages; returning them all from one `.await` amortizes the
268    /// per-await overhead that becomes a real fraction of the budget at
269    /// multi-million-msg/s rates. It is the receive-side counterpart to
270    /// [`PushSocket::send_batch`](crate::push::PushSocket::send_batch).
271    ///
272    /// Returns `Ok(Some(batch))` with one or more messages (in arrival order),
273    /// or `Ok(None)` if the connection was closed before any message arrived.
274    pub async fn recv_batch(&mut self) -> io::Result<Option<Vec<Vec<Bytes>>>> {
275        let Some(first) = self.recv().await? else {
276            return Ok(None);
277        };
278
279        let mut batch = Vec::with_capacity(8);
280        batch.push(first);
281
282        // Drain everything else already sitting in the receive buffer.
283        while let Some(msg) = self.try_recv()? {
284            batch.push(msg);
285        }
286
287        // A PING may have been decoded mid-drain, queuing a PONG; flush it.
288        if !self.base.send_buffer.is_empty() {
289            self.base.flush_send_buffer().await?;
290        }
291
292        Ok(Some(batch))
293    }
294
295    /// Close the socket gracefully by shutting down the underlying stream.
296    pub async fn close(mut self) -> io::Result<()> {
297        trace!("[PULL] Closing socket");
298        self.base.close().await
299    }
300
301    /// Get a reference to the socket options.
302    #[inline]
303    pub const fn options(&self) -> &SocketOptions {
304        &self.base.options
305    }
306
307    /// Get a mutable reference to the socket options.
308    #[inline]
309    pub fn options_mut(&mut self) -> &mut SocketOptions {
310        &mut self.base.options
311    }
312
313    /// Set socket options (builder-style).
314    #[inline]
315    pub fn set_options(&mut self, options: SocketOptions) {
316        self.base.set_options(options);
317    }
318}
319
320// Specialized implementation for TCP streams to enable TCP_NODELAY
321impl PullSocket<TcpStream> {
322    /// Create a new PULL socket from a TCP stream with TCP_NODELAY enabled.
323    pub async fn from_tcp(stream: TcpStream) -> io::Result<Self> {
324        Self::from_tcp_with_options(stream, SocketOptions::default()).await
325    }
326
327    /// Create a new PULL socket from a TCP stream with TCP_NODELAY and custom options.
328    pub async fn from_tcp_with_options(
329        stream: TcpStream,
330        options: SocketOptions,
331    ) -> io::Result<Self> {
332        // Configure TCP optimizations including keepalive
333        crate::utils::configure_tcp_stream(&stream, &options, "PULL")?;
334        Self::with_options(stream, options).await
335    }
336
337    /// Connect to a remote PULL socket, storing the endpoint for automatic reconnection.
338    pub async fn connect(addr: impl monocoque_core::rt::ToSocketAddrs) -> io::Result<Self> {
339        Self::connect_with_options(addr, SocketOptions::default()).await
340    }
341
342    /// Connect with custom options, storing the endpoint for reconnection.
343    pub async fn connect_with_options(
344        addr: impl monocoque_core::rt::ToSocketAddrs,
345        options: SocketOptions,
346    ) -> io::Result<Self> {
347        let stream = TcpStream::connect(addr).await?;
348        let peer_addr = stream.peer_addr()?;
349        crate::utils::configure_tcp_stream(&stream, &options, "PULL")?;
350
351        let mut stream = stream;
352        let handshake_result = perform_handshake_with_options(
353            &mut stream,
354            SocketType::Pull,
355            options.routing_id.as_deref(),
356            Some(options.handshake_timeout),
357            &options,
358        )
359        .await
360        .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
361
362        debug!(
363            peer_identity = ?handshake_result.peer_identity,
364            peer_socket_type = ?handshake_result.peer_socket_type,
365            "[PULL] Connected to {} (endpoint stored for reconnection)",
366            peer_addr
367        );
368
369        let endpoint = monocoque_core::endpoint::Endpoint::Tcp(peer_addr);
370        let mut base =
371            crate::base::SocketBase::with_endpoint(stream, SocketType::Pull, endpoint, options);
372        base.curve_cipher = handshake_result.curve_cipher;
373        Ok(Self {
374            base,
375            frames: SmallVec::new(),
376        })
377    }
378
379    /// Check if the socket is currently connected.
380    #[inline]
381    pub fn is_connected(&self) -> bool {
382        self.base.is_connected()
383    }
384
385    /// Try to reconnect to the stored endpoint.
386    pub async fn try_reconnect(&mut self) -> io::Result<()> {
387        self.base.try_reconnect(SocketType::Pull).await
388    }
389
390    /// Receive a message with automatic reconnection on EOF or network error.
391    ///
392    /// If the socket was created with `connect()` and stores an endpoint, this
393    /// method loops: on EOF or broken-pipe it clears the stream and calls
394    /// `try_reconnect()` (which applies exponential backoff), then retries `recv()`.
395    ///
396    /// Respects `max_reconnect_attempts`  -  returns `NotConnected` when exhausted.
397    pub async fn recv_with_reconnect(&mut self) -> io::Result<Option<Vec<Bytes>>> {
398        let max = self.base.options.max_reconnect_attempts;
399        let mut attempts = 0u32;
400
401        loop {
402            if self.base.stream.is_none() {
403                if let Some(limit) = max
404                    && attempts >= limit
405                {
406                    return Err(io::Error::new(
407                        io::ErrorKind::NotConnected,
408                        format!("Max {} reconnection attempts exceeded", limit),
409                    ));
410                }
411                attempts += 1;
412                trace!(
413                    "[PULL] Stream disconnected, reconnecting (attempt {})",
414                    attempts
415                );
416                self.try_reconnect().await?;
417            }
418
419            match self.recv().await {
420                Ok(Some(msg)) => return Ok(Some(msg)),
421                // EOF: read_raw() already set stream = None
422                Ok(None) => {
423                    debug!("[PULL] EOF on recv, will reconnect");
424                }
425                Err(e) => {
426                    if self.base.stream.is_none()
427                        || matches!(
428                            e.kind(),
429                            io::ErrorKind::ConnectionReset
430                                | io::ErrorKind::ConnectionAborted
431                                | io::ErrorKind::BrokenPipe
432                                | io::ErrorKind::UnexpectedEof
433                        )
434                    {
435                        debug!("[PULL] Connection error on recv ({}), will reconnect", e);
436                        self.base.stream = None;
437                    } else {
438                        return Err(e);
439                    }
440                }
441            }
442        }
443    }
444}
445
446crate::impl_socket_trait_recv_only!(PullSocket<S>, SocketType::Pull);