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