Skip to main content

ruststream_pulsar/
broker.rs

1//! The broker ladder: [`PulsarBroker`] -> [`ConnectedPulsarBroker`].
2//!
3//! Construction is synchronous and I/O-free; the client dials in the consuming
4//! [`Broker::connect`], and the connected form holds the live client directly. One shared cell
5//! remains so publishers can be handed out while the application is still being assembled,
6//! before `connect` runs.
7
8use std::collections::HashMap;
9use std::num::{NonZeroU32, NonZeroUsize};
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::time::Duration;
13
14use pulsar::{Authentication, OperationRetryOptions, Pulsar, TokioExecutor};
15#[cfg(feature = "testing")]
16use ruststream::testing::{Coordinator, InProcess, TestableBroker};
17use ruststream::{
18    Broker, BrokerMoves, ConnectedBroker, DeclareRetryError, DefaultPublish, DescribeServer,
19    RetryDeclaration, ServerSpec, Subscribe,
20};
21#[cfg(feature = "testing")]
22use ruststream::{BytesMut, OutgoingMessage, RawMessage};
23use tokio::runtime::Handle;
24use tokio::sync::{Mutex, OnceCell};
25
26use crate::error::{PulsarError, box_err};
27#[cfg(feature = "testing")]
28use crate::in_process::{self, Bus, Route, Subscriptions};
29use crate::publisher::{PulsarProducer, PulsarPublish, PulsarPublisher};
30use crate::subscriber::PulsarSubscriber;
31use crate::subscription::{DeclaredRetries, PulsarSubscription};
32
33/// The largest payload a Pulsar server accepts by default: its `maxMessageSize` setting.
34pub(crate) const DEFAULT_MAX_MESSAGE_SIZE: usize = 5 * 1024 * 1024;
35
36/// How long the client keeps asking for an operation a server has not accepted yet.
37///
38/// The client retries an answer that says "not now" - a broker that is still coming up, a
39/// producer or a consumer the topic already has - rather than reporting it, and by default it
40/// retries for ever, five seconds apart. That is what waits out a broker restart, and it is also
41/// what makes a subscription a server refuses (an
42/// [`Exclusive`](crate::SubscriptionType::Exclusive) one another consumer holds) a call that
43/// never returns. [`attempts`](Self::attempts) puts a number on it.
44///
45/// The bound covers every retried operation of the connection, the consumer's own reconnect
46/// included, so a service that shortens it trades waiting out an outage for hearing about it.
47///
48/// # Examples
49///
50/// ```
51/// # mod demo {
52/// use std::time::Duration;
53///
54/// use ruststream_pulsar::prelude::*;
55/// # use serde::Deserialize;
56/// # #[derive(Deserialize)]
57/// # struct Order {
58/// #     id: u64,
59/// # }
60/// # #[subscriber(PulsarSubscription::new("orders", "workers").subscription_type(SubscriptionType::Exclusive))]
61/// # async fn handle(order: &Order) -> HandlerOutcome {
62/// #     tracing::info!(order.id, "accepted");
63/// #     HandlerOutcome::ack()
64/// # }
65///
66/// #[ruststream::app]
67/// fn app() -> impl App {
68///     // Three tries a second apart, then startup reports what the server said instead of
69///     // waiting for the holder of the exclusive subscription to leave.
70///     let retries = OperationRetries::attempts(nonzero!(3u32)).delay(Duration::from_secs(1));
71///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
72///         PulsarBroker::new("pulsar://localhost:6650").operation_retries(retries),
73///         |b| {
74///             b.include(handle);
75///         },
76///     )
77/// }
78/// # }
79/// # fn main() {}
80/// ```
81#[derive(Debug, Clone, PartialEq, Eq)]
82#[must_use]
83pub struct OperationRetries {
84    attempts: Option<NonZeroU32>,
85    delay: Duration,
86    timeout: Duration,
87}
88
89/// The client's own numbers, which apply unless a step below names another.
90const DEFAULT_RETRY_DELAY: Duration = Duration::from_secs(5);
91const DEFAULT_OPERATION_TIMEOUT: Duration = Duration::from_secs(30);
92
93impl OperationRetries {
94    /// Keeps asking for ever, which is the client's own behaviour.
95    pub const fn unbounded() -> Self {
96        Self {
97            attempts: None,
98            delay: DEFAULT_RETRY_DELAY,
99            timeout: DEFAULT_OPERATION_TIMEOUT,
100        }
101    }
102
103    /// Gives up after `attempts` tries and reports what the server answered.
104    ///
105    /// One attempt means the first answer is the final one.
106    pub const fn attempts(attempts: NonZeroU32) -> Self {
107        Self {
108            attempts: Some(attempts),
109            ..Self::unbounded()
110        }
111    }
112
113    /// Waits `delay` between tries. Defaults to five seconds.
114    pub const fn delay(mut self, delay: Duration) -> Self {
115        self.delay = delay;
116        self
117    }
118
119    /// Gives one try `timeout` to be answered at all. Defaults to thirty seconds.
120    pub const fn timeout(mut self, timeout: Duration) -> Self {
121        self.timeout = timeout;
122        self
123    }
124}
125
126impl Default for OperationRetries {
127    fn default() -> Self {
128        Self::unbounded()
129    }
130}
131
132impl From<OperationRetries> for OperationRetryOptions {
133    /// The client counts the retries that follow the first try, so a bound of `n` tries is
134    /// `n - 1` retries.
135    fn from(retries: OperationRetries) -> Self {
136        Self {
137            operation_timeout: retries.timeout,
138            retry_delay: retries.delay,
139            max_retries: retries.attempts.map(|attempts| attempts.get() - 1),
140        }
141    }
142}
143
144/// What a connected broker and every handle derived from it speak over: the client, or, under
145/// the `testing` feature, the in-process transport the test harness connected instead.
146///
147/// Without the feature there is one variant, so the type is the client itself and every `match`
148/// on it is irrefutable: a production build carries no second transport and no branch to it.
149pub(crate) enum Transport {
150    Client(Pulsar<TokioExecutor>),
151    #[cfg(feature = "testing")]
152    InProcess(Arc<Bus>),
153}
154
155// The zero-cost promise of the in-process mode, held by the compiler: a build without it gives
156// the transport exactly the size of the client it wraps.
157#[cfg(not(feature = "testing"))]
158const _: () = assert!(size_of::<Transport>() == size_of::<Pulsar<TokioExecutor>>());
159
160/// The live client state shared by the connected form and every handle derived from it.
161///
162/// Why runtime checks exist here at all: the client handle is `Clone` and would happily
163/// reconnect after our typed shutdown, and publishers may be handed out before `connect` and
164/// outlive `shutdown` (aliasing) - so the closed state is an explicit flag a stale handle
165/// trips over instead of silently succeeding.
166pub(crate) struct Core {
167    pub(crate) transport: Transport,
168    pub(crate) closed: AtomicBool,
169    /// Per-topic producers, shared by every publisher handle so shutdown can close them.
170    pub(crate) producers: Mutex<HashMap<String, Arc<Mutex<PulsarProducer>>>>,
171    /// The subscription a bare topic name joins; see [`PulsarBroker::default_subscription`].
172    default_subscription: Option<String>,
173    /// What registrations mounted by a bare topic name declared about their retries.
174    pub(crate) declared_retries: DeclaredRetries,
175    /// The largest payload a publish may carry; see [`PulsarBroker::max_message_size`].
176    pub(crate) max_message_size: usize,
177    /// The runtime `connect` ran on. Every task the broker starts runs here: a subscription's
178    /// driver, and through it a delayed retry, whichever thread settles the delivery.
179    pub(crate) runtime: Handle,
180    /// The subscriptions this connection opened, which the test harness's routing answer reads
181    /// on either transport.
182    #[cfg(feature = "testing")]
183    opened: Subscriptions,
184}
185
186impl Core {
187    fn new(transport: Transport, broker: &PulsarBroker, runtime: Handle) -> Self {
188        Self {
189            transport,
190            runtime,
191            closed: AtomicBool::new(false),
192            producers: Mutex::new(HashMap::new()),
193            default_subscription: broker.default_subscription.clone(),
194            declared_retries: DeclaredRetries::default(),
195            max_message_size: broker.max_message_size,
196            #[cfg(feature = "testing")]
197            opened: Subscriptions::default(),
198        }
199    }
200
201    pub(crate) fn ensure_open(&self) -> Result<(), PulsarError> {
202        if self.closed.load(Ordering::Acquire) {
203            return Err(PulsarError::NotConnected);
204        }
205        Ok(())
206    }
207}
208
209impl std::fmt::Debug for Core {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        f.debug_struct("Core")
212            .field("closed", &self.closed.load(Ordering::Relaxed))
213            .finish_non_exhaustive()
214    }
215}
216
217pub(crate) type CoreCell = Arc<OnceCell<Arc<Core>>>;
218
219/// An Apache Pulsar broker for the `RustStream` messaging framework.
220///
221/// `new` is synchronous and records only configuration; the runtime dials once at startup via
222/// the consuming [`Broker::connect`]. That is what lets a service compose with the synchronous
223/// `#[ruststream::app]` builder.
224///
225/// # Examples
226///
227/// ```
228/// # mod demo {
229/// use std::env;
230///
231/// use ruststream_pulsar::prelude::*;
232/// # use serde::Deserialize;
233/// # #[derive(Deserialize)]
234/// # struct Order {
235/// #     id: u64,
236/// # }
237/// # #[subscriber(PulsarSubscription::new("orders", "workers"))]
238/// # async fn handle(order: &Order) -> HandlerOutcome {
239/// #     tracing::info!(order.id, "accepted");
240/// #     HandlerOutcome::ack()
241/// # }
242///
243/// #[ruststream::app]
244/// fn app() -> impl App {
245///     let mut broker = PulsarBroker::new("pulsar+ssl://pulsar.acme.internal:6651");
246///     if let Ok(token) = env::var("PULSAR_TOKEN") {
247///         broker = broker.token(token);
248///     }
249///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
250///         b.include(handle);
251///     })
252/// }
253/// # }
254/// # fn main() {}
255/// ```
256///
257/// A test runs the app `main` runs. With the `testing` feature, `TestApp::start` connects this
258/// broker in process, with no server, and the test addresses it by its production type:
259///
260/// ```
261/// # #[cfg(feature = "testing")]
262/// # mod demo {
263/// use std::error::Error;
264///
265/// use ruststream::testing::TestApp;
266/// use ruststream_pulsar::prelude::*;
267/// use serde::{Deserialize, Serialize};
268///
269/// #[derive(Debug, Deserialize, Eq, Outgoing, PartialEq, Serialize)]
270/// struct Order {
271///     id: u64,
272///     quantity: u32,
273/// }
274///
275/// #[subscriber(PulsarSubscription::new("orders", "workers"))]
276/// async fn handle(order: &Order) -> HandlerOutcome {
277///     if order.quantity == 0 {
278///         return HandlerOutcome::drop();
279///     }
280///     HandlerOutcome::ack()
281/// }
282///
283/// /// The app `main` runs, and the one the tests hand the harness.
284/// pub fn app() -> RustStream {
285///     RustStream::new(AppInfo::new("orders", "0.1.0"))
286///         .with_broker(PulsarBroker::new("pulsar://localhost:6650"), |b| {
287///             b.include(handle);
288///         })
289/// }
290///
291/// pub async fn an_empty_order_is_dropped() -> Result<(), Box<dyn Error>> {
292///     let tb = TestApp::start(app()).await?;
293///
294///     // The publish returns once the handler it woke has settled.
295///     tb.broker::<PulsarBroker>()
296///         .message(&Order { id: 1, quantity: 0 })
297///         .to("orders")
298///         .publish()
299///         .await?;
300///
301///     tb.broker::<PulsarBroker>()
302///         .subscriber("orders")
303///         .assert_called_once()
304///         .with(&Order { id: 1, quantity: 0 })
305///         .settled(HandlerOutcome::drop());
306///     tb.shutdown().await?;
307///     Ok(())
308/// }
309/// # }
310/// # #[cfg(feature = "testing")]
311/// # #[tokio::main]
312/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
313/// #     demo::an_empty_order_is_dropped().await
314/// # }
315/// # #[cfg(not(feature = "testing"))]
316/// # fn main() {}
317/// ```
318#[derive(Debug, Clone)]
319#[must_use]
320pub struct PulsarBroker {
321    url: String,
322    token: Option<String>,
323    default_subscription: Option<String>,
324    // None leaves the client's own retry behaviour in place, which is unbounded.
325    retries: Option<OperationRetries>,
326    max_message_size: usize,
327    // Shared with publishers handed out before connect; the consuming connect fills it.
328    cell: CoreCell,
329}
330
331impl PulsarBroker {
332    /// Records the service URL (`pulsar://` or `pulsar+ssl://`). No I/O.
333    pub fn new(url: impl Into<String>) -> Self {
334        Self {
335            url: url.into(),
336            token: None,
337            default_subscription: None,
338            retries: None,
339            max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
340            cell: Arc::new(OnceCell::new()),
341        }
342    }
343
344    /// Authenticates with a JWT token.
345    pub fn token(mut self, token: impl Into<String>) -> Self {
346        self.token = Some(token.into());
347        self
348    }
349
350    /// The durable subscription a subscription by bare topic name joins.
351    ///
352    /// `#[subscriber("orders")]` names a topic and no subscription, and Pulsar has no anonymous
353    /// consumer, so the name comes from here. A subscription name is the cursor on the server:
354    /// every consumer under it shares one backlog, the handlers of this service and those of any
355    /// other service that names the same one. Name it after the service that owns the cursor.
356    /// A bare topic name on a broker without it fails at startup with
357    /// [`PulsarError::Invalid`] naming this setting; a [`PulsarSubscription`] names its own
358    /// subscription and needs none.
359    ///
360    /// # Examples
361    ///
362    /// ```
363    /// # mod demo {
364    /// use ruststream_pulsar::prelude::*;
365    /// # use serde::Deserialize;
366    /// # #[derive(Deserialize)]
367    /// # struct Order {
368    /// #     id: u64,
369    /// # }
370    ///
371    /// /// Joins the `orders-worker` subscription on `orders`, with every other instance of the
372    /// /// service.
373    /// #[subscriber("orders")]
374    /// async fn handle(order: &Order) -> HandlerOutcome {
375    ///     tracing::info!(order.id, "accepted");
376    ///     HandlerOutcome::ack()
377    /// }
378    ///
379    /// #[ruststream::app]
380    /// fn app() -> impl App {
381    ///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
382    ///         PulsarBroker::new("pulsar://localhost:6650").default_subscription("orders-worker"),
383    ///         |b| {
384    ///             b.include(handle);
385    ///         },
386    ///     )
387    /// }
388    /// # }
389    /// # fn main() {}
390    /// ```
391    pub fn default_subscription(mut self, subscription: impl Into<String>) -> Self {
392        self.default_subscription = Some(subscription.into());
393        self
394    }
395
396    /// Bounds how long the client keeps asking for an operation a server has not accepted.
397    ///
398    /// Left alone, the client retries such an answer for ever, five seconds apart, which is what
399    /// waits out a broker that is restarting. The same patience covers a subscription the server
400    /// refuses because another consumer holds it, so a second consumer of an
401    /// [`Exclusive`](crate::SubscriptionType::Exclusive) subscription neither takes it nor
402    /// reports anything - it waits, and the service does not start. A service that has to hear
403    /// about that instead bounds the tries here, and `subscribe` then returns
404    /// [`PulsarError::Subscribe`](PulsarError) naming the topic.
405    ///
406    /// The bound is the connection's, not one operation's: it covers the lookups, the producers
407    /// and the consumer's own reconnect after a disconnect, so a short bound turns a long outage
408    /// into a subscription that gives up rather than one that waits.
409    ///
410    /// # Examples
411    ///
412    /// ```
413    /// # mod demo {
414    /// use ruststream_pulsar::prelude::*;
415    /// # use serde::Deserialize;
416    /// # #[derive(Deserialize)]
417    /// # struct Order {
418    /// #     id: u64,
419    /// # }
420    /// # #[subscriber(PulsarSubscription::new("orders", "workers").subscription_type(SubscriptionType::Exclusive))]
421    /// # async fn handle(order: &Order) -> HandlerOutcome {
422    /// #     tracing::info!(order.id, "accepted");
423    /// #     HandlerOutcome::ack()
424    /// # }
425    ///
426    /// #[ruststream::app]
427    /// fn app() -> impl App {
428    ///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
429    ///         PulsarBroker::new("pulsar://localhost:6650")
430    ///             .operation_retries(OperationRetries::attempts(nonzero!(3u32))),
431    ///         |b| {
432    ///             b.include(handle);
433    ///         },
434    ///     )
435    /// }
436    /// # }
437    /// # fn main() {}
438    /// ```
439    pub fn operation_retries(mut self, retries: OperationRetries) -> Self {
440        self.retries = Some(retries);
441        self
442    }
443
444    /// The largest payload a publish may carry, in bytes: the server's `maxMessageSize`, five
445    /// mebibytes unless the deployment raised or lowered it.
446    ///
447    /// A larger publish fails with [`PulsarError::Publish`] before it leaves the service. The
448    /// client does not check the size itself, and a server answers an oversized frame by
449    /// dropping the connection, which fails every producer and consumer sharing it; the
450    /// in-process mode refuses the same publishes.
451    ///
452    /// # Examples
453    ///
454    /// ```
455    /// # mod demo {
456    /// use ruststream_pulsar::prelude::*;
457    /// # use serde::Deserialize;
458    /// # #[derive(Deserialize)]
459    /// # struct Scan {
460    /// #     id: u64,
461    /// # }
462    /// # #[subscriber(PulsarSubscription::new("scans", "archive"))]
463    /// # async fn archive(scan: &Scan) -> HandlerOutcome {
464    /// #     tracing::info!(scan.id, "archived");
465    /// #     HandlerOutcome::ack()
466    /// # }
467    ///
468    /// #[ruststream::app]
469    /// fn app() -> impl App {
470    ///     // The cluster runs with `maxMessageSize=10485760`.
471    ///     RustStream::new(AppInfo::new("archive", "0.1.0")).with_broker(
472    ///         PulsarBroker::new("pulsar://localhost:6650")
473    ///             .max_message_size(nonzero!(10_485_760usize)),
474    ///         |b| {
475    ///             b.include(archive);
476    ///         },
477    ///     )
478    /// }
479    /// # }
480    /// # fn main() {}
481    /// ```
482    pub fn max_message_size(mut self, bytes: NonZeroUsize) -> Self {
483        self.max_message_size = bytes.get();
484        self
485    }
486
487    /// A publisher sharing this broker's connection cell; buildable before `connect`.
488    #[must_use]
489    pub fn publisher(&self) -> PulsarPublisher {
490        PulsarPublisher::new(Arc::clone(&self.cell))
491    }
492}
493
494impl Broker for PulsarBroker {
495    type Error = PulsarError;
496    type Connected = ConnectedPulsarBroker;
497
498    async fn connect(self) -> Result<Self::Connected, Self::Error> {
499        let core = self
500            .cell
501            .get_or_try_init(async || {
502                let mut builder = Pulsar::builder(self.url.clone(), TokioExecutor);
503                if let Some(token) = &self.token {
504                    builder = builder.with_auth(Authentication {
505                        name: "token".to_owned(),
506                        data: token.clone().into_bytes(),
507                    });
508                }
509                if let Some(retries) = self.retries.clone() {
510                    builder = builder.with_operation_retry_options(retries.into());
511                }
512                let client = builder
513                    .build()
514                    .await
515                    .map_err(|e| PulsarError::Connect(box_err(e)))?;
516                Ok::<_, PulsarError>(Arc::new(Core::new(
517                    Transport::Client(client),
518                    &self,
519                    Handle::current(),
520                )))
521            })
522            .await?
523            .clone();
524        Ok(ConnectedPulsarBroker {
525            core,
526            cell: self.cell,
527        })
528    }
529}
530
531/// The in-process mode: the connected form a test runs the production app against, carrying the
532/// in-process transport in place of the client and every setting of this broker.
533///
534/// The service URL is parsed as the client parses it, so a broker a service could not connect is
535/// not one a test can connect either. The connection cell is the one `connect` fills, so a
536/// publisher taken from the broker before the harness connected it publishes in process.
537#[cfg(feature = "testing")]
538impl InProcess for PulsarBroker {
539    async fn connect_in_process(self) -> Result<Self::Connected, Self::Error> {
540        in_process::check_url(&self.url)?;
541        let core = self
542            .cell
543            .get_or_init(async || {
544                let runtime = Handle::current();
545                Arc::new(Core::new(
546                    Transport::InProcess(Bus::new(runtime.clone())),
547                    &self,
548                    runtime,
549                ))
550            })
551            .await
552            .clone();
553        Ok(ConnectedPulsarBroker {
554            core,
555            cell: self.cell,
556        })
557    }
558}
559
560#[cfg(feature = "testing")]
561ruststream::register_testable_broker!(PulsarBroker);
562
563/// The description carries the address a client dials and nothing else: the generated `AsyncAPI`
564/// document is published and shared, so a `user:password@` in the service URL must not reach it.
565/// `ServerSpec::from_url` is the framework's own reduction, so every broker crate drops the
566/// userinfo the same way.
567impl DescribeServer for PulsarBroker {
568    fn describe_server(&self) -> ServerSpec {
569        ServerSpec::from_url(&self.url, "pulsar")
570    }
571}
572
573/// The typed witness that `connect` succeeded: holds the live client directly.
574#[derive(Debug)]
575pub struct ConnectedPulsarBroker {
576    pub(crate) core: Arc<Core>,
577    // Keeps the cell of publishers handed out before connect alive and filled.
578    cell: CoreCell,
579}
580
581impl ConnectedPulsarBroker {
582    /// A publisher from the connected form. It rides the same cell-backed publisher type as
583    /// the early path; by now `connect` has filled the cell, so it resolves immediately.
584    #[must_use]
585    pub fn publisher(&self) -> PulsarPublisher {
586        PulsarPublisher::new(Arc::clone(&self.cell))
587    }
588
589    /// Opens the subscription described by `descriptor`.
590    ///
591    /// # Errors
592    ///
593    /// Returns [`PulsarError`] when the descriptor is invalid, the consumer cannot be created,
594    /// or the broker is shut down.
595    pub async fn subscribe_descriptor(
596        &self,
597        descriptor: PulsarSubscription,
598    ) -> Result<PulsarSubscriber, PulsarError> {
599        descriptor.validate()?;
600        self.core.ensure_open()?;
601        #[cfg(feature = "testing")]
602        let (route, name, subscription) = (
603            Route::of(&descriptor)?,
604            descriptor.source_name().to_owned(),
605            descriptor.subscription().to_owned(),
606        );
607        #[cfg(feature = "testing")]
608        let recorded = route.clone();
609        let subscriber = match &self.core.transport {
610            Transport::Client(client) => {
611                // The consumer is built on the runtime the broker connected on: the client starts
612                // the connection it needs on the runtime that asks, and a subscription opened from
613                // a runtime that stops (a dedicated thread's) would lose that connection with it.
614                let (client, core) = (client.clone(), Arc::clone(&self.core));
615                let topic = descriptor.display_topic();
616                self.core
617                    .runtime
618                    .spawn(async move { PulsarSubscriber::open(&client, descriptor, &core).await })
619                    .await
620                    .map_err(|err| PulsarError::Subscribe {
621                        topic,
622                        source: box_err(err),
623                    })??
624            }
625            #[cfg(feature = "testing")]
626            Transport::InProcess(bus) => {
627                let (subscriber, consumer) = in_process::subscribe(bus, descriptor, route)?;
628                self.core
629                    .opened
630                    .record(name, subscription, recorded, Some(consumer));
631                return Ok(subscriber);
632            }
633        };
634        #[cfg(feature = "testing")]
635        self.core.opened.record(name, subscription, recorded, None);
636        Ok(subscriber)
637    }
638}
639
640impl ConnectedBroker for ConnectedPulsarBroker {
641    type Error = PulsarError;
642    type Closed = ();
643
644    async fn shutdown(self) -> Result<(), Self::Error> {
645        self.core.closed.store(true, Ordering::Release);
646        #[cfg(feature = "testing")]
647        if let Transport::InProcess(bus) = &self.core.transport {
648            bus.close();
649        }
650        // The client has no close of its own; producers are the handles holding broker-side
651        // state worth a clean goodbye.
652        let producers: Vec<_> = {
653            let mut map = self.core.producers.lock().await;
654            map.drain().map(|(_, producer)| producer).collect()
655        };
656        for producer in producers {
657            let mut producer = producer.lock().await;
658            if let Err(err) = Box::pin(producer.close()).await {
659                tracing::debug!(error = %err, "pulsar producer close failed");
660            }
661        }
662        Ok(())
663    }
664}
665
666impl Subscribe for ConnectedPulsarBroker {
667    type Subscriber = PulsarSubscriber;
668    /// A bare name opens the same consumer a descriptor does, so a spent delivery moves at the
669    /// client here too, under the policy the registration declared.
670    type Copies = BrokerMoves;
671
672    /// A bare name joins the broker's [default subscription](PulsarBroker::default_subscription),
673    /// so the handlers mounted by name compete on one durable cursor. A broker that names none
674    /// refuses with [`PulsarError::Invalid`] naming the setting.
675    async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
676        let subscription = self.core.default_subscription.as_deref().ok_or_else(|| {
677            PulsarError::Invalid(format!(
678                "bare topic name '{name}' has no subscription to join: set \
679                 `PulsarBroker::default_subscription(..)`, or mount \
680                 `PulsarSubscription::new(topic, subscription)`"
681            ))
682        })?;
683        let descriptor = self.core.declared_retries.subscription(name, subscription);
684        self.subscribe_descriptor(descriptor).await
685    }
686
687    /// Maps the declaration onto the consumer this name opens: the limit and the topic become
688    /// the client's own `DeadLetterPolicy`, built in `subscribe` where the connection exists.
689    fn declare_retry(
690        &self,
691        name: &str,
692        declaration: &RetryDeclaration,
693    ) -> Result<(), DeclareRetryError> {
694        self.core.declared_retries.declare(name, declaration)
695    }
696}
697
698impl DefaultPublish for ConnectedPulsarBroker {
699    type Policy = PulsarPublish;
700}
701
702/// The harness's view of the connected broker: what it injects and reads back on the in-process
703/// transport, and, on either transport, which subscriptions a publish reaches.
704///
705/// # Panics
706///
707/// `inject` and `published` panic on a broker connected with `connect`: the harness drives only
708/// the transport `connect_in_process` produced, and a live connection has no log to read and no
709/// synchronous way to take a message. `inject` panics on a message a server refuses (a
710/// destination that is no topic name, a header value that is not UTF-8), which the trait gives no
711/// way to report.
712#[cfg(feature = "testing")]
713impl TestableBroker for ConnectedPulsarBroker {
714    fn install_coordinator(&self, coordinator: Coordinator) {
715        if let Transport::InProcess(bus) = &self.core.transport {
716            bus.install(coordinator);
717        }
718    }
719
720    fn inject(&self, message: OutgoingMessage<'_>) {
721        let bus = self.bus("inject");
722        // An injection stands in for an external producer; against a shut-down transport it is
723        // dropped, which is what a producer talking to a closed broker achieves in effect.
724        if self.core.ensure_open().is_err() {
725            return;
726        }
727        let name = message.name();
728        let owned = OutgoingMessage::produced(name, BytesMut::from(message.payload()))
729            .with_headers(message.headers().clone());
730        if let Err(err) = bus.publish(owned, None) {
731            panic!("the injected message to {name:?} is not one a server takes: {err}");
732        }
733    }
734
735    fn published(&self, name: &str) -> Vec<RawMessage> {
736        self.bus("published").published(name)
737    }
738
739    /// Pulsar's routing: a message reaches every subscription over its topic once, and within one
740    /// subscription one of its consumers, so each durable subscription the topic reaches is owed
741    /// the message once, by one of its consumers in `subscriptions`. In process that is the
742    /// consumer the transport picks for the next message; against a server, which picks its
743    /// consumer itself, it is the first of them. A topic reads the same under either spelling,
744    /// and a pattern matches the fully qualified names of the namespace its prefix names.
745    fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
746        let router = match &self.core.transport {
747            Transport::InProcess(bus) => Some(bus.router()),
748            Transport::Client(_) => None,
749        };
750        self.core.opened.routes(destination, subscriptions, router)
751    }
752}
753
754#[cfg(feature = "testing")]
755impl ConnectedPulsarBroker {
756    /// The in-process transport, which is all the harness injects into and reads back from.
757    fn bus(&self, what: &str) -> &Arc<Bus> {
758        match &self.core.transport {
759            Transport::InProcess(bus) => bus,
760            Transport::Client(_) => panic!(
761                "TestableBroker::{what} reached a broker connected with `connect`; the harness \
762                 drives the transport `connect_in_process` produces"
763            ),
764        }
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    #[cfg(feature = "testing")]
771    use ruststream::Publisher;
772    use ruststream::nonzero;
773
774    use super::*;
775
776    /// Every URL shape this broker accepts, reduced to the address a client dials.
777    #[test]
778    fn the_description_carries_the_address_alone() {
779        for (url, address) in [
780            ("pulsar://broker:6650", "broker:6650"),
781            ("pulsar+ssl://broker:6651", "broker:6651"),
782            ("pulsar://broker", "broker"),
783            ("pulsar://broker:6650/", "broker:6650"),
784            ("pulsar://admin:s3cret@broker:6650", "broker:6650"),
785            ("pulsar+ssl://admin:s3cret@broker", "broker"),
786            // A password may hold an `@`, so the userinfo ends at the last one.
787            ("pulsar://admin:p@ssw0rd@broker:6650", "broker:6650"),
788        ] {
789            assert_eq!(
790                PulsarBroker::new(url).describe_server().host.as_deref(),
791                Some(address),
792                "url {url}",
793            );
794        }
795    }
796
797    /// The client counts what follows the first try, so the bound a service writes in tries is
798    /// one more than the number the client is given. An off-by-one here is a service that gives
799    /// up a try early or waits one longer than it asked to.
800    #[test]
801    fn a_bound_in_tries_becomes_the_retries_that_follow_the_first() {
802        let bounded = OperationRetryOptions::from(OperationRetries::attempts(nonzero!(3u32)));
803        assert_eq!(bounded.max_retries, Some(2));
804
805        let once = OperationRetryOptions::from(OperationRetries::attempts(nonzero!(1u32)));
806        assert_eq!(
807            once.max_retries,
808            Some(0),
809            "one try must leave the first answer final",
810        );
811
812        let unbounded = OperationRetryOptions::from(OperationRetries::unbounded());
813        assert_eq!(unbounded.max_retries, None);
814        assert_eq!(unbounded.retry_delay, DEFAULT_RETRY_DELAY);
815        assert_eq!(unbounded.operation_timeout, DEFAULT_OPERATION_TIMEOUT);
816    }
817
818    /// A URL the client cannot connect with is refused in process as `connect` refuses it, so a
819    /// test cannot pass on a broker the service could not start.
820    #[cfg(feature = "testing")]
821    #[tokio::test]
822    async fn an_address_connect_refuses_is_refused_in_process() {
823        for url in ["not a url", "pulsar://"] {
824            let refused = PulsarBroker::new(url).connect_in_process().await;
825            assert!(
826                matches!(refused, Err(PulsarError::Connect(_))),
827                "{url:?} must be refused, got {refused:?}",
828            );
829        }
830    }
831
832    /// A publisher taken off the broker before the harness connected it shares the connection the
833    /// in-process transition fills, as it shares the one `connect` fills.
834    #[cfg(feature = "testing")]
835    #[tokio::test]
836    async fn an_early_publisher_publishes_in_process() {
837        let broker = PulsarBroker::new("pulsar://localhost:6650");
838        let early = broker.publisher();
839        let connected = broker
840            .connect_in_process()
841            .await
842            .expect("connects in process");
843
844        early
845            .publish(
846                OutgoingMessage::produced("orders", BytesMut::from(&b"{}"[..])),
847                None,
848            )
849            .await
850            .expect("the early publisher reaches the in-process transport");
851        assert_eq!(connected.published("orders").len(), 1);
852
853        connected.shutdown().await.expect("shutdown");
854        let late = early
855            .publish(
856                OutgoingMessage::produced("orders", BytesMut::from(&b"{}"[..])),
857                None,
858            )
859            .await;
860        assert!(matches!(late, Err(PulsarError::NotConnected)), "{late:?}");
861    }
862
863    /// A delivery held across the shutdown has no consumer to go back to in process, so handing it
864    /// back answers an error rather than promising a redelivery that never comes.
865    #[cfg(feature = "testing")]
866    #[tokio::test]
867    async fn a_delivery_held_across_shutdown_is_not_handed_back_in_process() {
868        use futures::StreamExt;
869        use ruststream::{IncomingMessage, Subscriber};
870
871        let connected = PulsarBroker::new("pulsar://localhost:6650")
872            .connect_in_process()
873            .await
874            .expect("connects in process");
875        let mut subscriber = connected
876            .subscribe_descriptor(PulsarSubscription::new("orders", "workers"))
877            .await
878            .expect("subscribes");
879        let publisher = connected.publisher();
880        for _ in 0..2 {
881            publisher
882                .publish(
883                    OutgoingMessage::produced("orders", BytesMut::from(&b"{}"[..])),
884                    None,
885                )
886                .await
887                .expect("publishes");
888        }
889        let (requeued, delayed) = {
890            let mut stream = subscriber.stream();
891            let mut next = async || {
892                stream
893                    .next()
894                    .await
895                    .expect("the subscription is open")
896                    .expect("a delivery")
897            };
898            (next().await, next().await)
899        };
900        connected.shutdown().await.expect("shutdown");
901
902        let requeue = requeued.nack(true).await;
903        assert!(requeue.is_err(), "{requeue:?}");
904        let delay = delayed.nack_after(Duration::from_secs(1)).await;
905        assert!(delay.is_err(), "{delay:?}");
906    }
907
908    /// What a consumer leaves unsettled goes back to its subscription once the consumer closes,
909    /// and waits there for the subscription's next consumer, as on a server.
910    #[cfg(feature = "testing")]
911    #[tokio::test]
912    async fn what_a_closed_consumer_left_unsettled_reaches_the_next_one() {
913        use futures::StreamExt;
914        use ruststream::{IncomingMessage, Subscriber};
915
916        let connected = PulsarBroker::new("pulsar://localhost:6650")
917            .connect_in_process()
918            .await
919            .expect("connects in process");
920        let descriptor = PulsarSubscription::new("orders", "workers");
921        let publisher = connected.publisher();
922        let mut first = connected
923            .subscribe_descriptor(descriptor.clone())
924            .await
925            .expect("subscribes");
926        for payload in [b"dropped".as_slice(), b"requeued", b"queued"] {
927            publisher
928                .publish(
929                    OutgoingMessage::produced("orders", BytesMut::from(payload)),
930                    None,
931                )
932                .await
933                .expect("publishes");
934        }
935        let requeued = {
936            let mut stream = first.stream();
937            let dropped = stream.next().await.expect("open").expect("a delivery");
938            drop(dropped);
939            stream.next().await.expect("open").expect("a delivery")
940        };
941        drop(first);
942        requeued
943            .nack(true)
944            .await
945            .expect("a closed consumer still hands a delivery back");
946
947        let mut next = connected
948            .subscribe_descriptor(descriptor)
949            .await
950            .expect("subscribes again");
951        let mut stream = next.stream();
952        let mut back = Vec::new();
953        for _ in 0..3 {
954            let delivery = stream.next().await.expect("open").expect("a delivery");
955            back.push(delivery.payload().to_vec());
956            delivery.ack().await.expect("ack");
957        }
958        back.sort();
959        assert_eq!(
960            back,
961            [
962                b"dropped".to_vec(),
963                b"queued".to_vec(),
964                b"requeued".to_vec()
965            ]
966        );
967    }
968
969    /// A payload up to the broker's limit is published and one byte more is refused, in process
970    /// as against a server, where the check runs before the client sends anything.
971    #[cfg(feature = "testing")]
972    #[tokio::test]
973    async fn a_payload_over_the_limit_is_refused() {
974        let connected = PulsarBroker::new("pulsar://localhost:6650")
975            .max_message_size(nonzero!(16usize))
976            .connect_in_process()
977            .await
978            .expect("connects in process");
979        let publisher = connected.publisher();
980        let publish = async |size: usize| {
981            publisher
982                .publish(
983                    OutgoingMessage::produced("orders", BytesMut::from(vec![0; size].as_slice())),
984                    None,
985                )
986                .await
987        };
988
989        publish(16)
990            .await
991            .expect("a payload at the limit is published");
992        let refused = publish(17).await;
993        assert!(
994            matches!(refused, Err(PulsarError::Publish { .. })),
995            "{refused:?}"
996        );
997        assert_eq!(connected.published("orders").len(), 1);
998    }
999
1000    /// The generated document is published and shared, so what a service put in its URL to
1001    /// authenticate must not be in it.
1002    #[test]
1003    fn credentials_never_reach_the_description() {
1004        let described = PulsarBroker::new("pulsar://admin:s3cret@broker:6650").describe_server();
1005        let host = described.host.expect("a URL with a host describes one");
1006        assert!(!host.contains('@'), "userinfo survived in {host:?}");
1007        assert!(!host.contains("admin"), "user name survived in {host:?}");
1008        assert!(!host.contains("s3cret"), "password survived in {host:?}");
1009    }
1010}