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}