Skip to main content

monocoque/zmq/
publisher.rs

1//! PUB socket implementation with worker pool architecture.
2
3use bytes::Bytes;
4use monocoque_core::monitor::{SocketEventSender, SocketMonitor, create_monitor};
5use monocoque_core::options::SocketOptions;
6use monocoque_core::rt::TcpListener;
7use monocoque_zmtp::SocketType;
8use monocoque_zmtp::publisher::PubSocket as InternalPub;
9use std::io;
10
11/// A PUB socket for broadcasting messages to multiple subscribers.
12///
13/// PubSocket uses a **worker pool architecture** to handle multiple subscribers efficiently:
14/// - Multiple OS threads (default: CPU core count)
15/// - Each worker runs its own compio runtime with io_uring
16/// - Round-robin subscriber distribution across workers
17/// - Zero-copy message broadcasting via `Arc<Bytes>`
18/// - Lock-free subscription management
19///
20/// ## Example
21///
22/// ```rust,no_run
23/// use monocoque::zmq::PubSocket;
24/// use bytes::Bytes;
25///
26/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
27/// let mut socket = PubSocket::bind("127.0.0.1:5555").await?;
28///
29/// // Accept subscribers (non-blocking with worker pool)
30/// socket.accept_subscriber().await?;
31///
32/// // Broadcast to all subscribers
33/// socket.send(vec![Bytes::from("topic"), Bytes::from("data")]).await?;
34/// # Ok(())
35/// # }
36/// ```
37pub struct PubSocket {
38    inner: InternalPub,
39    listener: TcpListener,
40    monitor: Option<SocketEventSender>,
41}
42
43impl PubSocket {
44    /// Bind to an address with default worker count (CPU cores).
45    pub async fn bind(addr: impl monocoque_core::rt::ToSocketAddrs) -> io::Result<Self> {
46        let listener = TcpListener::bind(addr).await?;
47        Ok(Self {
48            inner: InternalPub::new()?,
49            listener,
50            monitor: None,
51        })
52    }
53
54    /// Bind with a specific number of worker threads.
55    pub async fn bind_with_workers(
56        addr: impl monocoque_core::rt::ToSocketAddrs,
57        worker_count: usize,
58    ) -> io::Result<Self> {
59        let listener = TcpListener::bind(addr).await?;
60        Ok(Self {
61            inner: InternalPub::with_workers(worker_count)?,
62            listener,
63            monitor: None,
64        })
65    }
66
67    /// Bind with custom socket options and the default worker count.
68    ///
69    /// [`bind`](Self::bind) and [`bind_with_workers`](Self::bind_with_workers)
70    /// leave every worker on [`SocketOptions::default`], which means write
71    /// coalescing stays off. A publisher pushing small frames to many
72    /// subscribers wants it on, so this is the constructor to reach for when
73    /// tuning a PUB socket for throughput.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error if the address cannot be bound or a worker thread
78    /// cannot be spawned.
79    pub async fn bind_with_options(
80        addr: impl monocoque_core::rt::ToSocketAddrs,
81        options: SocketOptions,
82    ) -> io::Result<Self> {
83        Self::bind_with_workers_opts(addr, InternalPub::default_worker_count(), options).await
84    }
85
86    /// Bind with a specific number of worker threads and custom socket options.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if the address cannot be bound or a worker thread
91    /// cannot be spawned.
92    pub async fn bind_with_workers_opts(
93        addr: impl monocoque_core::rt::ToSocketAddrs,
94        worker_count: usize,
95        options: SocketOptions,
96    ) -> io::Result<Self> {
97        let listener = TcpListener::bind(addr).await?;
98        Ok(Self {
99            inner: InternalPub::with_workers_opts(worker_count, options)?,
100            listener,
101            monitor: None,
102        })
103    }
104
105    /// Accept a new subscriber connection.
106    ///
107    /// Performs ZMTP handshake and assigns the subscriber to a worker thread.
108    /// Returns the subscriber ID.
109    pub async fn accept_subscriber(&mut self) -> io::Result<u64> {
110        self.inner.accept_subscriber(&self.listener).await
111    }
112
113    /// Broadcast a multipart message to all matching subscribers.
114    ///
115    /// Messages are distributed to all workers in parallel.
116    /// The first frame is typically used as a topic for subscription filtering.
117    pub async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
118        self.inner.send(msg).await
119    }
120
121    /// Broadcast a message given as borrowed frames.
122    ///
123    /// Allocation-light counterpart to [`send`](Self::send): the shared message
124    /// is allocated only when it matches a subscription, so publishing from a
125    /// stack array (`send_frames(&[topic, payload])`) pays no per-message heap
126    /// allocation on the common drop path of a topic-filtered stream.
127    pub async fn send_frames(&mut self, frames: &[Bytes]) -> io::Result<()> {
128        self.inner.send_frames(frames).await
129    }
130
131    /// Get the number of active subscribers.
132    pub const fn subscriber_count(&self) -> usize {
133        self.inner.subscriber_count()
134    }
135
136    /// Get the local address this socket is bound to.
137    pub fn local_addr(&self) -> io::Result<std::net::SocketAddr> {
138        self.listener.local_addr()
139    }
140
141    /// Get the socket type.
142    ///
143    /// # ZeroMQ Compatibility
144    ///
145    /// Corresponds to `ZMQ_TYPE` (16) option.
146    #[inline]
147    pub const fn socket_type() -> SocketType {
148        SocketType::Pub
149    }
150
151    /// Enable monitoring for this socket.
152    ///
153    /// Returns a receiver for socket lifecycle events.
154    pub fn monitor(&mut self) -> SocketMonitor {
155        let (sender, receiver) = create_monitor();
156        self.monitor = Some(sender);
157        receiver
158    }
159
160    /// Get a mutable reference to this socket's options.
161    #[inline]
162    pub fn options_mut(&mut self) -> &mut SocketOptions {
163        self.inner.options_mut()
164    }
165
166    /// Number of messages dropped due to HWM backpressure.
167    #[inline]
168    pub fn drop_count(&self) -> u64 {
169        self.inner.drop_count()
170    }
171}