Skip to main content

ruststream_zeromq/
queue.rs

1//! [`ZmqQueue`]: the PUSH/PULL pattern - competing consumers, round-robin.
2
3use std::sync::Arc;
4
5use futures::Stream;
6use ruststream::{
7    Broker, ConnectedBroker, DefaultPublish, DescribeServer, OutgoingMessage, PairError,
8    PublishPolicy, Publisher, ServerSpec, Subscribe, Subscriber,
9};
10use tokio::sync::{Mutex, OnceCell, mpsc};
11use zeromq::prelude::*;
12use zeromq::{PullSocket, PushSocket};
13
14use crate::common::{DriverHandle, Lifecycle, SharedLifecycle, send_with_retry};
15use crate::endpoint::ZmqEndpoint;
16use crate::error::ZmqError;
17use crate::message::ZmqMessage;
18use crate::wire;
19
20/// The PUSH/PULL queue: each message reaches one of the competing consumers.
21///
22/// # Examples
23///
24/// ```
25/// use ruststream_zeromq::{ZmqEndpoint, ZmqQueue};
26///
27/// let consumer = ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555"));
28/// let producer = ZmqQueue::new(ZmqEndpoint::connect("tcp://worker:5555"));
29/// # let _ = (consumer, producer);
30/// ```
31#[derive(Debug, Clone)]
32#[must_use]
33pub struct ZmqQueue {
34    endpoint: ZmqEndpoint,
35    cell: Arc<OnceCell<SharedLifecycle>>,
36}
37
38impl ZmqQueue {
39    /// Records the endpoint. No I/O.
40    pub fn new(endpoint: ZmqEndpoint) -> Self {
41        Self {
42            endpoint,
43            cell: Arc::new(OnceCell::new()),
44        }
45    }
46
47    /// A publisher sharing this queue's state; buildable before `connect`.
48    #[must_use]
49    pub fn publisher(&self) -> ZmqQueuePublisher {
50        ZmqQueuePublisher {
51            cell: Arc::clone(&self.cell),
52            push: Arc::new(Mutex::new(None)),
53        }
54    }
55}
56
57impl Broker for ZmqQueue {
58    type Error = ZmqError;
59    type Connected = ConnectedZmqQueue;
60
61    async fn connect(self) -> Result<Self::Connected, Self::Error> {
62        let lifecycle = self
63            .cell
64            .get_or_try_init(async || {
65                self.endpoint.validate()?;
66                Ok::<_, ZmqError>(Arc::new(Lifecycle::new(self.endpoint.clone())))
67            })
68            .await?
69            .clone();
70        Ok(ConnectedZmqQueue {
71            lifecycle,
72            cell: self.cell,
73        })
74    }
75}
76
77impl DescribeServer for ZmqQueue {
78    fn describe_server(&self) -> ServerSpec {
79        ServerSpec::new(self.endpoint.address(), "zeromq")
80    }
81}
82
83/// The connected form of [`ZmqQueue`]; sockets attach lazily per subscription and publisher.
84#[derive(Debug)]
85pub struct ConnectedZmqQueue {
86    lifecycle: SharedLifecycle,
87    cell: Arc<OnceCell<SharedLifecycle>>,
88}
89
90impl ConnectedZmqQueue {
91    /// The address a local subscription resolved by binding (useful with an ephemeral
92    /// `tcp://...:0` endpoint); `None` until a subscription has bound.
93    #[must_use]
94    pub fn bound_address(&self) -> Option<String> {
95        self.lifecycle.resolved.get().cloned()
96    }
97
98    /// A publisher from the connected form.
99    #[must_use]
100    pub fn publisher(&self) -> ZmqQueuePublisher {
101        ZmqQueuePublisher {
102            cell: Arc::clone(&self.cell),
103            push: Arc::new(Mutex::new(None)),
104        }
105    }
106}
107
108impl ConnectedBroker for ConnectedZmqQueue {
109    type Error = ZmqError;
110    type Closed = ();
111
112    async fn shutdown(self) -> Result<(), Self::Error> {
113        self.lifecycle
114            .closed
115            .store(true, std::sync::atomic::Ordering::Release);
116        Ok(())
117    }
118}
119
120impl Subscribe for ConnectedZmqQueue {
121    type Subscriber = ZmqSubscriber;
122
123    async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
124        self.lifecycle.ensure_open()?;
125        let mut socket = PullSocket::new();
126        self.lifecycle.attach_receiver(&mut socket).await?;
127
128        let (tx, rx) = mpsc::unbounded_channel();
129        let task = tokio::spawn(async move {
130            loop {
131                match socket.recv().await {
132                    Ok(message) => {
133                        let item =
134                            wire::decode(message).map(|(name, headers, payload)| ZmqMessage {
135                                name,
136                                headers,
137                                payload,
138                            });
139                        if tx.send(item).is_err() {
140                            break;
141                        }
142                    }
143                    Err(err) => {
144                        if tx.send(Err(ZmqError::Receive(err.to_string()))).is_err() {
145                            break;
146                        }
147                    }
148                }
149            }
150        });
151        Ok(ZmqSubscriber {
152            name: name.to_owned(),
153            rx,
154            _driver: DriverHandle { task },
155        })
156    }
157}
158
159/// A subscription on one of the transport's patterns; yields [`ZmqMessage`]s.
160pub struct ZmqSubscriber {
161    name: String,
162    rx: mpsc::UnboundedReceiver<Result<ZmqMessage, ZmqError>>,
163    _driver: DriverHandle,
164}
165
166impl std::fmt::Debug for ZmqSubscriber {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("ZmqSubscriber")
169            .field("name", &self.name)
170            .finish_non_exhaustive()
171    }
172}
173
174impl ZmqSubscriber {
175    pub(crate) fn from_parts(
176        name: String,
177        rx: mpsc::UnboundedReceiver<Result<ZmqMessage, ZmqError>>,
178        driver: DriverHandle,
179    ) -> Self {
180        Self {
181            name,
182            rx,
183            _driver: driver,
184        }
185    }
186}
187
188impl Subscriber for ZmqSubscriber {
189    type Message = ZmqMessage;
190    type Error = ZmqError;
191
192    fn stream(&mut self) -> impl Stream<Item = Result<ZmqMessage, ZmqError>> + Send + '_ {
193        // Poll the channel in place rather than wrapping it in an owning stream, so `stream`
194        // can be called again after the returned stream is dropped (the runtime and the
195        // conformance helpers re-enter it per call).
196        futures::stream::poll_fn(move |cx| self.rx.poll_recv(cx))
197    }
198}
199
200/// Publishes into the queue over a lazily attached PUSH socket.
201#[derive(Clone)]
202pub struct ZmqQueuePublisher {
203    cell: Arc<OnceCell<SharedLifecycle>>,
204    push: Arc<Mutex<Option<PushSocket>>>,
205}
206
207impl std::fmt::Debug for ZmqQueuePublisher {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        f.debug_struct("ZmqQueuePublisher").finish_non_exhaustive()
210    }
211}
212
213impl Publisher for ZmqQueuePublisher {
214    type Error = ZmqError;
215
216    // The socket guard intentionally spans the lazy attach and the send: the socket takes
217    // &mut for every operation.
218    #[allow(clippy::significant_drop_tightening)]
219    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
220        let lifecycle = self.cell.get().ok_or(ZmqError::NotConnected)?;
221        lifecycle.ensure_open()?;
222        let mut push = self.push.lock().await;
223        if push.is_none() {
224            let mut socket = PushSocket::new();
225            lifecycle.attach_sender(&mut socket).await?;
226            *push = Some(socket);
227        }
228        let socket = push.as_mut().expect("just attached");
229        send_with_retry(
230            socket,
231            msg.name(),
232            wire::encode(msg.name(), msg.headers(), msg.payload()),
233        )
234        .await
235    }
236}
237
238/// The publish policy for [`ZmqQueuePublisher`].
239///
240/// # Examples
241///
242/// ```
243/// use ruststream_zeromq::ZmqQueuePublish;
244///
245/// let policy = ZmqQueuePublish::default();
246/// # let _ = policy;
247/// ```
248#[derive(Debug, Clone, Copy, Default)]
249#[must_use]
250pub struct ZmqQueuePublish;
251
252impl PublishPolicy<ConnectedZmqQueue> for ZmqQueuePublish {
253    type Live = ZmqQueuePublisher;
254
255    async fn pair(self, connected: &ConnectedZmqQueue) -> Result<Self::Live, PairError> {
256        Ok(connected.publisher())
257    }
258}
259
260impl DefaultPublish for ConnectedZmqQueue {
261    type Policy = ZmqQueuePublish;
262}