prospero_core/distributed_bus.rs
1//! Clustered live distribution via Postgres `LISTEN/NOTIFY` — the doorbell.
2//!
3//! The owner replica appends an event to Postgres (durable), then
4//! `NOTIFY prospero_events '<stream_key>:<seq>'`. The payload is a *pointer*,
5//! not the event, so it sidesteps NOTIFY's ~8 KB cap and keeps Postgres the
6//! single source of truth. A subscriber on any replica holds one `LISTEN`
7//! connection; on each doorbell for its stream it `replay`s the delta from the
8//! durable store. See the topology design spec §3.2; clustered mode is
9//! durable-first (§4) — the live tail carries only what is durable.
10//!
11//! **Scaling note:** each `subscribe` holds one dedicated `LISTEN` connection
12//! from this bus's pool for the subscription's lifetime, so live subscribers and
13//! pool connections grow 1:1. The daemon gives the bus its own pool (the
14//! clustered seams use a pool each today; a single shared pool is a future
15//! tuning option), sized by [`DistributedBus::connect`] for the expected
16//! concurrent SSE fan-out. If that ceiling is ever hit, multiplex one listener
17//! across streams — deferred until measured.
18
19use std::sync::Arc;
20
21use sqlx::postgres::{PgListener, PgPool, PgPoolOptions};
22
23use crate::Result;
24use crate::bus::{BusEvent, BusSubscription, EventBus};
25use crate::event::FleetEvent;
26use crate::store::Store;
27
28/// Postgres `NOTIFY` channel for the event doorbell.
29const CHANNEL: &str = "prospero_events";
30
31/// Clustered `EventBus`: a doorbell over the durable store (spec §3.2).
32pub struct DistributedBus {
33 pool: PgPool,
34 store: Arc<dyn Store>,
35}
36
37impl DistributedBus {
38 /// Build a bus on its own pool. Each live subscription pins one connection
39 /// for its `LISTEN`, so the pool is sized above sqlx's default of 10 to
40 /// allow a useful SSE fan-out before `subscribe`/`publish` start queuing on
41 /// the pool; raise it further (or share a pool) for high-fan-out deployments.
42 pub async fn connect(url: &str, store: Arc<dyn Store>) -> Result<Self> {
43 let pool = PgPoolOptions::new()
44 .max_connections(32)
45 .connect(url)
46 .await
47 .map_err(|e| crate::error::CoreError::Store(format!("connecting to postgres: {e}")))?;
48 Ok(Self { pool, store })
49 }
50
51 /// Build a bus on an existing pool (shared-pool wiring — size the pool for
52 /// the SSE fan-out, since each subscription pins a `LISTEN` connection).
53 pub fn new(pool: PgPool, store: Arc<dyn Store>) -> Self {
54 Self { pool, store }
55 }
56
57 /// [`EventBus::subscribe_all`], but the `LISTEN` is established **before**
58 /// this returns rather than lazily on the stream's first poll (#132).
59 ///
60 /// The lazy form cannot tell a caller when it is safe to publish. A
61 /// doorbell rung in the gap is simply not heard, and because the per-key
62 /// high-water starts at 0, the miss is only repaired by the *next* doorbell
63 /// for that key — which, for a stream that just went quiet, may never come.
64 /// Ringing repeatedly and hoping is what the #132 test was doing, and it
65 /// still starved to zero deliveries on a contended runner.
66 ///
67 /// Connection and `LISTEN` failures surface here as an `Err` instead of
68 /// silently ending the stream, which is the other thing the lazy form
69 /// cannot express.
70 pub async fn subscribe_all_ready(&self) -> Result<BusSubscription> {
71 let mut listener = PgListener::connect_with(&self.pool)
72 .await
73 .map_err(|e| crate::error::CoreError::Store(format!("PgListener connect: {e}")))?;
74 listener
75 .listen(CHANNEL)
76 .await
77 .map_err(|e| crate::error::CoreError::Store(format!("LISTEN {CHANNEL}: {e}")))?;
78 Ok(Box::pin(Self::drain_doorbell(listener, self.store.clone())))
79 }
80
81 /// The unfiltered doorbell loop, over an already-established `LISTEN`.
82 ///
83 /// Shared by [`EventBus::subscribe_all`] and [`Self::subscribe_all_ready`]
84 /// so the two differ only in *when* they start listening, never in what
85 /// they deliver.
86 ///
87 /// Unlike `subscribe` (one stream, one `last_seq`), an unfiltered doorbell
88 /// can arrive for any stream key, so the high-water mark is tracked per key,
89 /// seeded at 0 the same way and for the same reason (see `subscribe`'s
90 /// comment): correctness over a late-seed race, at the cost of one deduped
91 /// re-read per stream on its first doorbell.
92 fn drain_doorbell(
93 mut listener: PgListener,
94 store: Arc<dyn Store>,
95 ) -> impl futures::Stream<Item = BusEvent> + Send {
96 async_stream::stream! {
97 let mut last_seq: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
98
99 loop {
100 let notif = match listener.recv().await {
101 Ok(n) => n,
102 Err(e) => {
103 tracing::warn!(target: "prospero_bus", error = %e, "LISTEN recv failed");
104 break;
105 }
106 };
107 let Some((nkey, _seq)) = notif.payload().rsplit_once(':') else {
108 continue;
109 };
110 let from = last_seq.get(nkey).copied().unwrap_or(0) + 1;
111 match store.replay(nkey, from).await {
112 Ok(events) => {
113 for ev in events {
114 let cur = last_seq.entry(nkey.to_string()).or_insert(0);
115 if ev.seq <= *cur {
116 continue;
117 }
118 *cur = ev.seq;
119 yield BusEvent::Event(ev);
120 }
121 }
122 Err(e) => {
123 tracing::warn!(target: "prospero_bus", error = %e, "doorbell replay failed");
124 }
125 }
126 }
127 }
128 }
129}
130
131impl EventBus for DistributedBus {
132 fn publish(&self, event: FleetEvent) {
133 // Doorbell only: the event is already (best-effort) durable in Postgres.
134 // Payload is a pointer "<stream_key>:<seq>", never the event itself.
135 // Fire-and-forget (best-effort vs. the durable store, ADR-0004); a lost
136 // NOTIFY is recovered by the next doorbell's delta replay or the
137 // poll-fallback escape hatch (spec §3.2).
138 let pool = self.pool.clone();
139 let payload = format!("{}:{}", event.stream_key(), event.seq);
140 tokio::spawn(async move {
141 if let Err(e) = sqlx::query("SELECT pg_notify($1, $2)")
142 .bind(CHANNEL)
143 .bind(&payload)
144 .execute(&pool)
145 .await
146 {
147 tracing::warn!(target: "prospero_bus", error = %e, "pg_notify failed");
148 }
149 });
150 }
151
152 fn subscribe(&self, stream_key: &str) -> BusSubscription {
153 let pool = self.pool.clone();
154 let store = self.store.clone();
155 let key = stream_key.to_string();
156 Box::pin(async_stream::stream! {
157 // One dedicated LISTEN connection per subscriber.
158 let mut listener = match PgListener::connect_with(&pool).await {
159 Ok(l) => l,
160 Err(e) => {
161 tracing::warn!(target: "prospero_bus", error = %e, "PgListener connect failed");
162 return;
163 }
164 };
165 if let Err(e) = listener.listen(CHANNEL).await {
166 tracing::warn!(target: "prospero_bus", error = %e, "LISTEN failed");
167 return;
168 }
169
170 // Seed at 0, so the FIRST doorbell replays the whole durable stream
171 // and the consumer's `seq`-dedup drops the history overlap. Seeding
172 // from a late `high_water()` read instead would be a gap: this
173 // subscription's LISTEN + seed run lazily on first poll — AFTER the
174 // SSE handler has already read history — so an event appended in that
175 // window would be below a late high-water and never replayed by any
176 // later doorbell, yet also absent from the history snapshot. Seeding
177 // at 0 makes delivery independent of the subscribe-vs-history race
178 // (the cost is one deduped re-read of the stream on the first
179 // doorbell; subsequent doorbells replay only the delta as `last_seq`
180 // advances). A floor passed in by the consumer could bound this, but
181 // that is a future optimization, not a correctness need.
182 let mut last_seq = 0u64;
183
184 loop {
185 let notif = match listener.recv().await {
186 Ok(n) => n,
187 // `PgListener::recv` re-connects and re-LISTENs internally on a
188 // dropped connection, so an `Err` here is a terminal listener
189 // failure: end the subscription. The SSE client is responsible
190 // for re-subscribing (and replays history on reconnect), so no
191 // durable event is lost — consistent with the best-effort
192 // doorbell posture (§3.2).
193 Err(e) => {
194 tracing::warn!(target: "prospero_bus", error = %e, "LISTEN recv failed");
195 break;
196 }
197 };
198 // Payload is "<stream_key>:<seq>"; stream keys may contain ':'
199 // (e.g. "repo:foo"), so split on the LAST colon.
200 let Some((nkey, _seq)) = notif.payload().rsplit_once(':') else {
201 continue;
202 };
203 if nkey != key {
204 continue; // doorbell for another stream
205 }
206 // Doorbell rung: replay the durable delta and advance.
207 match store.replay(&key, last_seq + 1).await {
208 Ok(events) => {
209 for ev in events {
210 if ev.seq <= last_seq {
211 continue;
212 }
213 last_seq = ev.seq;
214 yield BusEvent::Event(ev);
215 }
216 }
217 Err(e) => {
218 tracing::warn!(target: "prospero_bus", error = %e, "doorbell replay failed");
219 }
220 }
221 }
222 })
223 }
224
225 fn subscribe_all(&self) -> BusSubscription {
226 let pool = self.pool.clone();
227 let store = self.store.clone();
228 Box::pin(async_stream::stream! {
229 let mut listener = match PgListener::connect_with(&pool).await {
230 Ok(l) => l,
231 Err(e) => {
232 tracing::warn!(target: "prospero_bus", error = %e, "PgListener connect failed");
233 return;
234 }
235 };
236 if let Err(e) = listener.listen(CHANNEL).await {
237 tracing::warn!(target: "prospero_bus", error = %e, "LISTEN failed");
238 return;
239 }
240 // Same machinery as `subscribe_all_ready` from here on — only the
241 // moment `LISTEN` is established differs (lazily, on this first
242 // poll, versus before the call returns).
243 let inner = DistributedBus::drain_doorbell(listener, store);
244 futures::pin_mut!(inner);
245 while let Some(item) = futures::StreamExt::next(&mut inner).await {
246 yield item;
247 }
248 })
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::event::EventKind;
256 use crate::postgres_store::PostgresStore;
257 use std::time::Duration;
258 use tokio_stream::StreamExt;
259
260 fn ev(seq: u64, agent: &str) -> FleetEvent {
261 FleetEvent {
262 seq,
263 ts: "2026-06-18T00:00:00+00:00".into(),
264 repo: "r".into(),
265 agent_id: agent.into(),
266 kind: EventKind::AgentSpawned,
267 actor: None,
268 on_behalf_of: None,
269 }
270 }
271
272 /// A process-unique agent id so the two gated tests — which share one
273 /// persistent Postgres DB and run in parallel — never collide on a stream
274 /// key (their NOTIFY payloads target distinct streams, and `high_water` /
275 /// `replay` are stream-scoped). Avoids a global TRUNCATE that would wipe a
276 /// sibling test's rows mid-run.
277 fn unique_agent(tag: &str) -> String {
278 use std::sync::atomic::{AtomicU64, Ordering};
279 static N: AtomicU64 = AtomicU64::new(0);
280 let n = N.fetch_add(1, Ordering::Relaxed);
281 let nanos = std::time::SystemTime::now()
282 .duration_since(std::time::UNIX_EPOCH)
283 .map(|d| d.as_nanos())
284 .unwrap_or(0);
285 format!("{tag}-{nanos}-{n}")
286 }
287
288 /// Serialize the Postgres-gated bus tests against each other. They share one
289 /// database and — critically — one NOTIFY channel: `subscribe_all` replays
290 /// from the store for *every* notification on that channel, so a sibling
291 /// test publishing concurrently floods this channel and, under the full
292 /// suite's CPU pressure, can starve `subscribe_all`'s doorbell loop until it
293 /// times out. Distinct `unique_agent` keys keep their *data* from colliding;
294 /// this guard keeps their *doorbell traffic* from colliding. Held across
295 /// awaits, so it must be a `tokio` mutex. Each test takes it right after the
296 /// `DATABASE_URL` guard (an unset-DB skip never contends).
297 static BUS_TEST_SERIAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
298
299 // Multi-threaded runtime: these tests spawn a consumer task and the bus
300 // spawns a pg_notify task per publish, all of which must make progress
301 // concurrently with the publish loop. On the default current-thread runtime
302 // they contend cooperatively on one thread and — under the slow, instrumented
303 // coverage build especially — can starve the listener so no doorbell is ever
304 // processed. Real threads keep the listener draining while we publish.
305 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
306 async fn doorbell_delivers_a_live_event_to_a_subscriber() {
307 let Ok(url) = std::env::var("DATABASE_URL") else {
308 eprintln!("SKIP doorbell_delivers_a_live_event_to_a_subscriber: DATABASE_URL unset");
309 return;
310 };
311 let _serial = BUS_TEST_SERIAL.lock().await;
312
313 let store = PostgresStore::connect(&url).await.unwrap();
314 let store: Arc<dyn Store> = Arc::new(store);
315 let bus = DistributedBus::connect(&url, store.clone()).await.unwrap();
316
317 let agent = unique_agent("agent-deliver");
318 let mut sub = bus.subscribe(&agent);
319
320 // The subscriber establishes its LISTEN connection lazily on first poll;
321 // that moment isn't observable, and a single fixed sleep races it under a
322 // slow/instrumented build (e.g. coverage). So drive delivery with a
323 // bounded retry — append an event and ring the doorbell until the
324 // (now-live) listener replays it.
325 let recv =
326 tokio::spawn(
327 async move { tokio::time::timeout(Duration::from_secs(30), sub.next()).await },
328 );
329
330 // Keep nudging until the subscriber actually receives an event, NOT for a
331 // fixed number of tries: under the full suite's CPU pressure the lazy
332 // LISTEN can take many seconds to come up, and if the nudges stop before
333 // then, nothing is ever replayed. The cap (~25s of nudging) sits under
334 // the 30s recv timeout so a genuine hang still fails rather than hangs.
335 let mut delivered = None;
336 for seq in 1..=250u64 {
337 let e = ev(seq, &agent);
338 store.append(&e).await.unwrap();
339 bus.publish(e);
340 tokio::time::sleep(Duration::from_millis(100)).await;
341 if recv.is_finished() {
342 delivered = Some(recv.await.unwrap().expect("doorbell timed out"));
343 break;
344 }
345 }
346
347 match delivered.expect("subscriber never received a doorbell event") {
348 Some(BusEvent::Event(ev)) => assert_eq!(ev.agent_id, agent),
349 other => panic!("expected a live event, got {other:?}"),
350 }
351 }
352
353 /// Regression for the cross-replica subscribe-window gap: the subscriber's
354 /// LISTEN + seed run lazily on first poll, so an event already durable
355 /// BEFORE that first doorbell (e.g. appended on the owner replica between a
356 /// reader replica's history read and its first poll) must still be
357 /// delivered. Seeding `last_seq` from 0 replays it; a late `high_water` seed
358 /// would skip it forever.
359 // Multi-threaded runtime: these tests spawn a consumer task and the bus
360 // spawns a pg_notify task per publish, all of which must make progress
361 // concurrently with the publish loop. On the default current-thread runtime
362 // they contend cooperatively on one thread and — under the slow, instrumented
363 // coverage build especially — can starve the listener so no doorbell is ever
364 // processed. Real threads keep the listener draining while we publish.
365 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
366 async fn delivers_an_event_that_predates_the_first_doorbell() {
367 let Ok(url) = std::env::var("DATABASE_URL") else {
368 eprintln!(
369 "SKIP delivers_an_event_that_predates_the_first_doorbell: DATABASE_URL unset"
370 );
371 return;
372 };
373 let _serial = BUS_TEST_SERIAL.lock().await;
374
375 let store = PostgresStore::connect(&url).await.unwrap();
376 let store: Arc<dyn Store> = Arc::new(store);
377 let bus = DistributedBus::connect(&url, store.clone()).await.unwrap();
378
379 let agent = unique_agent("agent-predate");
380 // Append the "window" event BEFORE the subscriber's first poll.
381 let early = ev(1, &agent);
382 store.append(&early).await.unwrap();
383
384 let mut sub = bus.subscribe(&agent);
385 let recv =
386 tokio::spawn(
387 async move { tokio::time::timeout(Duration::from_secs(30), sub.next()).await },
388 );
389
390 // Ring the doorbell until the (now-live) listener replays the delta;
391 // re-NOTIFY is idempotent (replay starts from last_seq+1 = 1). Keep
392 // nudging until it's delivered, not for a fixed window: under the full
393 // suite's CPU pressure the lazy LISTEN can come up well after a short
394 // fixed window would have stopped nudging, leaving nothing to replay it.
395 let mut delivered = None;
396 for _ in 0..250 {
397 bus.publish(early.clone());
398 tokio::time::sleep(Duration::from_millis(100)).await;
399 if recv.is_finished() {
400 delivered = Some(recv.await.unwrap().expect("doorbell timed out"));
401 break;
402 }
403 }
404
405 match delivered.expect("never delivered the pre-doorbell event") {
406 Some(BusEvent::Event(ev)) => {
407 assert_eq!(ev.agent_id, agent);
408 assert_eq!(
409 ev.seq, 1,
410 "the event appended before the first doorbell must arrive"
411 );
412 }
413 other => panic!("expected the early event, got {other:?}"),
414 }
415 }
416
417 // Multi-threaded runtime: these tests spawn a consumer task and the bus
418 // spawns a pg_notify task per publish, all of which must make progress
419 // concurrently with the publish loop. On the default current-thread runtime
420 // they contend cooperatively on one thread and — under the slow, instrumented
421 // coverage build especially — can starve the listener so no doorbell is ever
422 // processed. Real threads keep the listener draining while we publish.
423 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
424 async fn doorbell_ignores_other_streams() {
425 let Ok(url) = std::env::var("DATABASE_URL") else {
426 eprintln!("SKIP doorbell_ignores_other_streams: DATABASE_URL unset");
427 return;
428 };
429 let _serial = BUS_TEST_SERIAL.lock().await;
430
431 let store = PostgresStore::connect(&url).await.unwrap();
432 let store: Arc<dyn Store> = Arc::new(store);
433 let bus = DistributedBus::connect(&url, store.clone()).await.unwrap();
434
435 let ours = unique_agent("agent-ours");
436 let theirs = unique_agent("agent-theirs");
437 let mut sub = bus.subscribe(&ours);
438 let recv = tokio::spawn(async move {
439 tokio::time::timeout(Duration::from_millis(800), sub.next()).await
440 });
441 tokio::time::sleep(Duration::from_millis(400)).await;
442
443 // An event on a DIFFERENT stream: its doorbell must not wake our sub.
444 let other = ev(1, &theirs);
445 store.append(&other).await.unwrap();
446 bus.publish(other);
447
448 assert!(
449 recv.await.unwrap().is_err(),
450 "should have timed out (no event on our stream)"
451 );
452 }
453
454 /// Ring the doorbell for `agent` **synchronously**, so the test controls
455 /// exactly when the NOTIFY lands relative to a subscription's `LISTEN`.
456 /// `DistributedBus::publish` spawns its notify, which is fine in production
457 /// but makes ordering unobservable in a test.
458 async fn ring(bus: &DistributedBus, agent: &str, seq: u64) {
459 sqlx::query("SELECT pg_notify($1, $2)")
460 .bind(CHANNEL)
461 .bind(format!("{agent}:{seq}"))
462 .execute(&bus.pool)
463 .await
464 .expect("pg_notify");
465 }
466
467 /// `subscribe_all` (unlike `subscribe`) has no stream-key filter, and
468 /// tracks a `last_seq` per discovered key rather than one fixed key. This
469 /// is the opposite assertion from `doorbell_ignores_other_streams`: two
470 /// DIFFERENT streams must both reach one unfiltered subscription, and
471 /// ringing either doorbell repeatedly must not re-deliver an already-seen
472 /// event on either key (per-key high-water advances independently).
473 ///
474 /// #132: this used to be `#[ignore]`d. `subscribe_all` returns a lazy
475 /// stream whose `LISTEN` is only established on the first poll, so a test
476 /// had no way to know when it was safe to ring. The old version worked
477 /// around that by re-ringing both doorbells up to 440 times over 44s and
478 /// waiting up to 45s — which still starved to zero deliveries on an
479 /// oversubscribed CI runner, so it was pulled from the gate entirely.
480 ///
481 /// `subscribe_all_ready` removes the guess: `LISTEN` is established before
482 /// it returns, so one ring per stream is enough and the assertion is about
483 /// delivery rather than about how long we were willing to wait.
484 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
485 async fn subscribe_all_delivers_events_from_multiple_streams() {
486 let Ok(url) = std::env::var("DATABASE_URL") else {
487 eprintln!(
488 "SKIP subscribe_all_delivers_events_from_multiple_streams: DATABASE_URL unset"
489 );
490 return;
491 };
492 let _serial = BUS_TEST_SERIAL.lock().await;
493
494 let store = PostgresStore::connect(&url).await.unwrap();
495 let store: Arc<dyn Store> = Arc::new(store);
496 let bus = DistributedBus::connect(&url, store.clone()).await.unwrap();
497
498 let agent_a = unique_agent("agent-all-a");
499 let agent_b = unique_agent("agent-all-b");
500
501 store.append(&ev(1, &agent_a)).await.unwrap();
502 store.append(&ev(1, &agent_b)).await.unwrap();
503
504 // Established `LISTEN` before the first ring: both doorbells below are
505 // rung at a listener that is already on the channel.
506 let mut sub = bus.subscribe_all_ready().await.unwrap();
507 ring(&bus, &agent_a, 1).await;
508 ring(&bus, &agent_b, 1).await;
509
510 // `subscribe_all` is global and unfiltered by design, so under a shared
511 // test database it also observes events from *sibling* tests running
512 // concurrently (their own `unique_agent(...)` streams). This test is
513 // only about OUR two streams: consume until both have arrived, skipping
514 // any foreign stream key (and lag signals), while asserting neither of
515 // ours is ever delivered twice.
516 let mut seen = std::collections::HashSet::new();
517 while seen.len() < 2 {
518 match tokio::time::timeout(Duration::from_secs(30), sub.next()).await {
519 Ok(Some(BusEvent::Event(delivered))) => {
520 if delivered.agent_id == agent_a || delivered.agent_id == agent_b {
521 assert!(
522 seen.insert(delivered.agent_id.clone()),
523 "duplicate delivery for stream {} (per-key high-water not advancing)",
524 delivered.agent_id
525 );
526 }
527 // Foreign stream keys (concurrent tests) are expected — skip them.
528 }
529 // Lag signals aren't a delivery of one of our streams — keep waiting.
530 Ok(Some(BusEvent::Lagged(_))) => {}
531 Ok(None) => panic!("subscription closed before both streams arrived"),
532 Err(_) => panic!("doorbell timed out; saw {seen:?} of our two streams"),
533 }
534 }
535
536 assert_eq!(
537 seen.len(),
538 2,
539 "subscribe_all must deliver events from both streams, unfiltered"
540 );
541 }
542
543 /// #132: the guarantee the test above rests on, asserted on its own.
544 ///
545 /// Exactly **one** doorbell is rung, after `subscribe_all_ready` returns
546 /// and never again. That is what the lazy `subscribe_all` cannot survive:
547 /// its `LISTEN` is established on the first poll, which here happens after
548 /// the ring, so the notification is already gone and — with no later
549 /// doorbell for the key to trigger the catch-up replay — the event is never
550 /// delivered. This is the exact hazard the old test's 440-ring loop existed
551 /// to paper over.
552 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
553 async fn subscribe_all_ready_is_listening_before_it_returns() {
554 let Ok(url) = std::env::var("DATABASE_URL") else {
555 eprintln!(
556 "SKIP subscribe_all_ready_is_listening_before_it_returns: DATABASE_URL unset"
557 );
558 return;
559 };
560 let _serial = BUS_TEST_SERIAL.lock().await;
561
562 let store = PostgresStore::connect(&url).await.unwrap();
563 let store: Arc<dyn Store> = Arc::new(store);
564 let bus = DistributedBus::connect(&url, store.clone()).await.unwrap();
565
566 let agent = unique_agent("agent-ready");
567 store.append(&ev(1, &agent)).await.unwrap();
568
569 let mut sub = bus.subscribe_all_ready().await.unwrap();
570 ring(&bus, &agent, 1).await;
571
572 loop {
573 match tokio::time::timeout(Duration::from_secs(30), sub.next()).await {
574 Ok(Some(BusEvent::Event(delivered))) if delivered.agent_id == agent => break,
575 Ok(Some(_)) => {}
576 Ok(None) => panic!("subscription closed before our event arrived"),
577 Err(_) => panic!(
578 "a single doorbell rung after subscribe_all_ready was not delivered — \
579 the subscription was not listening when it returned"
580 ),
581 }
582 }
583 }
584}