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}