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}