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