ruststream_zeromq/queue.rs
1//! [`ZmqQueue`]: the PUSH/PULL pattern, competing consumers served in turn.
2//!
3//! - [ZmqQueue][queue]: [`ZmqQueue`], connected into [`ConnectedZmqQueue`], with
4//! [`read_ahead`](ZmqQueue::read_ahead) as its one setting beside the endpoint.
5//! - [Batches][batches]: [`ZmqSubscriber`], the subscription of both one-way patterns, singly or
6//! in batches.
7//! - [Publishing][publishing]: [`ZmqQueuePublish`], offered as [`Publish`] by [`prelude`], and its
8//! live [`ZmqQueuePublisher`].
9//! - [Replies][replies]: results and replies through a queue on an endpoint of its own.
10//! - [out_retry][out-retry]: retry copies, back into a bound queue or to a place the mount names.
11//!
12//! [queue]: https://powersemmi.github.io/ruststream-zeromq/latest/components/broker/zmq-queue/
13//! [batches]: https://powersemmi.github.io/ruststream-zeromq/latest/components/subscriptions/batches/
14//! [publishing]: https://powersemmi.github.io/ruststream-zeromq/latest/components/publishing/
15//! [replies]: https://powersemmi.github.io/ruststream-zeromq/latest/components/publishing/replies/
16//! [out-retry]: https://powersemmi.github.io/ruststream-zeromq/latest/components/settlement/out-retry/
17
18/// The publish policy of this form, under the name a mount site writes.
19pub use self::ZmqQueuePublish as Publish;
20
21/// The imports a routes file on the PUSH/PULL queue writes, in one glob: the framework's prelude,
22/// the shared [`ZmqEndpoint`], the broker [`ZmqQueue`], and its publish policy as [`Publish`].
23///
24/// # Examples
25///
26/// ```
27/// use ruststream_zeromq::queue::prelude::*;
28/// use serde::{Deserialize, Serialize};
29///
30/// #[derive(Deserialize)]
31/// struct Job {
32/// id: u64,
33/// }
34///
35/// // The result queue belongs to the message, so the type names it and the clause stays bare.
36/// #[derive(Serialize, Outgoing)]
37/// #[outgoing(name = "results")]
38/// struct Done {
39/// id: u64,
40/// }
41///
42/// #[subscriber("jobs", reply)]
43/// async fn handle(job: &Job) -> Done {
44/// Done { id: job.id }
45/// }
46///
47/// #[ruststream::app]
48/// fn app() -> impl App {
49/// // The results travel on a queue of their own: the one the jobs arrive on would hand a
50/// // result back to `handle` as its next job.
51/// let results = ZmqQueue::new(ZmqEndpoint::connect("tcp://sink:5556")).bindable();
52/// let to_results = results.bind(Publish);
53/// RustStream::new(AppInfo::new("worker", "0.1.0"))
54/// .with_broker(ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555")), |b| {
55/// b.include(handle).out_reply(to_results);
56/// })
57/// .with_broker(results, |_b| {})
58/// }
59/// ```
60pub mod prelude {
61 pub use ruststream::prelude::*;
62
63 pub use crate::endpoint::ZmqEndpoint;
64
65 // `Publish` is the mount-site vocabulary, and it is why this glob belongs in a routes file
66 // rather than a handler one: a handler imports the framework prelude alone and bounds its
67 // injected publisher with a broker capability trait, so the two names never meet.
68 pub use super::{Publish, ZmqQueue};
69}
70
71use std::fmt;
72use std::future::{Future, ready};
73use std::marker::PhantomData;
74use std::num::NonZeroUsize;
75use std::sync::Arc;
76use std::sync::atomic::Ordering;
77
78use futures::Stream;
79use futures::lock::Mutex;
80#[cfg(feature = "asyncapi")]
81use ruststream::asyncapi::Bindings;
82use ruststream::{
83 AddressedCopies, BatchSubscriber, Broker, BufferedSubscriber, BytesMut, ConnectedBroker,
84 DefaultPublish, DescribeServer, NamedCopies, OutgoingMessage, PairError, PublishPolicy,
85 Publisher, ServerSpec, Subscribe, Subscriber, Take,
86};
87use tokio::sync::OnceCell;
88use zeromq::prelude::*;
89use zeromq::{PullSocket, PushSocket};
90
91#[cfg(feature = "asyncapi")]
92use crate::bindings::{self, SocketPair};
93use crate::common::{
94 BATCH_MAX_WAIT, DEFAULT_READ_AHEAD, DeliveryReceiver, DriverHandle, Lifecycle, Outbox,
95 OwnListener, Sender, SharedLifecycle, WireSubscriber, delivery_channel, dials_the_sender,
96 release, returns_to_subscription,
97};
98use crate::endpoint::{Bind, Connect, Endpoint, EndpointRole, ZmqEndpoint};
99use crate::error::ZmqError;
100#[cfg(feature = "testing")]
101use crate::in_process::Pick;
102use crate::message::ZmqMessage;
103use crate::wire;
104
105// A production publisher holds the outbox of the socket it attached and nothing beside it.
106#[cfg(not(feature = "testing"))]
107const _: () = assert!(size_of::<Sender<Outbox<PushSocket>>>() == size_of::<Outbox<PushSocket>>());
108
109/// The PUSH/PULL queue: each message reaches one of the competing consumers.
110///
111/// `Role` is the side of the endpoint this process takes, fixed by the endpoint's constructor:
112/// [`Bind`] for `ZmqEndpoint::bind`, [`Connect`] for `ZmqEndpoint::connect`. It decides where a
113/// retry copy goes. A subscription that binds takes a copy published to its own listener, so a
114/// mount site names no destination; one that dials reads from a PUSH peer that takes nothing, so
115/// a mount site names where its copies go.
116///
117/// # Examples
118///
119/// A worker that binds its queue retries on it. The copy goes back into the queue, whichever
120/// worker is free takes it, and the framework's retry-count header caps the deliveries at five:
121///
122/// ```
123/// use std::time::Duration;
124///
125/// use ruststream_zeromq::queue::prelude::*;
126/// use serde::Deserialize;
127///
128/// #[derive(Deserialize)]
129/// struct Job {
130/// id: u64,
131/// }
132///
133/// #[subscriber("jobs")]
134/// async fn handle(job: &Job) -> HandlerOutcome {
135/// if job.id == 0 {
136/// return HandlerOutcome::retry_after(Duration::from_secs(30));
137/// }
138/// HandlerOutcome::ack()
139/// }
140///
141/// #[ruststream::app]
142/// fn app() -> impl App {
143/// RustStream::new(AppInfo::new("worker", "0.1.0")).with_broker(
144/// ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555")),
145/// |b| {
146/// b.include(handle)
147/// .max_attempts(nonzero!(5u32))
148/// .out_retry(Publish);
149/// },
150/// )
151/// }
152/// ```
153///
154/// One of several workers that dial the same ventilator and compete for its jobs. The ventilator
155/// takes nothing back, so the retry copies go to a queue the worker binds itself, where the same
156/// handler takes them. The copies carry the framework's counter, so the cap holds across both
157/// subscriptions:
158///
159/// ```
160/// use std::time::Duration;
161///
162/// use ruststream_zeromq::queue::prelude::*;
163/// use serde::Deserialize;
164///
165/// #[derive(Deserialize)]
166/// struct Job {
167/// id: u64,
168/// }
169///
170/// #[subscriber("jobs")]
171/// async fn work(job: &Job) -> HandlerOutcome {
172/// if job.id == 0 {
173/// return HandlerOutcome::retry_after(Duration::from_secs(30));
174/// }
175/// HandlerOutcome::ack()
176/// }
177///
178/// #[ruststream::app]
179/// fn app() -> impl App {
180/// let retries = ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5560")).bindable();
181/// let copies = retries.bind(Publish);
182/// RustStream::new(AppInfo::new("worker", "0.1.0"))
183/// .with_broker(ZmqQueue::new(ZmqEndpoint::connect("tcp://ventilator:5555")), |b| {
184/// b.include(work)
185/// .max_attempts(nonzero!(5u32))
186/// .out_retry(copies)
187/// .to("jobs");
188/// })
189/// .with_broker(retries, |b| {
190/// b.include(work).max_attempts(nonzero!(5u32));
191/// })
192/// }
193/// ```
194#[must_use]
195pub struct ZmqQueue<Role = Bind> {
196 endpoint: Endpoint,
197 read_ahead: NonZeroUsize,
198 cell: Arc<OnceCell<SharedLifecycle>>,
199 role: PhantomData<fn() -> Role>,
200}
201
202impl<Role: EndpointRole> ZmqQueue<Role> {
203 /// Records the endpoint. No I/O.
204 ///
205 /// # Examples
206 ///
207 /// A worker that listens for the jobs its producers push:
208 ///
209 /// ```
210 /// use ruststream_zeromq::queue::prelude::*;
211 /// use serde::Deserialize;
212 ///
213 /// #[derive(Deserialize)]
214 /// struct Job {
215 /// id: u64,
216 /// }
217 ///
218 /// #[subscriber("jobs")]
219 /// async fn work(job: &Job) -> HandlerOutcome {
220 /// tracing::info!(job.id, "working on the job");
221 /// HandlerOutcome::ack()
222 /// }
223 ///
224 /// #[ruststream::app]
225 /// fn app() -> impl App {
226 /// RustStream::new(AppInfo::new("worker", "0.1.0")).with_broker(
227 /// ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555")),
228 /// |b| {
229 /// b.include(work);
230 /// },
231 /// )
232 /// }
233 /// ```
234 pub fn new(endpoint: ZmqEndpoint<Role>) -> Self {
235 Self {
236 endpoint: endpoint.into_inner(),
237 read_ahead: DEFAULT_READ_AHEAD,
238 cell: Arc::new(OnceCell::new()),
239 role: PhantomData,
240 }
241 }
242}
243
244impl<Role> ZmqQueue<Role> {
245 /// How many deliveries a subscription reads off its socket ahead of the handler; 1000 unless
246 /// set, the receive high-water mark `ZeroMQ` itself gives a socket.
247 ///
248 /// Past this bound the subscription stops reading, the socket's buffers fill, and the sender
249 /// waits: a handler slower than the wire slows the sender down rather than growing this
250 /// process's memory.
251 ///
252 /// # Examples
253 ///
254 /// A worker whose renders take seconds holds 64 jobs ahead, and the producers wait beyond
255 /// that:
256 ///
257 /// ```
258 /// use ruststream_zeromq::queue::prelude::*;
259 /// use serde::Deserialize;
260 ///
261 /// #[derive(Deserialize)]
262 /// struct Render {
263 /// scene: String,
264 /// }
265 ///
266 /// #[subscriber("renders")]
267 /// async fn render(job: &Render) -> HandlerOutcome {
268 /// tracing::info!(scene = %job.scene, "rendering");
269 /// HandlerOutcome::ack()
270 /// }
271 ///
272 /// #[ruststream::app]
273 /// fn app() -> impl App {
274 /// let renders = ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555"))
275 /// .read_ahead(nonzero!(64_usize));
276 /// RustStream::new(AppInfo::new("renderer", "0.1.0")).with_broker(renders, |b| {
277 /// b.include(render);
278 /// })
279 /// }
280 /// ```
281 pub const fn read_ahead(mut self, read_ahead: NonZeroUsize) -> Self {
282 self.read_ahead = read_ahead;
283 self
284 }
285
286 /// A publisher sharing this queue's state; buildable before `connect`.
287 #[must_use]
288 pub fn publisher(&self) -> ZmqQueuePublisher {
289 ZmqQueuePublisher {
290 cell: Arc::clone(&self.cell),
291 push: Arc::new(Mutex::new(None)),
292 }
293 }
294}
295
296// Written out rather than derived: `Role` is a type-level tag, and a derive would demand of it
297// what is only asked of the fields.
298impl<Role> Clone for ZmqQueue<Role> {
299 fn clone(&self) -> Self {
300 Self {
301 endpoint: self.endpoint.clone(),
302 read_ahead: self.read_ahead,
303 cell: Arc::clone(&self.cell),
304 role: PhantomData,
305 }
306 }
307}
308
309impl<Role> fmt::Debug for ZmqQueue<Role> {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 f.debug_struct("ZmqQueue")
312 .field("endpoint", &self.endpoint)
313 .finish_non_exhaustive()
314 }
315}
316
317impl<Role: EndpointRole> Broker for ZmqQueue<Role> {
318 type Error = ZmqError;
319 type Connected = ConnectedZmqQueue<Role>;
320
321 async fn connect(self) -> Result<Self::Connected, Self::Error> {
322 self.connect_with(Lifecycle::new).await
323 }
324}
325
326impl<Role: EndpointRole> ZmqQueue<Role> {
327 /// The connect transition over the transport `lifecycle` builds: the sockets, or the
328 /// in-process transport the test harness connects instead.
329 pub(crate) async fn connect_with(
330 self,
331 lifecycle: fn(Endpoint) -> Lifecycle,
332 ) -> Result<ConnectedZmqQueue<Role>, ZmqError> {
333 let lifecycle = self
334 .cell
335 .get_or_try_init(async || {
336 self.endpoint.validate()?;
337 Ok::<_, ZmqError>(Arc::new(lifecycle(self.endpoint.clone())))
338 })
339 .await?
340 .clone();
341 Ok(ConnectedZmqQueue {
342 lifecycle,
343 read_ahead: self.read_ahead,
344 cell: self.cell,
345 role: PhantomData,
346 })
347 }
348}
349
350impl<Role: EndpointRole> DescribeServer for ZmqQueue<Role> {
351 fn describe_server(&self) -> ServerSpec {
352 self.endpoint.server_spec()
353 }
354}
355
356/// The connected form of [`ZmqQueue`]; sockets attach per subscription and per publisher.
357pub struct ConnectedZmqQueue<Role = Bind> {
358 lifecycle: SharedLifecycle,
359 /// How far this subscription reads ahead, from the broker this form was connected from.
360 read_ahead: NonZeroUsize,
361 cell: Arc<OnceCell<SharedLifecycle>>,
362 role: PhantomData<fn() -> Role>,
363}
364
365impl<Role> fmt::Debug for ConnectedZmqQueue<Role> {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 f.debug_struct("ConnectedZmqQueue")
368 .field("lifecycle", &self.lifecycle)
369 .finish_non_exhaustive()
370 }
371}
372
373impl<Role> ConnectedZmqQueue<Role> {
374 #[cfg(feature = "testing")]
375 pub(crate) fn lifecycle(&self) -> &Lifecycle {
376 &self.lifecycle
377 }
378
379 /// The address a local subscription resolved by binding (useful with an ephemeral
380 /// `tcp://...:0` endpoint); `None` until a subscription has bound.
381 #[must_use]
382 pub fn bound_address(&self) -> Option<String> {
383 self.lifecycle.bound_address()
384 }
385
386 /// A publisher from the connected form.
387 #[must_use]
388 pub fn publisher(&self) -> ZmqQueuePublisher {
389 ZmqQueuePublisher {
390 cell: Arc::clone(&self.cell),
391 push: Arc::new(Mutex::new(None)),
392 }
393 }
394
395 /// The publisher a publish policy pairs: one whose side binds attaches its socket now, rather
396 /// than on its first publish, so a peer that dials the endpoint connects before anything is
397 /// sent. Startup work, once per pairing: the publish path itself is unchanged.
398 async fn paired(&self) -> Result<ZmqQueuePublisher, PairError> {
399 let publisher = self.publisher();
400 if self.lifecycle.attaches_on_pairing() {
401 publisher
402 .attach(&self.lifecycle)
403 .await
404 .map_err(PairError::new)?;
405 }
406 Ok(publisher)
407 }
408
409 /// Opens a PULL subscription on the endpoint, bound or dialed per the side.
410 async fn open(&self, name: &str) -> Result<ZmqSubscriber, ZmqError> {
411 self.lifecycle.ensure_open()?;
412 #[cfg(feature = "testing")]
413 if let Some(bus) = self.lifecycle.in_process_bus() {
414 let slot = self.lifecycle.attach_in_process(name).await?;
415 let (rx, registration) = bus.subscribe(name, wire::read_delivery);
416 return Ok(ZmqSubscriber::from_parts(
417 name.to_owned(),
418 DeliveryReceiver::InProcess(rx),
419 DriverHandle::InProcess(registration, slot),
420 ));
421 }
422 let lifecycle = Arc::clone(&self.lifecycle);
423 let subscription = name.to_owned();
424 let (mut socket, slot) = self
425 .lifecycle
426 .on_runtime(async move {
427 let mut socket = PullSocket::new();
428 let slot = lifecycle
429 .attach_receiver(&mut socket, &subscription)
430 .await?;
431 Ok((socket, slot))
432 })
433 .await?;
434
435 let (tx, rx) = delivery_channel(self.read_ahead);
436 let task = self.lifecycle.spawn(async move {
437 loop {
438 match socket.recv().await {
439 Ok(message) => {
440 let Some(item) = wire::read_delivery(message) else {
441 continue;
442 };
443 if tx.send(item).await.is_err() {
444 break;
445 }
446 }
447 Err(err) => {
448 if tx
449 .send(Err(ZmqError::Receive(err.to_string())))
450 .await
451 .is_err()
452 {
453 break;
454 }
455 }
456 }
457 }
458 });
459 Ok(ZmqSubscriber::from_parts(
460 name.to_owned(),
461 rx,
462 DriverHandle::Task(task, slot),
463 ))
464 }
465}
466
467impl<Role: EndpointRole> ConnectedBroker for ConnectedZmqQueue<Role> {
468 type Error = ZmqError;
469 type Closed = ();
470
471 fn shutdown(self) -> impl Future<Output = Result<(), Self::Error>> {
472 self.lifecycle.closed.store(true, Ordering::Release);
473 ready(Ok(()))
474 }
475}
476
477impl Subscribe for ConnectedZmqQueue<Bind> {
478 type Subscriber = ZmqSubscriber;
479
480 /// The subscribe name is the address: the subscription binds the endpoint, a PUSH socket of
481 /// this service dials that listener, and a publish under the subscription's name reaches it.
482 /// The runtime publishes a retry copy there, and nothing has to be written at the mount site.
483 ///
484 /// This is the whole retry path on `ZeroMQ`. Nothing settles a delivery, so a handler asking
485 /// for `retry_after` is served only by the deferred copy the runtime publishes here.
486 /// Competing consumers still compete for that copy, so the worker that retries is not
487 /// necessarily the one that asked.
488 type Copies = AddressedCopies;
489
490 async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
491 self.open(name).await
492 }
493}
494
495impl Subscribe for ConnectedZmqQueue<Connect> {
496 type Subscriber = ZmqSubscriber;
497
498 /// A subscription that dials reads from the PUSH peer at the endpoint, a ventilator that
499 /// sends and takes nothing, so a copy published back to it is refused by the handshake. The
500 /// subscription therefore addresses no copy, and every registration names where its copies
501 /// go: `.out_retry(policy).to("name")` over a broker that reaches a consumer, or a transform
502 /// that names one per delivery. A registration that names neither is refused before the
503 /// subscription opens, and a `.build()` chain that names neither does not compile.
504 type Copies = NamedCopies;
505
506 async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
507 self.open(name).await
508 }
509}
510
511/// A subscription on one of the one-way patterns - PUSH/PULL or PUB/SUB - yielding
512/// [`ZmqMessage`]s singly or in batches.
513///
514/// A receive on a socket yields one multipart message, so a batch is collected on this side: it
515/// closes once it holds the size the mount named, or 20 ms after its first delivery, whichever
516/// comes first. The 20 ms is a constant of this crate.
517///
518/// # Examples
519///
520/// A handler that takes a slice receives a whole batch, and the mount site caps its size:
521///
522/// ```
523/// use ruststream_zeromq::queue::prelude::*;
524/// use serde::Deserialize;
525///
526/// #[derive(Deserialize)]
527/// struct Job {
528/// id: u64,
529/// }
530///
531/// #[subscriber("jobs")]
532/// async fn drain(jobs: &[Job]) -> HandlerOutcome {
533/// tracing::info!(size = jobs.len(), "working on a batch");
534/// HandlerOutcome::ack()
535/// }
536///
537/// #[ruststream::app]
538/// fn app() -> impl App {
539/// RustStream::new(AppInfo::new("worker", "0.1.0")).with_broker(
540/// ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555")),
541/// |b| {
542/// b.include(drain.batch(nonzero!(32)));
543/// },
544/// )
545/// }
546/// ```
547pub struct ZmqSubscriber {
548 name: String,
549 inner: BufferedSubscriber<WireSubscriber>,
550}
551
552impl fmt::Debug for ZmqSubscriber {
553 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554 f.debug_struct("ZmqSubscriber")
555 .field("name", &self.name)
556 .finish_non_exhaustive()
557 }
558}
559
560impl ZmqSubscriber {
561 pub(crate) fn from_parts(name: String, rx: DeliveryReceiver, driver: DriverHandle) -> Self {
562 Self {
563 name,
564 inner: BufferedSubscriber::new(WireSubscriber {
565 rx,
566 _driver: driver,
567 })
568 .max_wait(BATCH_MAX_WAIT),
569 }
570 }
571}
572
573impl Subscriber for ZmqSubscriber {
574 type Message = ZmqMessage;
575 type Error = ZmqError;
576
577 fn stream(&mut self) -> impl Stream<Item = Result<ZmqMessage, ZmqError>> + Send + '_ {
578 self.inner.stream()
579 }
580}
581
582/// The transport has no batches of its own - a receive yields one multipart message - so they are
583/// assembled on the client, to the size the registration named. The deadline that closes a partial
584/// batch is the crate's own (20 ms); the size is not, it arrives per subscription.
585impl BatchSubscriber for ZmqSubscriber {
586 type Batch = Vec<ZmqMessage>;
587
588 fn batches(
589 &mut self,
590 size: NonZeroUsize,
591 ) -> impl Stream<Item = Result<Self::Batch, ZmqError>> + Send + '_ {
592 self.inner.batches(size)
593 }
594}
595
596/// Publishes into the queue over a PUSH socket of its own.
597///
598/// Paired by the runtime on the bind side, the socket listens from the moment the service starts,
599/// so a PULL peer that dials the endpoint connects before the first push. A publisher that dials,
600/// or one taken from a `publisher()` constructor, attaches on its first publish.
601///
602/// On an endpoint a subscription of this service bound, the socket dials that subscription, so a
603/// publish reaches it and nothing else: one under the subscription's own name (a retry copy, a
604/// job the service feeds itself) is sent, and one under any other name returns
605/// [`ZmqError::Send`] rather than arriving at the subscription as its next delivery. On an
606/// endpoint a subscription of this service dials, the peer there only pushes, so every publish
607/// returns [`ZmqError::Send`] naming that subscription.
608///
609/// A publisher that sends before any subscription of its service binds the endpoint listens on
610/// the endpoint itself, for peers to dial. When a subscription of the same broker binds the
611/// endpoint later, the publisher gives the endpoint up first, on a fixed port and on port zero
612/// alike, and its next publish dials that subscription.
613///
614/// # Examples
615///
616/// A worker's results leave through a queue on an endpoint of their own, here one a sink binds.
617/// The jobs arrive on the socket the `jobs` subscription binds, and that socket takes every
618/// message pushed into it: a result published there would come back as the next job, so the
619/// worker's own publisher refuses it, and the mount binds the reply to the second broker:
620///
621/// ```
622/// use ruststream_zeromq::queue::prelude::*;
623/// use serde::{Deserialize, Serialize};
624///
625/// #[derive(Deserialize)]
626/// struct Job {
627/// id: u64,
628/// }
629///
630/// #[derive(Serialize, Outgoing)]
631/// #[outgoing(name = "results")]
632/// struct Done {
633/// id: u64,
634/// }
635///
636/// #[subscriber("jobs", reply)]
637/// async fn work(job: &Job) -> Done {
638/// Done { id: job.id }
639/// }
640///
641/// #[ruststream::app]
642/// fn app() -> impl App {
643/// let results = ZmqQueue::new(ZmqEndpoint::connect("tcp://sink:5556")).bindable();
644/// let to_results = results.bind(Publish);
645/// RustStream::new(AppInfo::new("worker", "0.1.0"))
646/// .with_broker(ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555")), |b| {
647/// b.include(work).out_reply(to_results);
648/// })
649/// .with_broker(results, |_b| {})
650/// }
651/// ```
652#[derive(Clone)]
653pub struct ZmqQueuePublisher {
654 cell: Arc<OnceCell<SharedLifecycle>>,
655 push: Arc<Mutex<Option<Attached>>>,
656}
657
658/// A PUSH socket, and the subscription of this service it reaches when it dialed one.
659pub(crate) struct Attached {
660 socket: Sender<Outbox<PushSocket>>,
661 /// Set when a subscription of this service bound the endpoint and the socket dialed it: that
662 /// subscription receives every message sent, so only its own name may be sent.
663 local: Option<String>,
664 /// Set when the socket dialed a peer at the endpoint. The client does not dial a PUSH socket
665 /// again when that peer goes, so a failed send drops the socket and the next publish dials
666 /// anew.
667 dials: bool,
668}
669
670impl Attached {
671 /// Attaches a PUSH socket per the endpoint's side, or its in-process counterpart.
672 async fn attach(lifecycle: &SharedLifecycle) -> Result<Self, ZmqError> {
673 #[cfg(feature = "testing")]
674 if let Some(bus) = lifecycle.in_process_bus() {
675 let local = lifecycle
676 .local_listener()
677 .map(|listener| listener.subscription.clone());
678 return Ok(Self {
679 socket: Sender::InProcess {
680 bus: Arc::clone(bus),
681 local: local.is_some(),
682 },
683 local,
684 dials: false,
685 });
686 }
687 let attaching = Arc::clone(lifecycle);
688 let (socket, local) = lifecycle
689 .on_runtime(async move {
690 let mut socket = PushSocket::new();
691 let local = attaching
692 .attach_sender(&mut socket)
693 .await?
694 .map(|listener| listener.subscription.clone());
695 Ok((socket, local))
696 })
697 .await?;
698 Ok(Self {
699 socket: Sender::Socket(Outbox::new(socket)),
700 local,
701 dials: lifecycle.dials(),
702 })
703 }
704
705 /// Closes the listener of a socket that listens on the endpoint itself, so a subscription of
706 /// this service can bind it.
707 pub(crate) async fn give_way(self) {
708 match self.socket {
709 Sender::Socket(outbox) => {
710 if let Some(mut socket) = outbox.into_socket().await {
711 release(&mut socket).await;
712 }
713 }
714 #[cfg(feature = "testing")]
715 Sender::InProcess { .. } => {}
716 }
717 }
718
719 async fn send(&mut self, name: &str, frames: zeromq::ZmqMessage) -> Result<(), ZmqError> {
720 match &mut self.socket {
721 Sender::Socket(outbox) => outbox.send(name, frames).await,
722 // The subscription that bound the endpoint takes whatever is pushed into it.
723 #[cfg(feature = "testing")]
724 Sender::InProcess { bus, local } => {
725 let pick = if *local { Pick::All } else { Pick::Nobody };
726 bus.send(name, frames, None, pick);
727 Ok(())
728 }
729 }
730 }
731}
732
733impl ZmqQueuePublisher {
734 /// Attaches the socket under the lifecycle's attach lock. One that listens on the endpoint
735 /// itself is recorded there, for a subscription of this service that binds it later to make it
736 /// give way.
737 async fn attach(&self, lifecycle: &SharedLifecycle) -> Result<(), ZmqError> {
738 // Attaches run one at a time under this lock, so the slot found empty stays empty until
739 // the socket is installed, without holding the slot across the attach.
740 let _attaching = lifecycle.attaching().await;
741 if self.push.lock().await.is_some() {
742 return Ok(());
743 }
744 let attached = Attached::attach(lifecycle).await?;
745 if lifecycle.listens_here(attached.local.is_some()) {
746 lifecycle.listens_alongside(OwnListener::Queue(Arc::downgrade(&self.push)));
747 }
748 *self.push.lock().await = Some(attached);
749 Ok(())
750 }
751}
752
753impl fmt::Debug for ZmqQueuePublisher {
754 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
755 f.debug_struct("ZmqQueuePublisher").finish_non_exhaustive()
756 }
757}
758
759impl Publisher for ZmqQueuePublisher {
760 /// A frame owns its bytes: the payload becomes the message's third frame and the socket
761 /// keeps it until the send completes.
762 type Payload = Take;
763
764 type Error = ZmqError;
765
766 /// ZMTP carries no per-message setting: a send takes the frames and nothing else, so there is
767 /// nothing for a call site to adjust. The
768 /// [Publishing](https://powersemmi.github.io/ruststream-zeromq/latest/components/publishing/)
769 /// page shows what a publish carries.
770 type Options = ();
771
772 /// # Cancel safety
773 ///
774 /// Cancel-safe. A publish dropped before the socket takes its message sends nothing. One
775 /// dropped mid-send, while the peer applies back-pressure, leaves its message with the
776 /// publisher: the next publish through it or a clone completes that send before its own, and
777 /// a failure of it is logged against its name.
778 // The socket guard intentionally spans the lazy attach and the send: the socket takes
779 // &mut for every operation.
780 #[allow(clippy::significant_drop_tightening)]
781 async fn publish(
782 &self,
783 msg: OutgoingMessage<'_, BytesMut>,
784 _options: Option<&Self::Options>,
785 ) -> Result<(), Self::Error> {
786 let lifecycle = self.cell.get().ok_or(ZmqError::NotConnected)?;
787 lifecycle.ensure_open()?;
788 // Framed before the socket is touched: a message that cannot be written costs no attach.
789 let (name, payload, headers) = msg.into_parts();
790 let frames = wire::encode_to(name, name, &headers, payload.freeze())?;
791 // Checked here rather than by the type: whether one broker both subscribes and publishes is
792 // decided by the scopes a service mounts, and the peer at the far end is the deployment's,
793 // so the refusal comes before the handshake would give it. Checked on every publish, not
794 // only on the attach: a subscription that dials after the socket attached turns the
795 // publisher's peer into its own. One load of a set-once cell.
796 if let Some(subscription) = lifecycle.dialer() {
797 return Err(dials_the_sender(name, subscription, "PUSH", "queue"));
798 }
799 let mut push = self.push.lock().await;
800 // A loop, because a subscription of this service that binds the endpoint may take back a
801 // socket that listened there between the attach and this lock; the next attach dials it.
802 while push.is_none() {
803 drop(push);
804 self.attach(lifecycle).await?;
805 push = self.push.lock().await;
806 }
807 let attached = push.as_mut().expect("the loop leaves an attached socket");
808 // Checked per message because the name is per message: a slot names it at the call site
809 // and a transform may rewrite it per delivery, while whether a subscription of this
810 // service holds the endpoint is deployment configuration read once one has bound. Nothing
811 // before the first publish sees both. A socket that dials out or listens carries no name
812 // and skips it.
813 if let Some(subscription) = attached.local.as_deref()
814 && subscription != name
815 {
816 return Err(returns_to_subscription(name, subscription));
817 }
818 let dials = attached.dials;
819 let sent = attached.send(name, frames).await;
820 // A send fails on a dialed socket when its peer is gone: the connection broke, or no
821 // peer is left. The client keeps the socket peerless for good, so it goes, and the next
822 // publish dials the address again and reaches the peer once it is back. Only the failure
823 // path pays for it.
824 if sent.is_err() && dials {
825 *push = None;
826 }
827 sent
828 }
829}
830
831/// The publish policy for [`ZmqQueuePublisher`].
832///
833/// # Examples
834///
835/// A handler that splits each order into picks and pushes them to the pickers' queue, bound at the
836/// mount site:
837///
838/// ```
839/// use std::error::Error;
840///
841/// use ruststream_zeromq::prelude::*;
842/// use serde::{Deserialize, Serialize};
843///
844/// #[derive(Deserialize)]
845/// struct Order {
846/// id: u64,
847/// lines: Vec<u64>,
848/// }
849///
850/// #[derive(Serialize, Outgoing)]
851/// #[outgoing(name = "picks")]
852/// struct Pick {
853/// order: u64,
854/// item: u64,
855/// }
856///
857/// #[subscriber("orders")]
858/// async fn split(
859/// order: &Order,
860/// Out(picks): Out<impl Publisher>,
861/// ) -> Result<(), Box<dyn Error + Send + Sync>> {
862/// for &item in &order.lines {
863/// picks.message(&Pick { order: order.id, item }).publish().await?;
864/// }
865/// Ok(())
866/// }
867///
868/// #[ruststream::app]
869/// fn app() -> impl App {
870/// let pickers = ZmqQueue::new(ZmqEndpoint::connect("tcp://pickers:5556")).bindable();
871/// let to_pickers = pickers.bind(ZmqQueuePublish);
872/// RustStream::new(AppInfo::new("splitter", "0.1.0"))
873/// .with_broker(ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555")), |b| {
874/// b.include(split).out(DefaultSlot, to_pickers).build();
875/// })
876/// .register_broker(pickers)
877/// }
878/// ```
879#[derive(Debug, Clone, Copy, Default)]
880#[must_use]
881pub struct ZmqQueuePublish;
882
883impl<Role: EndpointRole> PublishPolicy<ConnectedZmqQueue<Role>> for ZmqQueuePublish {
884 type Live = ZmqQueuePublisher;
885
886 /// On the bind side the publisher's socket attaches here, so it listens once the service has
887 /// started; a dialing publisher attaches on its first publish.
888 ///
889 /// # Errors
890 ///
891 /// Returns [`PairError`] when the socket of a binding publisher cannot take the endpoint.
892 fn pair(
893 self,
894 connected: &ConnectedZmqQueue<Role>,
895 ) -> impl Future<Output = Result<Self::Live, PairError>> {
896 connected.paired()
897 }
898
899 /// The destination reaches the binding, because on this pattern it is also what frame 0 of
900 /// every message on the channel holds.
901 #[cfg(feature = "asyncapi")]
902 fn channel_bindings(&self, channel: &str) -> Bindings {
903 bindings::channel(SocketPair::PushPull, channel)
904 }
905}
906
907impl<Role: EndpointRole> DefaultPublish for ConnectedZmqQueue<Role> {
908 type Policy = ZmqQueuePublish;
909}
910
911#[cfg(test)]
912mod tests {
913 use ruststream::nonzero;
914
915 use super::*;
916
917 #[tokio::test]
918 async fn clones_sharing_a_lifecycle_keep_their_own_read_ahead() {
919 let wide = ZmqQueue::new(ZmqEndpoint::bind("tcp://127.0.0.1:0"));
920 let narrow = wide.clone().read_ahead(nonzero!(4_usize));
921 let wide = wide.connect().await.expect("connects");
922 let narrow = narrow.connect().await.expect("connects");
923
924 assert!(
925 Arc::ptr_eq(&wide.lifecycle, &narrow.lifecycle),
926 "one lifecycle"
927 );
928 assert_eq!(wide.read_ahead, DEFAULT_READ_AHEAD);
929 assert_eq!(narrow.read_ahead, nonzero!(4_usize));
930 }
931}