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