Skip to main content

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}