Skip to main content

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}