ruststream/runtime/publish_source.rs
1//! Cross-broker publisher tokens: a [`PublishPolicy`] bound to a concrete broker instance.
2//!
3//! A bare policy attached at an include site pairs with that scope's own broker. When the
4//! handler must publish to a *different* broker (consume Kafka, publish to Redis), or a sibling
5//! task needs a publisher after start, wrap the target broker in [`Bindable`] and mint [`Bound`]
6//! tokens from it before registration - tokens carry the instance identity a foreign scope
7//! cannot provide, and the registration order stops mattering (a bidirectional bridge binds
8//! both directions up front).
9
10use std::sync::Arc;
11
12use std::sync::Mutex;
13
14#[cfg(feature = "asyncapi")]
15use crate::asyncapi::Bindings;
16use crate::runtime::lifecycle::ConnectedSlot;
17#[cfg(feature = "testing")]
18use crate::testing::coordinator::pairs_against;
19use crate::{Broker, Connected, ConnectedBroker, PairError, PublishPolicy};
20
21/// A broker wrapped for token minting before registration.
22///
23/// `Bindable` owns the slot the runtime fills with the broker's connected form, so [`bind`]
24/// hands out [`Bound`] tokens *before* `with_broker` runs - registration order stops mattering,
25/// which is what a bidirectional bridge needs (each direction's scope takes the other broker's
26/// token). Pass the wrapper itself to
27/// [`with_broker`](crate::runtime::RustStream::with_broker); a token whose broker never
28/// registers fails fast at pairing with a clear error.
29///
30/// [`bind`]: Self::bind
31///
32/// # Examples
33///
34/// ```
35/// # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
36/// # mod demo {
37/// use ruststream::memory::prelude::*;
38/// use serde::{Deserialize, Serialize};
39///
40/// #[derive(Deserialize)]
41/// struct Order {
42/// id: u64,
43/// }
44///
45/// /// An order relayed out of the east region, under a name the west region does not relay.
46/// #[derive(Serialize, Outgoing)]
47/// #[outgoing(name = "orders.from-east")]
48/// struct FromEast {
49/// id: u64,
50/// }
51///
52/// /// An order relayed out of the west region, under a name the east region does not relay.
53/// #[derive(Serialize, Outgoing)]
54/// #[outgoing(name = "orders.from-west")]
55/// struct FromWest {
56/// id: u64,
57/// }
58///
59/// #[subscriber("orders", reply)]
60/// async fn east_to_west(order: &Order) -> FromEast {
61/// FromEast { id: order.id }
62/// }
63///
64/// #[subscriber("orders", reply)]
65/// async fn west_to_east(order: &Order) -> FromWest {
66/// FromWest { id: order.id }
67/// }
68///
69/// fn app() -> RustStream {
70/// // Each region relays to the other: both tokens exist before either scope is built.
71/// let east = MemoryBroker::new().bindable();
72/// let west = MemoryBroker::new().bindable();
73/// let to_east = east.bind(Publish);
74/// let to_west = west.bind(Publish);
75/// RustStream::new(AppInfo::new("bridge", "0.1.0"))
76/// .with_broker_labeled("east", east, |b| {
77/// b.include(east_to_west).out_reply(to_west);
78/// })
79/// .with_broker_labeled("west", west, |b| {
80/// b.include(west_to_east).out_reply(to_east);
81/// })
82/// }
83/// # }
84/// # fn main() {}
85/// ```
86pub struct Bindable<B: Broker> {
87 pub(crate) broker: B,
88 pub(crate) slot: ConnectedSlot<B>,
89}
90
91impl<B: Broker + std::fmt::Debug> std::fmt::Debug for Bindable<B> {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("Bindable")
94 .field("broker", &self.broker)
95 .finish_non_exhaustive()
96 }
97}
98
99impl<B: Broker + 'static> Bindable<B> {
100 /// Wraps `broker` so publisher tokens can be minted before it registers.
101 #[must_use]
102 pub fn new(broker: B) -> Self {
103 Self {
104 broker,
105 slot: Arc::new(Mutex::new(None)),
106 }
107 }
108
109 /// Returns the wrapped broker, for its own constructors (publishers, subscribers).
110 #[must_use]
111 pub fn broker(&self) -> &B {
112 &self.broker
113 }
114
115 /// Mints a token for `source`: a publisher source carrying this broker's instance identity,
116 /// usable at any include site, in a sibling task via
117 /// [`RunningApp::publisher`](crate::runtime::RunningApp::publisher), or directly with
118 /// [`Bound::live`] once the app started.
119 #[must_use]
120 pub fn bind<S>(&self, source: S) -> Bound<B, S>
121 where
122 S: PublishPolicy<Connected<B>>,
123 {
124 Bound {
125 slot: Arc::clone(&self.slot),
126 source,
127 }
128 }
129}
130
131/// What the `with_broker` family accepts.
132///
133/// Either a bare [`Broker`] (registered with a fresh internal slot) or a [`Bindable`] carrying
134/// pre-minted tokens. Implemented by those two shapes only; you never implement or call it.
135pub trait BrokerRegistration {
136 /// The broker being registered.
137 type Broker: Broker + 'static;
138
139 #[doc(hidden)]
140 fn into_parts(self) -> (Self::Broker, ConnectedSlot<Self::Broker>);
141}
142
143impl<B: Broker + 'static> BrokerRegistration for B {
144 type Broker = B;
145
146 fn into_parts(self) -> (B, ConnectedSlot<B>) {
147 (self, Arc::new(Mutex::new(None)))
148 }
149}
150
151impl<B: Broker + 'static> BrokerRegistration for Bindable<B> {
152 type Broker = B;
153
154 fn into_parts(self) -> (B, ConnectedSlot<B>) {
155 (self.broker, self.slot)
156 }
157}
158
159/// A publisher source bound to a concrete broker instance, minted by [`Bindable::bind`].
160///
161/// The token shares the slot the runtime fills with that broker's connected form at startup, so
162/// pairing needs no lookup and cannot pick a wrong instance. It implements [`PublishPolicy`]
163/// against *any* connected broker by ignoring it and pairing against its own; that is what lets
164/// a Kafka-scope registration accept a token for the Redis broker.
165pub struct Bound<B2: Broker, S> {
166 pub(crate) slot: ConnectedSlot<B2>,
167 pub(crate) source: S,
168}
169
170impl<B2: Broker, S: std::fmt::Debug> std::fmt::Debug for Bound<B2, S> {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 f.debug_struct("Bound")
173 .field("source", &self.source)
174 .finish_non_exhaustive()
175 }
176}
177
178impl<B2: Broker + 'static, S: Clone> Clone for Bound<B2, S> {
179 fn clone(&self) -> Self {
180 Self {
181 slot: Arc::clone(&self.slot),
182 source: self.source.clone(),
183 }
184 }
185}
186
187impl<B2, S> Bound<B2, S>
188where
189 B2: Broker + 'static,
190 S: PublishPolicy<Connected<B2>>,
191{
192 /// Pairs the token against its own broker, producing the live form.
193 ///
194 /// Available anywhere the application startup has already connected the broker: an
195 /// `after_startup` hook (the documented home of a first publish), or a sibling task after
196 /// [`start`](crate::runtime::RustStream::start) returned (the
197 /// [`RunningApp::publisher`](crate::runtime::RunningApp::publisher) sugar calls this).
198 ///
199 /// # Errors
200 ///
201 /// Returns [`PairError`] when the broker is not connected yet (pairing before startup
202 /// finished), or when the policy's own pairing fails.
203 ///
204 /// # Examples
205 ///
206 /// ```
207 /// # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
208 /// # mod demo {
209 /// use ruststream::memory::prelude::*;
210 /// use ruststream::memory::MemoryError;
211 /// use ruststream::runtime::PublishError;
212 /// use ruststream::PairError;
213 /// use serde::Serialize;
214 ///
215 /// # #[derive(Debug, thiserror::Error)]
216 /// # enum StartupError {
217 /// # #[error(transparent)]
218 /// # Pair(#[from] PairError),
219 /// # #[error(transparent)]
220 /// # Publish(#[from] PublishError<MemoryError>),
221 /// # }
222 /// #[derive(Serialize, Outgoing)]
223 /// #[outgoing(name = "services.started")]
224 /// struct Started {
225 /// service: &'static str,
226 /// }
227 ///
228 /// fn app() -> impl App {
229 /// let broker = MemoryBroker::new().bindable();
230 /// let announce = broker.bind(Publish);
231 /// RustStream::new(AppInfo::new("orders", "0.1.0"))
232 /// .with_broker(broker, |_b| {})
233 /// .after_startup(async move |_state| {
234 /// let publisher = announce.live().await?;
235 /// publisher.message(&Started { service: "orders" }).publish().await?;
236 /// Ok::<_, StartupError>(())
237 /// })
238 /// }
239 /// # }
240 /// # fn main() {}
241 /// ```
242 pub async fn live(self) -> Result<S::Live, PairError> {
243 pair_bound::<B2, S>(&self.slot, self.source).await
244 }
245}
246
247/// Pairs `source` against the broker in `slot`. The runtime's own pairing entry for tokens.
248pub(crate) async fn pair_bound<B2, S>(
249 slot: &ConnectedSlot<B2>,
250 source: S,
251) -> Result<S::Live, PairError>
252where
253 B2: Broker + 'static,
254 S: PublishPolicy<Connected<B2>>,
255{
256 let connected = slot
257 .lock()
258 .expect("connected slot mutex poisoned")
259 .clone()
260 .ok_or_else(|| {
261 PairError::from_boxed(Box::from(
262 "the token's broker is not connected: pairing happens after startup connects \
263 every registered broker",
264 ))
265 })?;
266 // The token's publisher publishes to its own broker, whatever scope pairs it.
267 #[cfg(feature = "testing")]
268 pairs_against(connected.as_ref());
269 source.pair(connected.as_ref()).await
270}
271
272// The token is a policy for ANY connected broker: it ignores the scope's broker and pairs
273// against its own.
274impl<C, B2, S> PublishPolicy<C> for Bound<B2, S>
275where
276 C: ConnectedBroker,
277 B2: Broker + 'static,
278 S: PublishPolicy<Connected<B2>> + Send,
279{
280 type Live = S::Live;
281
282 async fn pair(self, _connected: &C) -> Result<Self::Live, PairError> {
283 pair_bound::<B2, S>(&self.slot, self.source).await
284 }
285
286 // The token's channel is the other broker's, so what describes it is the policy bound to
287 // that broker rather than the scope the token was included in. The channel's `servers` list
288 // is the one thing that does not follow: it names the scope's server, because the
289 // registration's label is what the runtime knows.
290 #[cfg(feature = "asyncapi")]
291 fn channel_bindings(&self, channel: &str) -> Bindings {
292 <S as PublishPolicy<Connected<B2>>>::channel_bindings(&self.source, channel)
293 }
294
295 #[cfg(feature = "asyncapi")]
296 fn operation_bindings(&self, channel: &str) -> Bindings {
297 <S as PublishPolicy<Connected<B2>>>::operation_bindings(&self.source, channel)
298 }
299
300 #[cfg(feature = "asyncapi")]
301 fn message_bindings(&self, channel: &str) -> Bindings {
302 <S as PublishPolicy<Connected<B2>>>::message_bindings(&self.source, channel)
303 }
304
305 #[cfg(feature = "asyncapi")]
306 fn reply_address_location(&self) -> Option<&'static str> {
307 <S as PublishPolicy<Connected<B2>>>::reply_address_location(&self.source)
308 }
309}
310
311#[cfg(all(test, feature = "memory", feature = "json"))]
312mod tests {
313 use futures::StreamExt;
314
315 use crate::memory::{MemoryBroker, MemoryPublish};
316 use crate::runtime::{AppInfo, RustStream};
317 use crate::{Broker, IncomingMessage, OutgoingMessage, Publisher, Subscriber};
318
319 /// Cloning a token duplicates the binding, not the broker: both clones pair against the one
320 /// instance the wrapper registered.
321 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
322 async fn a_cloned_token_pairs_against_the_same_broker() {
323 let bindable = MemoryBroker::new().bindable();
324 let mut inbox = bindable.broker().subscribe("bound.out");
325 let token = bindable.bind(MemoryPublish);
326 let spare = token.clone();
327
328 assert!(
329 format!("{bindable:?}").contains("MemoryBroker"),
330 "the wrapper names the broker it holds",
331 );
332 assert!(
333 format!("{token:?}").contains("MemoryPublish"),
334 "the token names the policy it carries",
335 );
336
337 let app =
338 RustStream::new(AppInfo::new("bound", "0.1.0")).with_broker(bindable, |_scope| {});
339 let running = app.start().await.expect("startup failed");
340
341 for (token, payload) in [(token, b"one"), (spare, b"two")] {
342 let publisher = token.live().await.expect("pairing after startup failed");
343 publisher
344 .publish(OutgoingMessage::new("bound.out", payload), None)
345 .await
346 .expect("publish through the paired token failed");
347 }
348
349 let mut stream = std::pin::pin!(inbox.stream());
350 let mut seen = Vec::new();
351 for _ in 0..2 {
352 let msg = stream
353 .next()
354 .await
355 .expect("delivery missing")
356 .expect("memory subscriber never errors");
357 seen.push(msg.payload().to_vec());
358 msg.ack().await.expect("ack failed");
359 }
360 assert_eq!(
361 seen,
362 vec![b"one".to_vec(), b"two".to_vec()],
363 "both clones reach the broker the wrapper registered",
364 );
365
366 running.shutdown().await.expect("shutdown failed");
367 }
368}