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}