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