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