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}