Skip to main content

ruststream_zeromq/
queue.rs

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