Skip to main content

ruststream_zeromq/
fanout.rs

1//! [`ZmqFanout`]: the PUB/SUB pattern - broadcast with prefix filtering by name.
2//!
3//! Honest scope, straight from the protocol: a subscriber that connects after a publisher has
4//! started misses what was sent before it arrived (the slow joiner), and a message published
5//! with no matching subscriber is dropped silently. A publisher that reaches a subscription of
6//! its own service reads that subscription's filter before its first send, so it loses nothing
7//! to the slow joiner.
8
9/// The publish policy of this form, under the name a mount site writes.
10pub use self::ZmqFanoutPublish as Publish;
11
12/// The imports a routes file on the PUB/SUB fan-out writes, in one glob: the framework's prelude,
13/// the shared [`ZmqEndpoint`], the descriptor [`ZmqFanout`], and its publish policy as
14/// [`Publish`].
15///
16/// # Examples
17///
18/// ```
19/// use ruststream_zeromq::fanout::prelude::*;
20/// use serde::Deserialize;
21///
22/// #[derive(Deserialize)]
23/// struct Event {
24///     id: u64,
25/// }
26///
27/// #[subscriber("events")]
28/// async fn handle(event: &Event) -> HandlerOutcome {
29///     println!("event {}", event.id);
30///     HandlerOutcome::ack()
31/// }
32///
33/// #[ruststream::app]
34/// fn app() -> impl App {
35///     // The watcher binds and the publishers dial it, so a retry copy returns to its listener.
36///     RustStream::new(AppInfo::new("watcher", "0.1.0")).with_broker(
37///         ZmqFanout::new(ZmqEndpoint::bind("tcp://0.0.0.0:5556")),
38///         |b| {
39///             b.include(handle);
40///         },
41///     )
42/// }
43/// ```
44pub mod prelude {
45    pub use ruststream::prelude::*;
46
47    pub use crate::endpoint::ZmqEndpoint;
48
49    // `Publish` is the mount-site vocabulary, and it is why this glob belongs in a routes file
50    // rather than a handler one: a handler imports the framework prelude alone and bounds its
51    // injected publisher with a broker capability trait, so the two names never meet.
52    pub use super::{Publish, ZmqFanout};
53}
54
55use std::fmt;
56use std::future::{Future, ready};
57use std::marker::PhantomData;
58use std::num::NonZeroUsize;
59use std::sync::Arc;
60use std::sync::atomic::Ordering;
61
62use futures::lock::Mutex;
63#[cfg(feature = "asyncapi")]
64use ruststream::asyncapi::Bindings;
65use ruststream::{
66    AddressedCopies, Broker, BytesMut, ConnectedBroker, DefaultPublish, DescribeServer,
67    NamedCopies, OutgoingMessage, PairError, PublishPolicy, Publisher, ServerSpec, Subscribe, Take,
68};
69use tokio::sync::OnceCell;
70use tokio::time::timeout;
71use zeromq::prelude::*;
72use zeromq::{PubSocket, SubSocket, XPubSocket, ZmqError as WireError};
73
74#[cfg(feature = "asyncapi")]
75use crate::bindings::{self, SocketPair};
76#[cfg(feature = "testing")]
77use crate::common::DeliveryReceiver;
78use crate::common::{
79    DEFAULT_READ_AHEAD, DriverHandle, Lifecycle, SEND_RETRY_WINDOW, SendTarget, Sender,
80    SharedLifecycle, attach_to, delivery_channel, dials_the_sender, send_with_retry,
81};
82use crate::endpoint::{Bind, Connect, Endpoint, EndpointRole, ZmqEndpoint};
83use crate::error::{ZmqError, box_err};
84#[cfg(feature = "testing")]
85use crate::in_process::Pick;
86use crate::queue::ZmqSubscriber;
87use crate::wire;
88
89// A production publisher holds the socket it attached and nothing beside it.
90#[cfg(not(feature = "testing"))]
91const _: () = assert!(size_of::<Sender<PubSide>>() == size_of::<PubSide>());
92
93/// The PUB/SUB fan-out: each message reaches every subscriber whose name prefix matches.
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 this service publishes to its own
98/// listener, so a mount site names no destination; one that dials reads from a PUB peer that
99/// takes nothing, so a mount site names where its copies go.
100///
101/// # Examples
102///
103/// A watcher that the order services dial, taking every message under `orders`, `orders.created`
104/// and `orders.cancelled` included:
105///
106/// ```
107/// use ruststream_zeromq::fanout::prelude::*;
108/// use serde::Deserialize;
109///
110/// #[derive(Deserialize)]
111/// struct OrderEvent {
112///     id: u64,
113/// }
114///
115/// #[subscriber("orders")]
116/// async fn watch(event: &OrderEvent, ctx: &mut Context<'_>) -> HandlerOutcome {
117///     println!("{} for order {}", ctx.name(), event.id);
118///     HandlerOutcome::ack()
119/// }
120///
121/// #[ruststream::app]
122/// fn app() -> impl App {
123///     RustStream::new(AppInfo::new("watcher", "0.1.0")).with_broker(
124///         ZmqFanout::new(ZmqEndpoint::bind("tcp://0.0.0.0:5556")),
125///         |b| {
126///             b.include(watch);
127///         },
128///     )
129/// }
130/// ```
131#[must_use]
132pub struct ZmqFanout<Role = Bind> {
133    endpoint: Endpoint,
134    read_ahead: NonZeroUsize,
135    cell: Arc<OnceCell<SharedLifecycle>>,
136    role: PhantomData<fn() -> Role>,
137}
138
139impl<Role: EndpointRole> ZmqFanout<Role> {
140    /// Records the endpoint. No I/O.
141    ///
142    /// # Examples
143    ///
144    /// A price feed that broadcasts a tick for each trade to every watcher that dials it:
145    ///
146    /// ```
147    /// use ruststream_zeromq::prelude::*;
148    /// use serde::{Deserialize, Serialize};
149    ///
150    /// #[derive(Deserialize)]
151    /// struct Trade {
152    ///     price: f64,
153    /// }
154    ///
155    /// #[derive(Serialize, Outgoing)]
156    /// #[outgoing(name = "prices")]
157    /// struct Tick {
158    ///     price: f64,
159    /// }
160    ///
161    /// #[subscriber("trades", reply)]
162    /// async fn tick(trade: &Trade) -> Tick {
163    ///     Tick { price: trade.price }
164    /// }
165    ///
166    /// #[ruststream::app]
167    /// fn app() -> impl App {
168    ///     let prices = ZmqFanout::new(ZmqEndpoint::bind("tcp://0.0.0.0:5556")).bindable();
169    ///     let to_prices = prices.bind(ZmqFanoutPublish);
170    ///     RustStream::new(AppInfo::new("prices", "0.1.0"))
171    ///         .with_broker(ZmqQueue::new(ZmqEndpoint::bind("tcp://0.0.0.0:5555")), |b| {
172    ///             b.include(tick).out_reply(to_prices);
173    ///         })
174    ///         .register_broker(prices)
175    /// }
176    /// ```
177    pub fn new(endpoint: ZmqEndpoint<Role>) -> Self {
178        Self {
179            endpoint: endpoint.into_inner(),
180            read_ahead: DEFAULT_READ_AHEAD,
181            cell: Arc::new(OnceCell::new()),
182            role: PhantomData,
183        }
184    }
185}
186
187impl<Role> ZmqFanout<Role> {
188    /// How many deliveries a subscription reads off its socket ahead of the handler; 1000 unless
189    /// set, the receive high-water mark `ZeroMQ` itself gives a socket.
190    ///
191    /// Past this bound the subscription stops reading, the socket's buffers fill, and the sender
192    /// waits: a handler slower than the wire slows the sender down rather than growing this
193    /// process's memory.
194    ///
195    /// On PUB/SUB the sender that is held back is the PUB socket, and it waits for its slowest
196    /// matching subscriber: the `zeromq` implementation writes a message to each matching peer in
197    /// turn.
198    ///
199    /// # Examples
200    ///
201    /// An archiver that the event publishers dial, and that writes every event to slow storage,
202    /// holds 64 events ahead, and the publishers wait beyond that:
203    ///
204    /// ```
205    /// use ruststream_zeromq::fanout::prelude::*;
206    /// use serde::Deserialize;
207    ///
208    /// #[derive(Deserialize)]
209    /// struct Event {
210    ///     id: u64,
211    /// }
212    ///
213    /// #[subscriber("events")]
214    /// async fn archive(event: &Event) -> HandlerOutcome {
215    ///     println!("archiving event {}", event.id);
216    ///     HandlerOutcome::ack()
217    /// }
218    ///
219    /// #[ruststream::app]
220    /// fn app() -> impl App {
221    ///     let events = ZmqFanout::new(ZmqEndpoint::bind("tcp://0.0.0.0:5556"))
222    ///         .read_ahead(nonzero!(64_usize));
223    ///     RustStream::new(AppInfo::new("archiver", "0.1.0")).with_broker(events, |b| {
224    ///         b.include(archive);
225    ///     })
226    /// }
227    /// ```
228    pub const fn read_ahead(mut self, read_ahead: NonZeroUsize) -> Self {
229        self.read_ahead = read_ahead;
230        self
231    }
232
233    /// A publisher sharing this fan-out's state; buildable before `connect`.
234    #[must_use]
235    pub fn publisher(&self) -> ZmqFanoutPublisher {
236        ZmqFanoutPublisher {
237            cell: Arc::clone(&self.cell),
238            socket: Arc::new(Mutex::new(None)),
239        }
240    }
241}
242
243// Written out rather than derived: `Role` is a type-level tag, and a derive would demand of it
244// what is only asked of the fields.
245impl<Role> Clone for ZmqFanout<Role> {
246    fn clone(&self) -> Self {
247        Self {
248            endpoint: self.endpoint.clone(),
249            read_ahead: self.read_ahead,
250            cell: Arc::clone(&self.cell),
251            role: PhantomData,
252        }
253    }
254}
255
256impl<Role> fmt::Debug for ZmqFanout<Role> {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        f.debug_struct("ZmqFanout")
259            .field("endpoint", &self.endpoint)
260            .finish_non_exhaustive()
261    }
262}
263
264impl<Role: EndpointRole> Broker for ZmqFanout<Role> {
265    type Error = ZmqError;
266    type Connected = ConnectedZmqFanout<Role>;
267
268    async fn connect(self) -> Result<Self::Connected, Self::Error> {
269        self.connect_with(Lifecycle::new).await
270    }
271}
272
273impl<Role: EndpointRole> ZmqFanout<Role> {
274    /// The connect transition over the transport `lifecycle` builds: the sockets, or the
275    /// in-process transport the test harness connects instead.
276    pub(crate) async fn connect_with(
277        self,
278        lifecycle: fn(Endpoint) -> Lifecycle,
279    ) -> Result<ConnectedZmqFanout<Role>, ZmqError> {
280        let lifecycle = self
281            .cell
282            .get_or_try_init(async || {
283                self.endpoint.validate()?;
284                Ok::<_, ZmqError>(Arc::new(lifecycle(self.endpoint.clone())))
285            })
286            .await?
287            .clone();
288        Ok(ConnectedZmqFanout {
289            lifecycle,
290            read_ahead: self.read_ahead,
291            cell: self.cell,
292            role: PhantomData,
293        })
294    }
295}
296
297impl<Role: EndpointRole> DescribeServer for ZmqFanout<Role> {
298    fn describe_server(&self) -> ServerSpec {
299        self.endpoint.server_spec()
300    }
301}
302
303/// The connected form of [`ZmqFanout`].
304pub struct ConnectedZmqFanout<Role = Bind> {
305    lifecycle: SharedLifecycle,
306    /// How far this subscription reads ahead, from the descriptor this form was connected from.
307    read_ahead: NonZeroUsize,
308    cell: Arc<OnceCell<SharedLifecycle>>,
309    role: PhantomData<fn() -> Role>,
310}
311
312impl<Role> fmt::Debug for ConnectedZmqFanout<Role> {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        f.debug_struct("ConnectedZmqFanout")
315            .field("lifecycle", &self.lifecycle)
316            .finish_non_exhaustive()
317    }
318}
319
320impl<Role> ConnectedZmqFanout<Role> {
321    #[cfg(feature = "testing")]
322    pub(crate) fn lifecycle(&self) -> &Lifecycle {
323        &self.lifecycle
324    }
325
326    /// The address a local subscription resolved by binding (useful with an ephemeral
327    /// `tcp://...:0` endpoint); `None` until a subscription has bound.
328    #[must_use]
329    pub fn bound_address(&self) -> Option<String> {
330        self.lifecycle.bound_address()
331    }
332
333    /// A publisher from the connected form.
334    #[must_use]
335    pub fn publisher(&self) -> ZmqFanoutPublisher {
336        ZmqFanoutPublisher {
337            cell: Arc::clone(&self.cell),
338            socket: Arc::new(Mutex::new(None)),
339        }
340    }
341
342    /// Opens a SUB subscription on the endpoint, bound or dialed per the side, filtering on the
343    /// name as a prefix.
344    async fn open(&self, name: &str) -> Result<ZmqSubscriber, ZmqError> {
345        self.lifecycle.ensure_open()?;
346        // The prefix filter is the transport's in process: it picks the subscriptions whose name
347        // prefixes a message's name frame, as a SUB socket's filter does.
348        #[cfg(feature = "testing")]
349        if let Some(bus) = self.lifecycle.in_process_bus() {
350            let slot = self.lifecycle.attach_in_process(name).await?;
351            let (rx, registration) = bus.subscribe(name, wire::read_delivery);
352            return Ok(ZmqSubscriber::from_parts(
353                name.to_owned(),
354                DeliveryReceiver::InProcess(rx),
355                DriverHandle::InProcess(registration, slot),
356            ));
357        }
358        let lifecycle = Arc::clone(&self.lifecycle);
359        let subscription = name.to_owned();
360        let (mut socket, slot) = self
361            .lifecycle
362            .on_runtime(async move {
363                let mut socket = SubSocket::new();
364                let slot = lifecycle
365                    .attach_receiver(&mut socket, &subscription)
366                    .await?;
367                // The name frame doubles as the subscription prefix; filtering happens on the
368                // publisher side, per the protocol.
369                socket
370                    .subscribe(&subscription)
371                    .await
372                    .map_err(|e| ZmqError::Receive(e.to_string()))?;
373                Ok((socket, slot))
374            })
375            .await?;
376
377        let (tx, rx) = delivery_channel(self.read_ahead);
378        let task = self.lifecycle.spawn(async move {
379            loop {
380                match socket.recv().await {
381                    Ok(message) => {
382                        let Some(item) = wire::read_delivery(message) else {
383                            continue;
384                        };
385                        if tx.send(item).await.is_err() {
386                            break;
387                        }
388                    }
389                    Err(err) => {
390                        if tx
391                            .send(Err(ZmqError::Receive(err.to_string())))
392                            .await
393                            .is_err()
394                        {
395                            break;
396                        }
397                    }
398                }
399            }
400        });
401        Ok(ZmqSubscriber::from_parts(
402            name.to_owned(),
403            rx,
404            DriverHandle::Task(task, slot),
405        ))
406    }
407}
408
409impl<Role: EndpointRole> ConnectedBroker for ConnectedZmqFanout<Role> {
410    type Error = ZmqError;
411    type Closed = ();
412
413    fn shutdown(self) -> impl Future<Output = Result<(), Self::Error>> {
414        self.lifecycle.closed.store(true, Ordering::Release);
415        ready(Ok(()))
416    }
417}
418
419impl Subscribe for ConnectedZmqFanout<Bind> {
420    type Subscriber = ZmqSubscriber;
421
422    /// The subscribe name is the address, because the two ends of this pattern are the same
423    /// broker: the subscription binds the endpoint, the PUB socket a registration publishes
424    /// through dials that listener, and a name is a prefix of itself, so a publish by this
425    /// process reaches its own subscriber. That is what a retry copy needs.
426    ///
427    /// The pattern's own scope applies to the copy as it does to any other message: every
428    /// subscription whose prefix matches receives it. The publisher reads the subscription's
429    /// filter before its first send, so the first copy it publishes is not dropped.
430    type Copies = AddressedCopies;
431
432    async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
433        self.open(name).await
434    }
435}
436
437impl Subscribe for ConnectedZmqFanout<Connect> {
438    type Subscriber = ZmqSubscriber;
439
440    /// A subscription that dials reads from the PUB peer at the endpoint, which publishes and
441    /// takes nothing, so a copy published back to it is refused by the handshake. The
442    /// subscription therefore addresses no copy, and every registration names where its copies
443    /// go: `.out_retry(policy).to("name")` over a broker that reaches a consumer, or a transform
444    /// that names one per delivery. A registration that names neither is refused before the
445    /// subscription opens, and a `.build()` chain that names neither does not compile.
446    type Copies = NamedCopies;
447
448    async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
449        self.open(name).await
450    }
451}
452
453/// Publishes to the fan-out over a lazily attached PUB socket.
454///
455/// A message with no matching subscriber is dropped silently - that is the pattern's
456/// contract, not an error. On an endpoint a subscription of this service dials, the peer there
457/// only publishes, so every publish returns [`ZmqError::Send`] naming that subscription.
458#[derive(Clone)]
459pub struct ZmqFanoutPublisher {
460    cell: Arc<OnceCell<SharedLifecycle>>,
461    // The socket guard is the `futures` mutex rather than tokio's: the socket needs `&mut` per send,
462    // so something must serialise, and this one's uncontended lock and unlock are a pair of atomics
463    // where tokio's semaphore also takes its waiter list. It costs 1.8 of the 9 points a publish
464    // spends over a raw socket loop (#28).
465    socket: Arc<Mutex<Option<Sender<PubSide>>>>,
466}
467
468/// The sending socket of a fan-out publisher, by the place it attached to.
469enum PubSide {
470    /// A peer outside this service: the socket learns its filters as they arrive, and what it
471    /// sends before one arrives is dropped, as the pattern drops it.
472    Remote(PubSocket),
473    /// The listener a subscription of this service bound. The socket read that subscription's
474    /// filter before its first send, so nothing it sends is dropped for want of one.
475    Loopback(XPubSocket),
476}
477
478impl fmt::Debug for ZmqFanoutPublisher {
479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480        f.debug_struct("ZmqFanoutPublisher").finish_non_exhaustive()
481    }
482}
483
484impl Publisher for ZmqFanoutPublisher {
485    /// A frame owns its bytes: the payload becomes the message's third frame and the socket
486    /// keeps it until the send completes.
487    type Payload = Take;
488
489    type Error = ZmqError;
490
491    /// ZMTP carries no per-message setting: a send takes the frames and nothing else, so there is
492    /// nothing for a call site to adjust. See the [crate documentation](crate#per-message-settings).
493    type Options = ();
494
495    /// # Cancel safety
496    ///
497    /// Not cancel-safe. Dropping the future can leave the message half-handed to the socket: the
498    /// attach and the send share one guard, and a send that has begun is not undone. Publish from
499    /// a task of its own rather than inside a `select!` arm.
500    // The socket guard intentionally spans the lazy attach and the send: the socket takes
501    // &mut for every operation.
502    #[allow(clippy::significant_drop_tightening)]
503    async fn publish(
504        &self,
505        msg: OutgoingMessage<'_, BytesMut>,
506        _options: Option<&Self::Options>,
507    ) -> Result<(), Self::Error> {
508        let lifecycle = self.cell.get().ok_or(ZmqError::NotConnected)?;
509        lifecycle.ensure_open()?;
510        // Framed before the socket is touched: a message that cannot be written costs no attach.
511        let (name, payload, headers) = msg.into_parts();
512        let frames = wire::encode_to(name, name, &headers, payload.freeze())?;
513        // Checked here rather than by the type: whether one broker both subscribes and publishes is
514        // decided by the scopes a service mounts, and the peer at the far end is the deployment's,
515        // so the refusal comes before the handshake would give it. Checked on every publish, not
516        // only on the attach: a subscription that dials after the socket attached turns the
517        // publisher's peer into its own. One load of a set-once cell.
518        if let Some(subscription) = lifecycle.dialer() {
519            return Err(dials_the_sender(name, subscription, "PUB", "fan-out"));
520        }
521        let mut guard = self.socket.lock().await;
522        if guard.is_none() {
523            *guard = Some(attach(lifecycle).await?);
524        }
525        match guard.as_mut().expect("just attached") {
526            // PUB never reports "no peers": an unmatched message is dropped by design, so the
527            // retry helper only smooths transport-level failures.
528            Sender::Socket(PubSide::Remote(socket)) => send_with_retry(socket, name, frames).await,
529            Sender::Socket(PubSide::Loopback(socket)) => {
530                send_with_retry(socket, name, frames).await
531            }
532            #[cfg(feature = "testing")]
533            Sender::InProcess { bus, local } => {
534                let pick = if *local {
535                    Pick::Prefix(name)
536                } else {
537                    Pick::Nobody
538                };
539                bus.send(name, frames, None, pick);
540                Ok(())
541            }
542        }
543    }
544}
545
546/// Attaches a PUB socket per the endpoint's side, or its in-process counterpart.
547///
548/// On the listener a subscription of this service bound, the socket is an XPUB that reads the
549/// subscription's filter before it returns: a PUB socket drops a message until the filter of the
550/// peer it dialed has arrived, and on the loopback that peer is the subscription a retry copy is
551/// published to. The wait is paid once per publisher, at the attach.
552async fn attach(lifecycle: &SharedLifecycle) -> Result<Sender<PubSide>, ZmqError> {
553    #[cfg(feature = "testing")]
554    if let Some(bus) = lifecycle.in_process_bus() {
555        return Ok(Sender::InProcess {
556            bus: Arc::clone(bus),
557            local: lifecycle.local_listener().is_some(),
558        });
559    }
560    let attaching = Arc::clone(lifecycle);
561    let socket = lifecycle
562        .on_runtime(async move {
563            match attaching.send_target() {
564                target @ SendTarget::Local(_) => {
565                    let address = target.address().to_owned();
566                    let mut socket = XPubSocket::new();
567                    attach_to(&mut socket, target).await?;
568                    await_filter(&mut socket, &address).await?;
569                    Ok(PubSide::Loopback(socket))
570                }
571                target => {
572                    let mut socket = PubSocket::new();
573                    attach_to(&mut socket, target).await?;
574                    Ok(PubSide::Remote(socket))
575                }
576            }
577        })
578        .await?;
579    Ok(Sender::Socket(socket))
580}
581
582/// Reads the subscribe message the SUB socket at `address` sends once the handshake is done,
583/// which is what enters its filter into the socket's table.
584///
585/// The subscription subscribes once, to its own name, and a SUB socket sends its filters to every
586/// peer that connects, so the first message from it is the whole table.
587async fn await_filter(socket: &mut XPubSocket, address: &str) -> Result<(), ZmqError> {
588    timeout(SEND_RETRY_WINDOW, socket.recv())
589        .await
590        .unwrap_or(Err(WireError::Other("the subscription sent no filter")))
591        .map(drop)
592        .map_err(|err| ZmqError::Endpoint {
593            endpoint: address.to_owned(),
594            source: box_err(err),
595        })
596}
597
598/// The publish policy for [`ZmqFanoutPublisher`].
599///
600/// # Examples
601///
602/// An auditor that the order services dial, broadcasting an audit record for each order event on
603/// a fan-out of its own:
604///
605/// ```
606/// use ruststream_zeromq::prelude::*;
607/// use serde::{Deserialize, Serialize};
608///
609/// #[derive(Deserialize)]
610/// struct OrderEvent {
611///     id: u64,
612/// }
613///
614/// #[derive(Serialize, Outgoing)]
615/// #[outgoing(name = "audit.orders")]
616/// struct AuditRecord {
617///     order: u64,
618/// }
619///
620/// #[subscriber("orders", reply)]
621/// async fn audit(event: &OrderEvent) -> AuditRecord {
622///     AuditRecord { order: event.id }
623/// }
624///
625/// #[ruststream::app]
626/// fn app() -> impl App {
627///     let records = ZmqFanout::new(ZmqEndpoint::bind("tcp://0.0.0.0:5557")).bindable();
628///     let to_records = records.bind(ZmqFanoutPublish);
629///     RustStream::new(AppInfo::new("auditor", "0.1.0"))
630///         .with_broker(ZmqFanout::new(ZmqEndpoint::bind("tcp://0.0.0.0:5556")), |b| {
631///             b.include(audit).out_reply(to_records);
632///         })
633///         .register_broker(records)
634/// }
635/// ```
636#[derive(Debug, Clone, Copy, Default)]
637#[must_use]
638pub struct ZmqFanoutPublish;
639
640impl<Role: EndpointRole> PublishPolicy<ConnectedZmqFanout<Role>> for ZmqFanoutPublish {
641    type Live = ZmqFanoutPublisher;
642
643    fn pair(
644        self,
645        connected: &ConnectedZmqFanout<Role>,
646    ) -> impl Future<Output = Result<Self::Live, PairError>> {
647        ready(Ok(connected.publisher()))
648    }
649
650    /// The destination reaches the binding, because on this pattern it is both frame 0 of every
651    /// message on the channel and the prefix a SUB peer subscribes with to receive it.
652    #[cfg(feature = "asyncapi")]
653    fn channel_bindings(&self, channel: &str) -> Bindings {
654        bindings::channel(SocketPair::PubSub, channel)
655    }
656}
657
658impl<Role: EndpointRole> DefaultPublish for ConnectedZmqFanout<Role> {
659    type Policy = ZmqFanoutPublish;
660}