Skip to main content

monocoque_zmtp/
xsub.rs

1//! XSUB (Extended Subscriber) socket implementation
2//!
3//! XSUB extends SUB by sending subscription messages upstream to publishers,
4//! enabling subscription forwarding in message brokers and dynamic subscription
5//! management.
6//!
7//! # Use Cases
8//!
9//! - **Message brokers**: Forward subscriptions from frontend to backend
10//! - **Cascading pub/sub**: Build subscription trees across network boundaries
11//! - **Dynamic subscriptions**: Programmatically manage topic interests
12//!
13//! # Pattern
14//!
15//! ```text
16//! XSUB ──subscribe("topic.a")──> Publisher
17//!      <──────data("topic.a")───
18//! XSUB ──subscribe("topic.b")──> Publisher
19//!      <──────data("topic.b")───
20//! ```
21
22use crate::base::SocketBase;
23use bytes::Bytes;
24use compio_io::{AsyncRead, AsyncWrite};
25use monocoque_core::endpoint::Endpoint;
26use monocoque_core::options::SocketOptions;
27use monocoque_core::rt::TcpStream;
28use monocoque_core::subscription::{SubscriptionEvent, SubscriptionTrie};
29use smallvec::SmallVec;
30use std::io;
31use tracing::{debug, trace};
32
33use crate::handshake::perform_handshake_with_options;
34use crate::session::SocketType;
35
36/// XSUB (Extended Subscriber) socket.
37///
38/// Receives data messages and can send subscription messages upstream.
39///
40/// # Features
41///
42/// - **Dynamic subscriptions**: Subscribe/unsubscribe at runtime
43/// - **Subscription forwarding**: Forward subscriptions in proxies
44/// - **Verbose unsubscribe**: Optionally send explicit unsubscribe messages
45///
46/// # Examples
47///
48/// ```no_run
49/// use monocoque_zmtp::xsub::XSubSocket;
50/// use bytes::Bytes;
51///
52/// # async fn example() -> std::io::Result<()> {
53/// let mut xsub = XSubSocket::connect("127.0.0.1:5555").await?;
54///     
55///     // Subscribe to topics
56///     xsub.subscribe("topic.").await?;
57///
58///     // Receive messages
59///     if let Some(msg) = xsub.recv().await? {
60///         println!("Received: {:?}", msg);
61///     }
62///
63///     // Unsubscribe
64/// xsub.unsubscribe("topic.").await?;
65///
66/// # Ok(())
67/// # }
68/// ```
69pub struct XSubSocket<S = TcpStream>
70where
71    S: AsyncRead + AsyncWrite + Unpin,
72{
73    /// Base socket infrastructure
74    base: SocketBase<S>,
75    /// Local subscription tracking (XSUB manages subscriptions locally)
76    subscriptions: SubscriptionTrie,
77}
78
79impl<S> XSubSocket<S>
80where
81    S: AsyncRead + AsyncWrite + Unpin,
82{
83    /// Create a new XSUB socket from a stream.
84    pub async fn new(stream: S) -> io::Result<Self> {
85        Self::with_options(stream, SocketOptions::default()).await
86    }
87
88    /// Create a new XSUB socket with custom configuration and options.
89    pub async fn with_options(mut stream: S, options: SocketOptions) -> io::Result<Self> {
90        debug!("[XSUB] Creating new XSUB socket");
91
92        // Perform ZMTP handshake
93        debug!("[XSUB] Performing ZMTP handshake...");
94        let handshake_result = perform_handshake_with_options(
95            &mut stream,
96            SocketType::Xsub,
97            None,
98            Some(options.handshake_timeout),
99            &options,
100        )
101        .await
102        .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
103
104        debug!(
105            peer_socket_type = ?handshake_result.peer_socket_type,
106            "[XSUB] Handshake complete"
107        );
108
109        let mut base = SocketBase::new(stream, SocketType::Xsub, options);
110        base.curve_cipher = handshake_result.curve_cipher;
111        Ok(Self {
112            base,
113            subscriptions: SubscriptionTrie::new(),
114        })
115    }
116
117    /// Subscribe to messages with the given prefix.
118    ///
119    /// Sends a subscription message upstream to the publisher.
120    ///
121    /// # Examples
122    ///
123    /// ```no_run
124    /// # use monocoque_zmtp::xsub::XSubSocket;
125    /// # async fn example(mut xsub: XSubSocket) -> std::io::Result<()> {
126    /// // Subscribe to all messages starting with "topic."
127    /// xsub.subscribe("topic.").await?;
128    ///
129    /// // Subscribe to all messages (empty prefix)
130    /// xsub.subscribe("").await?;
131    /// # Ok(())
132    /// # }
133    /// ```
134    pub async fn subscribe(&mut self, prefix: impl Into<Bytes>) -> io::Result<()> {
135        let prefix = prefix.into();
136        trace!("[XSUB] Subscribing to: {:?}", prefix);
137
138        self.subscriptions.subscribe(prefix.clone());
139
140        // Send subscription message upstream
141        let event = SubscriptionEvent::Subscribe(prefix);
142        self.send_subscription_event(event).await?;
143
144        Ok(())
145    }
146
147    /// Unsubscribe from messages with the given prefix.
148    ///
149    /// Optionally sends an unsubscribe message upstream (if verbose mode enabled).
150    ///
151    /// # Examples
152    ///
153    /// ```no_run
154    /// # use monocoque_zmtp::xsub::XSubSocket;
155    /// # use bytes::Bytes;
156    /// # async fn example(mut xsub: XSubSocket) -> std::io::Result<()> {
157    /// let prefix = Bytes::from_static(b"topic.");
158    /// xsub.unsubscribe(prefix).await?;
159    /// # Ok(())
160    /// # }
161    /// ```
162    pub async fn unsubscribe(&mut self, prefix: impl Into<Bytes>) -> io::Result<()> {
163        let prefix = prefix.into();
164        trace!("[XSUB] Unsubscribing from: {:?}", prefix);
165
166        self.subscriptions.unsubscribe(&prefix);
167
168        // Send unsubscribe message if verbose mode enabled
169        if self.base.options.xsub_verbose_unsubs {
170            let event = SubscriptionEvent::Unsubscribe(prefix);
171            self.send_subscription_event(event).await?;
172        }
173
174        Ok(())
175    }
176
177    /// Send a raw subscription event upstream (for proxies).
178    ///
179    /// This allows forwarding subscription messages in broker patterns.
180    ///
181    /// # Examples
182    ///
183    /// ```no_run
184    /// # use monocoque_zmtp::xsub::XSubSocket;
185    /// # use monocoque_core::subscription::SubscriptionEvent;
186    /// # use bytes::Bytes;
187    /// # async fn example(mut xsub: XSubSocket) -> std::io::Result<()> {
188    /// let event = SubscriptionEvent::Subscribe(Bytes::from("topic"));
189    /// xsub.send_subscription_event(event).await?;
190    /// # Ok(())
191    /// # }
192    /// ```
193    pub async fn send_subscription_event(&mut self, event: SubscriptionEvent) -> io::Result<()> {
194        use bytes::BytesMut;
195        use compio_buf::BufResult;
196        use compio_io::AsyncWriteExt;
197
198        let raw = event.to_message();
199        trace!(
200            "[XSUB] Sending subscription event ({} bytes): {:?}",
201            raw.len(),
202            raw
203        );
204
205        // Encrypt if CURVE is active; otherwise plain ZMTP frame.
206        let mut wire = BytesMut::new();
207        if let Some(ref mut cipher) = self.base.curve_cipher {
208            let body = cipher
209                .encrypt_frame(&raw, false)
210                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
211            crate::base::append_zmtp_cmd_frame(&mut wire, &body);
212        } else {
213            crate::codec::encode_multipart(&[raw], &mut wire);
214        }
215        let wire = wire.freeze();
216
217        let stream =
218            self.base.stream.as_mut().ok_or_else(|| {
219                io::Error::new(io::ErrorKind::NotConnected, "Socket not connected")
220            })?;
221
222        let data = wire.to_vec();
223        let BufResult(result, _) = stream.write_all(data).await;
224        result?;
225
226        trace!("[XSUB] Subscription event sent successfully");
227        Ok(())
228    }
229
230    /// Receive a data message (non-blocking).
231    ///
232    /// Returns `None` if no message is available.
233    ///
234    /// # Examples
235    ///
236    /// ```no_run
237    /// # use monocoque_zmtp::xsub::XSubSocket;
238    /// # async fn example(mut xsub: XSubSocket) -> std::io::Result<()> {
239    /// if let Some(msg) = xsub.recv().await? {
240    ///     for frame in msg {
241    ///         println!("Frame: {:?}", frame);
242    ///     }
243    /// }
244    /// # Ok(())
245    /// # }
246    /// ```
247    pub async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
248        let mut frames: SmallVec<[Bytes; 4]> = SmallVec::new();
249
250        loop {
251            loop {
252                match self.base.process_frame()? {
253                    crate::base::FrameResult::NeedMore => break,
254                    crate::base::FrameResult::CommandHandled => {
255                        if !self.base.send_buffer.is_empty() {
256                            self.base.flush_send_buffer().await?;
257                        }
258                    }
259                    crate::base::FrameResult::Data(more, payload) => {
260                        frames.push(payload);
261                        if !more {
262                            trace!("[XSUB] Received {} frames", frames.len());
263                            return Ok(Some(frames.into_vec()));
264                        }
265                    }
266                }
267            }
268
269            let n = self.base.read_raw().await?;
270            if n == 0 {
271                trace!("[XSUB] Connection closed");
272                return Ok(None);
273            }
274            if self.base.check_heartbeat()? {
275                self.base.flush_send_buffer().await?;
276            }
277        }
278    }
279
280    /// Get the number of active subscriptions.
281    pub fn subscription_count(&self) -> usize {
282        self.subscriptions.len()
283    }
284
285    /// Check if subscribed to a specific topic.
286    pub fn is_subscribed(&self, topic: &[u8]) -> bool {
287        self.subscriptions.matches(topic)
288    }
289
290    /// Get all subscriptions.
291    pub fn subscriptions(&self) -> Vec<monocoque_core::subscription::Subscription> {
292        self.subscriptions.subscriptions()
293    }
294
295    /// Get the socket type.
296    pub const fn socket_type(&self) -> SocketType {
297        SocketType::Xsub
298    }
299
300    /// Get the endpoint this socket is connected/bound to, if available.
301    ///
302    /// Returns `None` if the socket was created from a raw stream.
303    ///
304    /// # ZeroMQ Compatibility
305    ///
306    /// Corresponds to `ZMQ_LAST_ENDPOINT` (32) option.
307    #[inline]
308    pub fn last_endpoint(&self) -> Option<&Endpoint> {
309        self.base.last_endpoint()
310    }
311
312    /// Check if the last received message has more frames coming.
313    ///
314    /// Returns `true` if there are more frames in the current multipart message.
315    ///
316    /// # ZeroMQ Compatibility
317    ///
318    /// Corresponds to `ZMQ_RCVMORE` (13) option.
319    #[inline]
320    pub fn has_more(&self) -> bool {
321        self.base.has_more()
322    }
323
324    /// Get the event state of the socket.
325    ///
326    /// Returns a bitmask indicating ready-to-receive and ready-to-send states.
327    ///
328    /// # Returns
329    ///
330    /// - `1` (POLLIN) - Socket is ready to receive
331    /// - `2` (POLLOUT) - Socket is ready to send
332    /// - `3` (POLLIN | POLLOUT) - Socket is ready for both
333    ///
334    /// # ZeroMQ Compatibility
335    ///
336    /// Corresponds to `ZMQ_EVENTS` (15) option.
337    #[inline]
338    pub fn events(&self) -> u32 {
339        self.base.events()
340    }
341}
342
343impl XSubSocket<TcpStream> {
344    /// Connect to a publisher, storing the endpoint for automatic reconnection.
345    ///
346    /// # Examples
347    ///
348    /// ```no_run
349    /// # use monocoque_zmtp::xsub::XSubSocket;
350    /// # async fn example() -> std::io::Result<()> {
351    /// let xsub = XSubSocket::connect("127.0.0.1:5555").await?;
352    /// # Ok(())
353    /// # }
354    /// ```
355    pub async fn connect(addr: &str) -> io::Result<Self> {
356        Self::connect_with_options(addr, SocketOptions::default()).await
357    }
358
359    /// Connect with custom socket options, storing the endpoint for automatic reconnection.
360    pub async fn connect_with_options(addr: &str, options: SocketOptions) -> io::Result<Self> {
361        let stream = TcpStream::connect(addr).await?;
362        let peer_addr = stream.peer_addr()?;
363
364        // Enable TCP_NODELAY (and keepalive) on the outbound connection, matching
365        // SUB's connect path. One-time setsockopt at connect, off the hot path.
366        crate::utils::configure_tcp_stream(&stream, &options, "XSUB")?;
367
368        let mut stream = stream;
369        let handshake_result = crate::handshake::perform_handshake_with_options(
370            &mut stream,
371            crate::session::SocketType::Xsub,
372            None,
373            Some(options.handshake_timeout),
374            &options,
375        )
376        .await
377        .map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
378
379        debug!(
380            peer_identity = ?handshake_result.peer_identity,
381            peer_socket_type = ?handshake_result.peer_socket_type,
382            "[XSUB] Connected to {} (endpoint stored for reconnection)",
383            peer_addr
384        );
385
386        let endpoint = monocoque_core::endpoint::Endpoint::Tcp(peer_addr);
387        let mut base = crate::base::SocketBase::with_endpoint(
388            stream,
389            crate::session::SocketType::Xsub,
390            endpoint,
391            options,
392        );
393        base.curve_cipher = handshake_result.curve_cipher;
394        Ok(Self {
395            base,
396            subscriptions: SubscriptionTrie::new(),
397        })
398    }
399
400    /// Check if the socket is currently connected.
401    #[inline]
402    pub fn is_connected(&self) -> bool {
403        self.base.is_connected()
404    }
405
406    /// Try to reconnect to the stored endpoint, re-sending all active subscriptions.
407    pub async fn try_reconnect(&mut self) -> io::Result<()> {
408        self.base
409            .try_reconnect(crate::session::SocketType::Xsub)
410            .await?;
411        // Re-send all subscriptions to the fresh connection
412        let prefixes: Vec<bytes::Bytes> = self
413            .subscriptions
414            .subscriptions()
415            .iter()
416            .map(|s| s.prefix.clone())
417            .collect();
418        for prefix in prefixes {
419            self.send_subscription_event(
420                monocoque_core::subscription::SubscriptionEvent::Subscribe(prefix),
421            )
422            .await?;
423        }
424        Ok(())
425    }
426
427    /// Receive a message with automatic reconnection on EOF or network error.
428    ///
429    /// Respects `max_reconnect_attempts` - returns `NotConnected` when exhausted.
430    pub async fn recv_with_reconnect(&mut self) -> io::Result<Option<Vec<bytes::Bytes>>> {
431        let max = self.base.options.max_reconnect_attempts;
432        let mut attempts = 0u32;
433
434        loop {
435            if self.base.stream.is_none() {
436                if let Some(limit) = max {
437                    if attempts >= limit {
438                        return Err(io::Error::new(
439                            io::ErrorKind::NotConnected,
440                            format!("Max {} reconnection attempts exceeded", limit),
441                        ));
442                    }
443                }
444                attempts += 1;
445                trace!(
446                    "[XSUB] Stream disconnected, reconnecting (attempt {})",
447                    attempts
448                );
449                self.try_reconnect().await?;
450            }
451
452            match self.recv().await {
453                Ok(Some(msg)) => return Ok(Some(msg)),
454                Ok(None) => {
455                    debug!("[XSUB] EOF on recv, will reconnect");
456                }
457                Err(e) => {
458                    if self.base.stream.is_none()
459                        || matches!(
460                            e.kind(),
461                            io::ErrorKind::ConnectionReset
462                                | io::ErrorKind::ConnectionAborted
463                                | io::ErrorKind::BrokenPipe
464                                | io::ErrorKind::UnexpectedEof
465                        )
466                    {
467                        debug!("[XSUB] Connection error on recv ({}), will reconnect", e);
468                        self.base.stream = None;
469                    } else {
470                        return Err(e);
471                    }
472                }
473            }
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    /// XSUB's outbound TCP connection must have TCP_NODELAY set, matching SUB.
483    /// Drives a real connect against an XPUB peer and reads TCP_NODELAY off the
484    /// live XSUB socket fd. One-time setsockopt at connect - off the hot path.
485    #[cfg(unix)]
486    #[test]
487    fn xsub_connect_sets_tcp_nodelay() {
488        use monocoque_core::rt::LocalRuntime;
489        use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
490        use std::sync::mpsc;
491        use std::thread;
492
493        fn fd_nodelay(fd: RawFd) -> bool {
494            let sock = unsafe { socket2::Socket::from_raw_fd(fd) };
495            let nd = sock.nodelay().expect("query TCP_NODELAY");
496            std::mem::forget(sock); // borrowed fd - do not close it
497            nd
498        }
499
500        let (port_tx, port_rx) = mpsc::channel::<u16>();
501        let (nd_tx, nd_rx) = mpsc::channel::<bool>();
502        let (done_tx, done_rx) = mpsc::channel::<()>();
503
504        // XPUB peer: bind, announce the port, accept the XSUB, hold it open.
505        let server = thread::spawn(move || {
506            let rt = LocalRuntime::new().unwrap();
507            rt.block_on(async move {
508                let mut xpub = crate::xpub::XPubSocket::bind("127.0.0.1:0").await.unwrap();
509                port_tx.send(xpub.local_addr().unwrap().port()).unwrap();
510                xpub.accept().await.unwrap();
511                done_rx.recv().unwrap();
512            });
513        });
514
515        let port = port_rx.recv().unwrap();
516        let client = thread::spawn(move || {
517            let rt = LocalRuntime::new().unwrap();
518            rt.block_on(async move {
519                let xsub = XSubSocket::connect(&format!("127.0.0.1:{port}"))
520                    .await
521                    .unwrap();
522                let fd = xsub.base.stream.as_ref().unwrap().as_raw_fd();
523                nd_tx.send(fd_nodelay(fd)).unwrap();
524                done_tx.send(()).unwrap();
525            });
526        });
527
528        let nodelay = nd_rx.recv().unwrap();
529        client.join().unwrap();
530        server.join().unwrap();
531        assert!(
532            nodelay,
533            "XSUB connect must set TCP_NODELAY on the outbound socket",
534        );
535    }
536
537    #[test]
538    fn test_subscription_tracking() {
539        use monocoque_core::rt::LocalRuntime as Runtime;
540
541        Runtime::new().unwrap().block_on(async {
542            // Mock stream for testing
543            // In real tests, use actual TCP connection
544        });
545    }
546
547    #[test]
548    fn test_subscription_event_creation() {
549        let event = SubscriptionEvent::Subscribe(Bytes::from_static(b"topic"));
550        let msg = event.to_message();
551        assert_eq!(msg[0], 0x01);
552        assert_eq!(&msg[1..], b"topic");
553    }
554}
555
556crate::impl_socket_trait!(XSubSocket<S>, SocketType::Xsub);