Skip to main content

monocoque_zmtp/
xpub.rs

1//! XPUB (Extended Publisher) socket implementation
2//!
3//! XPUB extends PUB by receiving subscription messages from subscribers,
4//! enabling manual subscription control, last value cache patterns, and
5//! subscription forwarding in message brokers.
6//!
7//! # Use Cases
8//!
9//! - **Message brokers**: Forward subscriptions between frontend and backend
10//! - **Last value cache (LVC)**: Track subscriptions and replay latest values
11//! - **Subscription auditing**: Monitor what topics subscribers are interested in
12//! - **Manual control**: Explicitly approve/deny subscriptions
13//!
14//! # Pattern
15//!
16//! ```text
17//! Subscriber 1 ──subscribe("topic.a")──> ┐
18//! Subscriber 2 ──subscribe("topic.b")──> ├─> XPUB (receives subscription events)
19//! Subscriber 3 ──unsubscribe("topic.a")─> ┘        │
20//!                                                   │
21//!                                      XPUB ────────┴──> Forwards subscriptions
22//! ```
23
24use bytes::{Bytes, BytesMut};
25use monocoque_core::io::take_read_buffer;
26use monocoque_core::options::SocketOptions;
27use monocoque_core::rt::{TcpListener, TcpStream};
28use monocoque_core::subscription::{SubscriptionEvent, SubscriptionTrie};
29use smallvec::SmallVec;
30use std::collections::{HashMap, HashSet};
31use std::fmt;
32use std::io;
33use tracing::{debug, trace};
34
35use crate::handshake::perform_handshake_with_options;
36use crate::session::SocketType;
37use crate::xsub::XSubSocket;
38
39/// Unique identifier for each subscriber connection
40type SubscriberId = u64;
41
42/// Per-subscriber state managed by XPUB
43struct XPubSubscriber {
44    id: SubscriberId,
45    stream: TcpStream,
46    subscriptions: SubscriptionTrie,
47    recv_buf: monocoque_core::buffer::SegmentedBuffer,
48    read_buf: BytesMut,
49    decoder: crate::codec::ZmtpDecoder,
50    curve_cipher: Option<crate::security::curve::CurveMessageCipher>,
51}
52
53impl XPubSubscriber {
54    /// Check if message matches subscriber's subscriptions. With
55    /// ZMQ_INVERT_MATCHING the prefix result is negated: deliver to subscribers
56    /// whose prefixes do NOT match.
57    fn matches(&self, msg: &[Bytes], invert: bool) -> bool {
58        // Check first frame against subscription prefixes
59        if let Some(first_frame) = msg.first() {
60            let matched = self.subscriptions.matches(first_frame);
61            if invert { !matched } else { matched }
62        } else {
63            false
64        }
65    }
66}
67
68/// XPUB (Extended Publisher) socket.
69///
70/// Receives subscription events and broadcasts messages to matching subscribers.
71///
72/// # Features
73///
74/// - **Subscription tracking**: Know what topics subscribers want
75/// - **Verbose mode**: Report all subscriptions (including duplicates)
76/// - **Manual mode**: Explicit subscription control
77/// - **Welcome messages**: Send initial message to new subscribers
78///
79/// # Examples
80///
81/// ```no_run
82/// use monocoque_zmtp::xpub::XPubSocket;
83/// use bytes::Bytes;
84///
85/// # async fn example() -> std::io::Result<()> {
86/// let mut xpub = XPubSocket::bind("127.0.0.1:5555").await?;
87///     
88///     loop {
89///         // Receive subscription events from subscribers
90///         if let Some(event) = xpub.recv_subscription().await? {
91///             println!("Subscription event: {:?}", event);
92///         }
93///         
94///         // Broadcast messages to matching subscribers
95///         xpub.send(vec![Bytes::from("topic"), Bytes::from("data")]).await?;
96///     }
97/// # }
98/// ```
99pub struct XPubSocket {
100    listener: TcpListener,
101    subscribers: HashMap<SubscriberId, XPubSubscriber>,
102    next_id: SubscriberId,
103    options: SocketOptions,
104    /// Pending subscription events to deliver
105    pending_events: SmallVec<[SubscriptionEvent; 8]>,
106    /// Optional upstream connection for manual-mode subscription forwarding.
107    ///
108    /// When set, `send_subscription()` writes subscription events to this
109    /// connection so they propagate to the upstream publisher.
110    upstream: Option<XSubSocket<TcpStream>>,
111    /// Tracks which unique topic prefixes currently have at least one subscriber.
112    ///
113    /// Used in non-verbose mode to deliver an event only the FIRST time a topic
114    /// is subscribed (and when it transitions back to zero subscribers).
115    seen_topics: HashSet<Vec<u8>>,
116    /// Reference-count of active subscriptions per topic prefix.
117    ///
118    /// Maps topic prefix → number of active subscribers interested in it.
119    /// When the count drops to zero, the topic is removed from `seen_topics`
120    /// and an Unsubscribe event is delivered.
121    topic_refcount: HashMap<Vec<u8>, usize>,
122}
123
124impl XPubSocket {
125    /// Bind to an address and start listening for subscribers.
126    ///
127    /// # Examples
128    ///
129    /// ```no_run
130    /// # use monocoque_zmtp::xpub::XPubSocket;
131    /// # async fn example() -> std::io::Result<()> {
132    /// let xpub = XPubSocket::bind("127.0.0.1:5555").await?;
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub async fn bind(addr: &str) -> io::Result<Self> {
137        Self::bind_with_options(addr, SocketOptions::default()).await
138    }
139
140    /// Bind with custom socket options.
141    ///
142    /// Honors `options.reuse_port`: when set, the listener is bound with
143    /// `SO_REUSEPORT` so several XPUB acceptors can share one port.
144    pub async fn bind_with_options(addr: &str, options: SocketOptions) -> io::Result<Self> {
145        let listener = if options.reuse_port {
146            let sock_addr = addr
147                .parse()
148                .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid bind address"))?;
149            monocoque_core::rt::bind_reuseport(sock_addr)?
150        } else {
151            TcpListener::bind(addr).await?
152        };
153        let local_addr = listener.local_addr()?;
154        debug!("[XPUB] Bound to {}", local_addr);
155
156        Ok(Self {
157            listener,
158            subscribers: HashMap::new(),
159            next_id: 1,
160            options,
161            pending_events: SmallVec::new(),
162            upstream: None,
163            seen_topics: HashSet::new(),
164            topic_refcount: HashMap::new(),
165        })
166    }
167
168    /// Accept new subscriber connections (non-blocking).
169    ///
170    /// Call this periodically to accept new subscribers.
171    pub async fn accept(&mut self) -> io::Result<()> {
172        match self.listener.accept().await {
173            Ok((mut stream, addr)) => {
174                debug!("[XPUB] New subscriber from {}", addr);
175
176                // Enable TCP_NODELAY (and keepalive) on the accepted subscriber,
177                // matching PUB's accept path. One-time setsockopt at accept, not
178                // in the publish hot path.
179                crate::utils::configure_tcp_stream(&stream, &self.options, "XPUB")?;
180
181                // Perform ZMTP handshake
182                let handshake_result = perform_handshake_with_options(
183                    &mut stream,
184                    SocketType::Xpub,
185                    self.options.routing_id.as_deref(),
186                    Some(self.options.handshake_timeout),
187                    &self.options,
188                )
189                .await?;
190
191                debug!(
192                    peer_socket_type = ?handshake_result.peer_socket_type,
193                    "[XPUB] Handshake complete with subscriber"
194                );
195
196                // Add subscriber
197                let id = self.next_id;
198                self.next_id += 1;
199
200                let mut curve_cipher = handshake_result.curve_cipher;
201
202                // Send welcome message if configured
203                if let Some(ref welcome_msg) = self.options.xpub_welcome_msg.clone() {
204                    use bytes::BytesMut;
205                    use compio_buf::BufResult;
206                    use compio_io::AsyncWriteExt;
207
208                    let wire = if let Some(ref mut cipher) = curve_cipher {
209                        let mut buf = BytesMut::new();
210                        let body = cipher.encrypt_frame(welcome_msg, false).map_err(|e| {
211                            io::Error::new(io::ErrorKind::InvalidData, e.to_string())
212                        })?;
213                        crate::base::append_zmtp_cmd_frame(&mut buf, &body);
214                        buf.freeze()
215                    } else {
216                        let mut buf = BytesMut::with_capacity(welcome_msg.len() + 9);
217                        crate::codec::encode_multipart(std::slice::from_ref(welcome_msg), &mut buf);
218                        buf.freeze()
219                    };
220
221                    let BufResult(result, _) = stream.write_all(wire).await;
222                    if let Err(e) = result {
223                        trace!(
224                            "[XPUB] Failed to send welcome message to subscriber {}: {}",
225                            id, e
226                        );
227                    }
228                }
229
230                self.subscribers.insert(
231                    id,
232                    XPubSubscriber {
233                        id,
234                        stream,
235                        subscriptions: SubscriptionTrie::new(),
236                        recv_buf: monocoque_core::buffer::SegmentedBuffer::new(),
237                        read_buf: BytesMut::new(),
238                        decoder: self.options.max_msg_size.map_or_else(
239                            crate::codec::ZmtpDecoder::new,
240                            crate::codec::ZmtpDecoder::with_max_frame_size,
241                        ),
242                        curve_cipher,
243                    },
244                );
245
246                debug!(
247                    "[XPUB] Subscriber {} added (total: {})",
248                    id,
249                    self.subscribers.len()
250                );
251                Ok(())
252            }
253            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
254                // No pending connections
255                Ok(())
256            }
257            Err(e) => {
258                // Throttle on fd exhaustion so a caller's accept loop cannot
259                // livelock while no descriptors are available.
260                crate::utils::backoff_on_fd_exhaustion(&e).await;
261                Err(e)
262            }
263        }
264    }
265
266    /// Receive a subscription event from subscribers (non-blocking).
267    ///
268    /// Returns `None` if no events are available.
269    ///
270    /// # Examples
271    ///
272    /// ```no_run
273    /// # use monocoque_zmtp::xpub::XPubSocket;
274    /// # async fn example(mut xpub: XPubSocket) -> std::io::Result<()> {
275    /// if let Some(event) = xpub.recv_subscription().await? {
276    ///     match event {
277    ///         monocoque_core::subscription::SubscriptionEvent::Subscribe(topic) => {
278    ///             println!("New subscription: {:?}", topic);
279    ///         }
280    ///         monocoque_core::subscription::SubscriptionEvent::Unsubscribe(topic) => {
281    ///             println!("Unsubscription: {:?}", topic);
282    ///         }
283    ///     }
284    /// }
285    /// # Ok(())
286    /// # }
287    /// ```
288    #[allow(clippy::too_many_lines)]
289    pub async fn recv_subscription(&mut self) -> io::Result<Option<SubscriptionEvent>> {
290        use compio_buf::BufResult;
291        use compio_io::AsyncRead;
292        use monocoque_core::rt::timeout;
293        use std::time::Duration;
294
295        // Return pending events first
296        if !self.pending_events.is_empty() {
297            return Ok(Some(self.pending_events.remove(0)));
298        }
299
300        // NOTE: Don't call accept() here - it blocks waiting for new connections
301        // The caller should call accept() separately to handle new connections
302
303        // Poll all subscribers for subscription messages
304        trace!(
305            "[XPUB] Polling {} subscribers for subscription events",
306            self.subscribers.len()
307        );
308        // Subscribers whose subscription stream failed to decode: evicted after
309        // the poll so a malformed frame is not re-decoded (and re-failed) on
310        // every poll, which would livelock the XPUB.
311        let mut to_evict: Vec<u64> = Vec::new();
312        for sub in self.subscribers.values_mut() {
313            // SAFETY: `slab` is passed straight to `read`; the data arm below
314            // truncates it to `n` before freezing, and every other arm drops it
315            // without inspecting its contents.
316            let slab = unsafe { take_read_buffer(&mut sub.read_buf, 256) };
317
318            // Use a short timeout to avoid blocking
319            let read_result = timeout(Duration::from_millis(1), sub.stream.read(slab)).await;
320
321            match read_result {
322                Ok(BufResult(Ok(n), mut slab)) if n > 0 => {
323                    trace!("[XPUB] Received {} bytes from subscriber {}", n, sub.id);
324                    debug_assert!(n <= 256);
325                    slab.truncate(n);
326                    sub.recv_buf.push(slab.freeze());
327
328                    // Drain all complete ZMTP frames from the buffer
329                    loop {
330                        match sub.decoder.decode(&mut sub.recv_buf) {
331                            Ok(Some(frame)) => {
332                                // Resolve the subscription payload, handling CURVE decryption.
333                                let payload = if frame.is_command() {
334                                    if let Some(ref mut cipher) = sub.curve_cipher {
335                                        if crate::security::curve::CurveMessageCipher::is_curve_message(&frame.payload) {
336                                            match cipher.decrypt_frame(&frame.payload) {
337                                                Ok((_more, data)) => data,
338                                                Err(_) => continue,
339                                            }
340                                        } else {
341                                            // Non-MESSAGE command (e.g. PING): handle and skip.
342                                            if crate::base::is_ping_payload(&frame.payload) {
343                                                use compio_io::AsyncWriteExt;
344                                                let pong = crate::base::build_pong_frame();
345                                                let BufResult(result, _) = sub.stream.write_all(pong).await;
346                                                let _ = result;
347                                            }
348                                            continue;
349                                        }
350                                    } else {
351                                        if crate::base::is_ping_payload(&frame.payload) {
352                                            use compio_io::AsyncWriteExt;
353                                            let pong = crate::base::build_pong_frame();
354                                            let BufResult(result, _) =
355                                                sub.stream.write_all(pong).await;
356                                            let _ = result;
357                                        }
358                                        continue;
359                                    }
360                                } else {
361                                    frame.payload
362                                };
363                                if let Some(event) = SubscriptionEvent::from_bytes(payload) {
364                                    trace!(
365                                        "[XPUB] Subscription event from subscriber {}: {:?}",
366                                        sub.id, event
367                                    );
368
369                                    let should_deliver = if self.options.xpub_verbose {
370                                        // Verbose mode: always deliver every event
371                                        match &event {
372                                            SubscriptionEvent::Subscribe(prefix) => {
373                                                sub.subscriptions.subscribe(prefix.clone());
374                                                let key = prefix.to_vec();
375                                                *self
376                                                    .topic_refcount
377                                                    .entry(key.clone())
378                                                    .or_insert(0) += 1;
379                                                self.seen_topics.insert(key);
380                                            }
381                                            SubscriptionEvent::Unsubscribe(prefix) => {
382                                                sub.subscriptions.unsubscribe(prefix);
383                                                let key = prefix.to_vec();
384                                                let count = self
385                                                    .topic_refcount
386                                                    .entry(key.clone())
387                                                    .or_insert(0);
388                                                if *count > 0 {
389                                                    *count -= 1;
390                                                }
391                                                if *count == 0 {
392                                                    self.seen_topics.remove(&key);
393                                                    self.topic_refcount.remove(&key);
394                                                }
395                                            }
396                                        }
397                                        true
398                                    } else {
399                                        // Non-verbose mode: deliver only on first subscribe / last unsubscribe
400                                        match &event {
401                                            SubscriptionEvent::Subscribe(prefix) => {
402                                                sub.subscriptions.subscribe(prefix.clone());
403                                                let key = prefix.to_vec();
404                                                let count = self
405                                                    .topic_refcount
406                                                    .entry(key.clone())
407                                                    .or_insert(0);
408                                                *count += 1;
409                                                if *count == 1 {
410                                                    // First subscriber for this topic
411                                                    self.seen_topics.insert(key);
412                                                    true
413                                                } else {
414                                                    false
415                                                }
416                                            }
417                                            SubscriptionEvent::Unsubscribe(prefix) => {
418                                                sub.subscriptions.unsubscribe(prefix);
419                                                let key = prefix.to_vec();
420                                                let count = self
421                                                    .topic_refcount
422                                                    .entry(key.clone())
423                                                    .or_insert(0);
424                                                if *count > 0 {
425                                                    *count -= 1;
426                                                }
427                                                if *count == 0 {
428                                                    // Last subscriber gone for this topic
429                                                    self.seen_topics.remove(&key);
430                                                    self.topic_refcount.remove(&key);
431                                                    true
432                                                } else {
433                                                    false
434                                                }
435                                            }
436                                        }
437                                    };
438
439                                    if should_deliver {
440                                        self.pending_events.push(event);
441                                    }
442                                }
443                            }
444                            Ok(None) => break,
445                            Err(_) => {
446                                // Malformed subscription frame: drop this
447                                // subscriber rather than re-decoding it forever.
448                                to_evict.push(sub.id);
449                                break;
450                            }
451                        }
452                    }
453                }
454                Ok(BufResult(Ok(_), _)) => {}
455                Ok(BufResult(Err(e), _)) => {
456                    if e.kind() != std::io::ErrorKind::WouldBlock {
457                        debug!("[XPUB] Error reading from subscriber {}: {}", sub.id, e);
458                    }
459                }
460                Err(_) => {
461                    // Timeout  -  no data available from this subscriber
462                }
463            }
464        }
465
466        // Evict subscribers whose subscription stream failed to decode.
467        for id in to_evict {
468            self.subscribers.remove(&id);
469        }
470
471        // Return any events collected from this poll round
472        if !self.pending_events.is_empty() {
473            return Ok(Some(self.pending_events.remove(0)));
474        }
475
476        Ok(None)
477    }
478
479    /// Broadcast a message to all matching subscribers.
480    ///
481    /// Only subscribers whose subscriptions match the message's first frame
482    /// will receive it.
483    ///
484    /// # Examples
485    ///
486    /// ```no_run
487    /// # use monocoque_zmtp::xpub::XPubSocket;
488    /// # use bytes::Bytes;
489    /// # async fn example(mut xpub: XPubSocket) -> std::io::Result<()> {
490    /// xpub.send(vec![
491    ///     Bytes::from("topic.temperature"),
492    ///     Bytes::from("23.5"),
493    /// ]).await?;
494    /// # Ok(())
495    /// # }
496    /// ```
497    pub async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
498        use bytes::BytesMut;
499        use compio_buf::BufResult;
500        use compio_io::AsyncWriteExt;
501
502        trace!("[XPUB] Broadcasting message with {} frames", msg.len());
503
504        // Pre-encode once for plaintext subscribers (shared via O(1) clone).
505        // Encrypted subscribers get per-subscriber encoding below.
506        let mut plain_wire: Option<bytes::Bytes> = None;
507
508        let mut dead_subs = Vec::new();
509        let invert = self.options.invert_matching;
510
511        for sub in self.subscribers.values_mut() {
512            if !sub.matches(&msg, invert) {
513                continue;
514            }
515
516            let wire = if let Some(ref mut cipher) = sub.curve_cipher {
517                let last = msg.len().saturating_sub(1);
518                let mut buf = BytesMut::new();
519                let mut ok = true;
520                for (i, frame) in msg.iter().enumerate() {
521                    if let Ok(body) = cipher.encrypt_frame(frame, i < last) {
522                        crate::base::append_zmtp_cmd_frame(&mut buf, &body);
523                    } else {
524                        ok = false;
525                        break;
526                    }
527                }
528                if !ok {
529                    dead_subs.push(sub.id);
530                    continue;
531                }
532                buf.freeze()
533            } else {
534                plain_wire
535                    .get_or_insert_with(|| {
536                        let wire_capacity: usize = msg
537                            .iter()
538                            .map(|part| part.len() + if part.len() >= 256 { 9 } else { 2 })
539                            .sum();
540                        let mut buf = BytesMut::with_capacity(wire_capacity);
541                        crate::codec::encode_multipart(&msg, &mut buf);
542                        buf.freeze()
543                    })
544                    .clone()
545            };
546
547            let BufResult(result, _) = sub.stream.write_all(wire).await;
548            if let Err(e) = result {
549                debug!("[XPUB] Failed to send to subscriber {}: {}", sub.id, e);
550                dead_subs.push(sub.id);
551            } else {
552                trace!("[XPUB] Sent to subscriber {}", sub.id);
553            }
554        }
555
556        for id in dead_subs {
557            self.subscribers.remove(&id);
558            debug!("[XPUB] Removed dead subscriber {}", id);
559        }
560
561        Ok(())
562    }
563
564    /// Get the number of active subscribers.
565    pub fn subscriber_count(&self) -> usize {
566        self.subscribers.len()
567    }
568
569    /// Get the local address.
570    pub fn local_addr(&self) -> io::Result<std::net::SocketAddr> {
571        self.listener.local_addr()
572    }
573
574    /// Get the socket type.
575    pub const fn socket_type(&self) -> SocketType {
576        SocketType::Xpub
577    }
578
579    /// Check if the last received message has more frames coming.
580    ///
581    /// For XPUB, subscription events are always single-frame.
582    ///
583    /// # ZeroMQ Compatibility
584    ///
585    /// Corresponds to `ZMQ_RCVMORE` (13) option.
586    #[inline]
587    pub fn has_more(&self) -> bool {
588        !self.pending_events.is_empty()
589    }
590
591    /// Get the event state of the socket.
592    ///
593    /// Returns a bitmask indicating ready-to-receive and ready-to-send states.
594    ///
595    /// # Returns
596    ///
597    /// - `1` (POLLIN) - Socket is ready to receive (has pending subscription events)
598    /// - `2` (POLLOUT) - Socket is ready to send (has active subscribers)
599    /// - `3` (POLLIN | POLLOUT) - Socket is ready for both
600    ///
601    /// # ZeroMQ Compatibility
602    ///
603    /// Corresponds to `ZMQ_EVENTS` (15) option.
604    #[inline]
605    pub fn events(&self) -> u32 {
606        let mut events = 0;
607        if !self.pending_events.is_empty() {
608            events |= 1; // POLLIN
609        }
610        if !self.subscribers.is_empty() {
611            events |= 2; // POLLOUT
612        }
613        events
614    }
615
616    /// Set verbose mode.
617    ///
618    /// When enabled, all subscription messages are reported (including duplicates).
619    pub fn set_verbose(&mut self, verbose: bool) {
620        self.options.xpub_verbose = verbose;
621    }
622
623    /// Set manual mode.
624    ///
625    /// When enabled, subscriptions must be explicitly approved by calling `send_subscription()`.
626    pub fn set_manual(&mut self, manual: bool) {
627        self.options.xpub_manual = manual;
628    }
629
630    /// Connect to an upstream publisher so that subscription events can be forwarded.
631    ///
632    /// The upstream is typically a PUB or XSUB socket.  After calling this method,
633    /// `send_subscription()` (manual mode) writes subscription messages to the upstream
634    /// connection, causing the upstream publisher to start or stop delivering matching
635    /// messages.
636    ///
637    /// # Examples
638    ///
639    /// ```no_run
640    /// # use monocoque_zmtp::xpub::XPubSocket;
641    /// # use monocoque_core::subscription::SubscriptionEvent;
642    /// # use bytes::Bytes;
643    /// # async fn example() -> std::io::Result<()> {
644    /// let mut xpub = XPubSocket::bind("127.0.0.1:5556").await?;
645    /// xpub.set_manual(true);
646    /// xpub.connect_upstream("127.0.0.1:5555").await?;
647    ///
648    /// // Receive a subscription from a downstream client and forward it upstream.
649    /// if let Some(event) = xpub.recv_subscription().await? {
650    ///     xpub.send_subscription(event).await?;
651    /// }
652    /// # Ok(())
653    /// # }
654    /// ```
655    pub async fn connect_upstream(&mut self, addr: &str) -> io::Result<()> {
656        debug!("[XPUB] Connecting upstream to {}", addr);
657        let xsub = XSubSocket::connect(addr).await?;
658        self.upstream = Some(xsub);
659        debug!("[XPUB] Upstream connected");
660        Ok(())
661    }
662
663    /// Manually send a subscription event to the upstream connection.
664    ///
665    /// Requires both manual mode (`set_manual(true)`) and an upstream connection
666    /// (`connect_upstream()`).  Writes the subscription message directly to the
667    /// upstream publisher so it starts (or stops) delivering matching messages.
668    pub async fn send_subscription(&mut self, event: SubscriptionEvent) -> io::Result<()> {
669        if !self.options.xpub_manual {
670            return Err(io::Error::new(
671                io::ErrorKind::InvalidInput,
672                "Manual mode not enabled",
673            ));
674        }
675
676        let upstream = self.upstream.as_mut().ok_or_else(|| {
677            io::Error::new(
678                io::ErrorKind::NotConnected,
679                "No upstream connection; call connect_upstream() first",
680            )
681        })?;
682
683        trace!("[XPUB] Forwarding subscription upstream: {:?}", event);
684        upstream.send_subscription_event(event).await
685    }
686}
687
688impl fmt::Debug for XPubSocket {
689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690        f.debug_struct("XPubSocket")
691            .field("subscribers", &self.subscribers.len())
692            .field("verbose", &self.options.xpub_verbose)
693            .field("manual", &self.options.xpub_manual)
694            .field("has_upstream", &self.upstream.is_some())
695            .finish()
696    }
697}
698
699// Implement Socket trait for XPubSocket (non-generic)
700impl crate::Socket for XPubSocket {
701    async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
702        self.send(msg).await
703    }
704
705    async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
706        // XPUB receives subscription events
707        self.recv_subscription()
708            .await
709            .map(|opt| opt.map(|event| vec![event.to_message()]))
710    }
711
712    fn socket_type(&self) -> SocketType {
713        SocketType::Xpub
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use crate::publisher::PubSocket as InternalPub;
721
722    /// XPUB must set TCP_NODELAY on accepted subscriber connections, matching
723    /// PUB. Connects a real XSUB peer, then reads TCP_NODELAY off the stored
724    /// subscriber's socket fd. One-time setsockopt at accept - off the hot path.
725    #[cfg(unix)]
726    #[test]
727    fn xpub_accept_sets_tcp_nodelay() {
728        use monocoque_core::rt::LocalRuntime;
729        use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
730        use std::sync::mpsc;
731        use std::thread;
732
733        fn fd_nodelay(fd: RawFd) -> bool {
734            let sock = unsafe { socket2::Socket::from_raw_fd(fd) };
735            let nd = sock.tcp_nodelay().expect("query TCP_NODELAY");
736            std::mem::forget(sock); // borrowed fd - do not close it
737            nd
738        }
739
740        let (port_tx, port_rx) = mpsc::channel::<u16>();
741        let (done_tx, done_rx) = mpsc::channel::<()>();
742
743        // XSUB client: connect once the port is known, then hold the connection.
744        let client = thread::spawn(move || {
745            let rt = LocalRuntime::new().unwrap();
746            rt.block_on(async move {
747                let port = port_rx.recv().unwrap();
748                let _xsub = crate::xsub::XSubSocket::connect(&format!("127.0.0.1:{port}"))
749                    .await
750                    .unwrap();
751                done_rx.recv().unwrap();
752            });
753        });
754
755        let rt = LocalRuntime::new().unwrap();
756        let nodelay = rt.block_on(async move {
757            let mut xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
758            port_tx.send(xpub.local_addr().unwrap().port()).unwrap();
759            xpub.accept().await.unwrap();
760            let sub = xpub.subscribers.values().next().expect("one subscriber");
761            fd_nodelay(sub.stream.as_raw_fd())
762        });
763        done_tx.send(()).unwrap();
764        client.join().unwrap();
765        assert!(
766            nodelay,
767            "XPUB accept must set TCP_NODELAY on subscriber sockets",
768        );
769    }
770
771    #[test]
772    fn test_xpub_bind() {
773        monocoque_core::rt::LocalRuntime::new()
774            .unwrap()
775            .block_on(test_xpub_bind_impl());
776    }
777
778    async fn test_xpub_bind_impl() {
779        let xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
780        assert_eq!(xpub.subscriber_count(), 0);
781        let addr = xpub.local_addr().unwrap();
782        assert!(addr.port() > 0);
783    }
784
785    #[test]
786    fn test_subscription_event_encoding() {
787        let event = SubscriptionEvent::Subscribe(Bytes::from_static(b"topic"));
788        let msg = event.to_message();
789        assert_eq!(msg[0], 0x01);
790        assert_eq!(&msg[1..], b"topic");
791
792        let parsed = SubscriptionEvent::from_message(&msg).unwrap();
793        assert_eq!(parsed, event);
794    }
795
796    /// `send_subscription` errors when manual mode is off.
797    #[test]
798    fn test_send_subscription_requires_manual_mode() {
799        monocoque_core::rt::LocalRuntime::new()
800            .unwrap()
801            .block_on(test_send_subscription_requires_manual_mode_impl());
802    }
803
804    async fn test_send_subscription_requires_manual_mode_impl() {
805        let mut xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
806        // manual mode is off by default
807        let err = xpub
808            .send_subscription(SubscriptionEvent::Subscribe(Bytes::from("topic")))
809            .await
810            .unwrap_err();
811        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
812    }
813
814    /// `send_subscription` errors when no upstream is connected.
815    #[test]
816    fn test_send_subscription_requires_upstream() {
817        monocoque_core::rt::LocalRuntime::new()
818            .unwrap()
819            .block_on(test_send_subscription_requires_upstream_impl());
820    }
821
822    async fn test_send_subscription_requires_upstream_impl() {
823        let mut xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
824        xpub.set_manual(true);
825        let err = xpub
826            .send_subscription(SubscriptionEvent::Subscribe(Bytes::from("topic")))
827            .await
828            .unwrap_err();
829        assert_eq!(err.kind(), std::io::ErrorKind::NotConnected);
830    }
831
832    /// `connect_upstream` + `send_subscription` forward subscription bytes to a PubSocket.
833    ///
834    /// The PubSocket's subscription reader (running inside a worker thread) picks up the
835    /// raw subscription bytes written by the upstream XSubSocket. We verify this
836    /// indirectly: after forwarding Subscribe("weather"), publishing a "weather" message
837    /// reaches the upstream connection (the XSubSocket), confirming the PUB socket
838    /// started delivering matching messages.
839    #[test]
840    fn test_connect_upstream_and_forward_subscription() {
841        monocoque_core::rt::LocalRuntime::new()
842            .unwrap()
843            .block_on(test_connect_upstream_and_forward_subscription_impl());
844    }
845
846    async fn test_connect_upstream_and_forward_subscription_impl() {
847        use monocoque_core::rt::TcpListener;
848
849        // Bind a PubSocket listener (the upstream data source).
850        let pub_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
851        let pub_addr = pub_listener.local_addr().unwrap();
852
853        // Spawn PubSocket: accept the XSubSocket upstream connection, then broadcast.
854        let pub_task = monocoque_core::rt::spawn(async move {
855            let mut pub_sock = InternalPub::new().unwrap();
856            // Accept the connection that connect_upstream() will make.
857            pub_sock.accept_subscriber(&pub_listener).await.unwrap();
858            // Give the subscription reader time to process Subscribe("weather").
859            monocoque_core::rt::sleep(std::time::Duration::from_millis(50)).await;
860            // Broadcast a matching message  -  should reach the upstream XSubSocket.
861            pub_sock
862                .send(vec![Bytes::from("weather"), Bytes::from("sunny")])
863                .await
864                .unwrap();
865        });
866
867        let mut xpub = XPubSocket::bind("127.0.0.1:0").await.unwrap();
868        xpub.set_manual(true);
869
870        // Connect upstream to the PubSocket listener.
871        xpub.connect_upstream(&pub_addr.to_string()).await.unwrap();
872        assert!(xpub.upstream.is_some());
873
874        // Forward a subscription to the PubSocket.
875        xpub.send_subscription(SubscriptionEvent::Subscribe(Bytes::from("weather")))
876            .await
877            .unwrap();
878
879        // Wait for the PubSocket to broadcast.
880        monocoque_core::rt::join(pub_task).await;
881
882        // The upstream XSubSocket should have received the "weather" message.
883        let msg = xpub
884            .upstream
885            .as_mut()
886            .unwrap()
887            .recv()
888            .await
889            .unwrap()
890            .expect("upstream should have received matching message");
891
892        assert_eq!(msg[0], Bytes::from("weather"));
893        assert_eq!(msg[1], Bytes::from("sunny"));
894    }
895}