Skip to main content

cdk_common/pub_sub/
remote_consumer.rs

1//! Pub-sub consumer
2//!
3//! Consumers are designed to connect to a producer, through a transport, and subscribe to events.
4use std::collections::{HashMap, VecDeque};
5use std::sync::atomic::AtomicBool;
6use std::sync::Arc;
7use std::time::Duration;
8
9use parking_lot::RwLock;
10use tokio::sync::mpsc;
11use tokio::time::{sleep, Instant};
12
13use super::subscriber::{ActiveSubscription, SubscriptionRequest};
14use super::{Error, Event, Pubsub, Spec};
15use crate::task::spawn;
16
17const STREAM_CONNECTION_BACKOFF: Duration = Duration::from_millis(2_000);
18
19const STREAM_CONNECTION_MAX_BACKOFF: Duration = Duration::from_millis(30_000);
20
21const INTERNAL_POLL_SIZE: usize = 1_000;
22
23const POLL_SLEEP: Duration = Duration::from_millis(2_000);
24
25struct UniqueSubscription<S>
26where
27    S: Spec,
28{
29    name: S::SubscriptionId,
30    total_subscribers: usize,
31}
32
33type UniqueSubscriptions<S> = Arc<RwLock<HashMap<<S as Spec>::Topic, UniqueSubscription<S>>>>;
34
35type ActiveSubscriptions<S> =
36    RwLock<HashMap<Arc<<S as Spec>::SubscriptionId>, Vec<<S as Spec>::Topic>>>;
37
38type CacheEvent<S> = HashMap<<<S as Spec>::Event as Event>::Topic, <S as Spec>::Event>;
39
40/// Subscription consumer
41#[allow(missing_debug_implementations)]
42pub struct Consumer<T>
43where
44    T: Transport + 'static,
45{
46    transport: T,
47    inner_pubsub: Arc<Pubsub<T::Spec>>,
48    remote_subscriptions: UniqueSubscriptions<T::Spec>,
49    subscriptions: ActiveSubscriptions<T::Spec>,
50    stream_ctrl: RwLock<Option<mpsc::Sender<StreamCtrl<T::Spec>>>>,
51    still_running: AtomicBool,
52    prefer_polling: bool,
53    /// Cached events
54    ///
55    /// The cached events are useful to share events. The cache is automatically evicted it is
56    /// disconnected from the remote source, meaning the cache is only active while there is an
57    /// active subscription to the remote source, and it remembers the latest event.
58    cached_events: Arc<RwLock<CacheEvent<T::Spec>>>,
59}
60
61/// Remote consumer
62#[allow(missing_debug_implementations)]
63pub struct RemoteActiveConsumer<T>
64where
65    T: Transport + 'static,
66{
67    inner: ActiveSubscription<T::Spec>,
68    previous_messages: VecDeque<<T::Spec as Spec>::Event>,
69    consumer: Arc<Consumer<T>>,
70}
71
72impl<T> RemoteActiveConsumer<T>
73where
74    T: Transport + 'static,
75{
76    /// Receives the next event
77    pub async fn recv(&mut self) -> Option<<T::Spec as Spec>::Event> {
78        if let Some(event) = self.previous_messages.pop_front() {
79            Some(event)
80        } else {
81            self.inner.recv().await
82        }
83    }
84
85    /// Try receive an event or return None right away
86    pub fn try_recv(&mut self) -> Option<<T::Spec as Spec>::Event> {
87        if let Some(event) = self.previous_messages.pop_front() {
88            Some(event)
89        } else {
90            self.inner.try_recv()
91        }
92    }
93
94    /// Get the subscription name
95    pub fn name(&self) -> &<T::Spec as Spec>::SubscriptionId {
96        self.inner.name()
97    }
98}
99
100impl<T> Drop for RemoteActiveConsumer<T>
101where
102    T: Transport + 'static,
103{
104    fn drop(&mut self) {
105        let _ = self.consumer.unsubscribe(self.name().clone());
106    }
107}
108
109/// Struct to relay events from Poll and Streams from the external subscription to the local
110/// subscribers
111#[allow(missing_debug_implementations)]
112pub struct InternalRelay<S>
113where
114    S: Spec + 'static,
115{
116    inner: Arc<Pubsub<S>>,
117    remote_subscriptions: UniqueSubscriptions<S>,
118    cached_events: Arc<RwLock<CacheEvent<S>>>,
119}
120
121impl<S> InternalRelay<S>
122where
123    S: Spec + 'static,
124{
125    /// Relay a remote event locally
126    pub fn send<X>(&self, event: X)
127    where
128        X: Into<S::Event>,
129    {
130        let event = event.into();
131
132        {
133            let active_topics = self.remote_subscriptions.read();
134            let mut cached_events = self.cached_events.write();
135
136            for topic in event.get_topics() {
137                if active_topics.contains_key(&topic) {
138                    cached_events.insert(topic, event.clone());
139                }
140            }
141        }
142
143        self.inner.publish(event);
144    }
145}
146
147impl<T> Consumer<T>
148where
149    T: Transport + 'static,
150{
151    /// Creates a new instance
152    pub fn new(
153        transport: T,
154        prefer_polling: bool,
155        context: <T::Spec as Spec>::Context,
156    ) -> Arc<Self> {
157        let this = Arc::new(Self {
158            transport,
159            prefer_polling,
160            inner_pubsub: Arc::new(Pubsub::new(T::Spec::new_instance(context))),
161            subscriptions: Default::default(),
162            remote_subscriptions: Default::default(),
163            stream_ctrl: RwLock::new(None),
164            cached_events: Default::default(),
165            still_running: true.into(),
166        });
167
168        spawn(Self::stream(this.clone()));
169
170        this
171    }
172
173    async fn stream(instance: Arc<Self>) {
174        let mut stream_supported = true;
175        let mut poll_supported = true;
176
177        let mut backoff = STREAM_CONNECTION_BACKOFF;
178        let mut retry_at = None;
179
180        loop {
181            if (!stream_supported && !poll_supported)
182                || !instance
183                    .still_running
184                    .load(std::sync::atomic::Ordering::Relaxed)
185            {
186                break;
187            }
188
189            if instance.remote_subscriptions.read().is_empty() {
190                sleep(Duration::from_millis(100)).await;
191                continue;
192            }
193
194            if stream_supported
195                && !instance.prefer_polling
196                && retry_at
197                    .map(|retry_at| retry_at < Instant::now())
198                    .unwrap_or(true)
199            {
200                let (sender, receiver) = mpsc::channel(INTERNAL_POLL_SIZE);
201
202                {
203                    *instance.stream_ctrl.write() = Some(sender);
204                }
205
206                let current_subscriptions = {
207                    instance
208                        .remote_subscriptions
209                        .read()
210                        .iter()
211                        .map(|(key, name)| (name.name.clone(), key.clone()))
212                        .collect::<Vec<_>>()
213                };
214
215                if let Err(err) = instance
216                    .transport
217                    .stream(
218                        receiver,
219                        current_subscriptions,
220                        InternalRelay {
221                            inner: instance.inner_pubsub.clone(),
222                            remote_subscriptions: instance.remote_subscriptions.clone(),
223                            cached_events: instance.cached_events.clone(),
224                        },
225                    )
226                    .await
227                {
228                    if matches!(&err, Error::NotSupported | Error::Terminal(_)) {
229                        stream_supported = false;
230                    } else {
231                        retry_at = Some(Instant::now() + backoff);
232                        backoff = backoff.saturating_mul(2).min(STREAM_CONNECTION_MAX_BACKOFF);
233                    }
234                    tracing::error!("Long connection failed with error {:?}", err);
235                } else {
236                    backoff = STREAM_CONNECTION_BACKOFF;
237                }
238
239                // remove sender to stream, as there is no stream
240                let _ = instance.stream_ctrl.write().take();
241            }
242
243            if poll_supported {
244                let current_subscriptions = {
245                    instance
246                        .remote_subscriptions
247                        .read()
248                        .iter()
249                        .map(|(key, name)| (name.name.clone(), key.clone()))
250                        .collect::<Vec<_>>()
251                };
252
253                if let Err(err) = instance
254                    .transport
255                    .poll(
256                        current_subscriptions,
257                        InternalRelay {
258                            inner: instance.inner_pubsub.clone(),
259                            remote_subscriptions: instance.remote_subscriptions.clone(),
260                            cached_events: instance.cached_events.clone(),
261                        },
262                    )
263                    .await
264                {
265                    if matches!(&err, Error::NotSupported | Error::Terminal(_)) {
266                        poll_supported = false;
267                    }
268                    tracing::error!("Polling failed with error {:?}", err);
269                }
270
271                sleep(POLL_SLEEP).await;
272            }
273        }
274    }
275
276    /// Unsubscribe from a topic, this is called automatically when RemoteActiveSubscription<T> goes
277    /// out of scope
278    fn unsubscribe(
279        self: &Arc<Self>,
280        subscription_name: <T::Spec as Spec>::SubscriptionId,
281    ) -> Result<(), Error> {
282        let topics = self
283            .subscriptions
284            .write()
285            .remove(&subscription_name)
286            .ok_or(Error::NoSubscription)?;
287
288        let mut remote_subscriptions = self.remote_subscriptions.write();
289
290        for topic in topics {
291            let mut remote_subscription =
292                if let Some(remote_subscription) = remote_subscriptions.remove(&topic) {
293                    remote_subscription
294                } else {
295                    continue;
296                };
297
298            remote_subscription.total_subscribers =
299                remote_subscription.total_subscribers.saturating_sub(1);
300
301            if remote_subscription.total_subscribers == 0 {
302                let mut cached_events = self.cached_events.write();
303
304                cached_events.remove(&topic);
305
306                self.message_to_stream(StreamCtrl::Unsubscribe(remote_subscription.name.clone()))?;
307            } else {
308                remote_subscriptions.insert(topic, remote_subscription);
309            }
310        }
311
312        if remote_subscriptions.is_empty() {
313            self.cached_events.write().clear();
314            self.message_to_stream(StreamCtrl::Stop)?;
315        }
316
317        Ok(())
318    }
319
320    #[inline(always)]
321    fn message_to_stream(&self, message: StreamCtrl<T::Spec>) -> Result<(), Error> {
322        let to_stream = self.stream_ctrl.read();
323
324        if let Some(to_stream) = to_stream.as_ref() {
325            Ok(to_stream.try_send(message)?)
326        } else {
327            Ok(())
328        }
329    }
330
331    /// Creates a subscription
332    ///
333    /// The subscriptions have two parts:
334    ///
335    /// 1. Will create the subscription to the remote Pubsub service, Any events will be moved to
336    ///    the internal pubsub
337    ///
338    /// 2. The internal subscription to the inner Pubsub. Because all subscriptions are going the
339    ///    transport, once events matches subscriptions, the inner_pubsub will receive the message and
340    ///    broadcasat the event.
341    pub fn subscribe<I>(self: &Arc<Self>, request: I) -> Result<RemoteActiveConsumer<T>, Error>
342    where
343        I: SubscriptionRequest<
344            Topic = <T::Spec as Spec>::Topic,
345            SubscriptionId = <T::Spec as Spec>::SubscriptionId,
346        >,
347    {
348        let subscription_name = request.subscription_name();
349        let topics = request.try_get_topics()?;
350
351        let mut remote_subscriptions = self.remote_subscriptions.write();
352        let mut subscriptions = self.subscriptions.write();
353
354        if subscriptions.get(&subscription_name).is_some() {
355            return Err(Error::NoSubscription);
356        }
357
358        let mut previous_messages = Vec::new();
359        let cached_events = self.cached_events.read();
360
361        for topic in topics.iter() {
362            if let Some(subscription) = remote_subscriptions.get_mut(topic) {
363                subscription.total_subscribers += 1;
364
365                if let Some(v) = cached_events.get(topic).cloned() {
366                    previous_messages.push(v);
367                }
368            } else {
369                let internal_sub_name = self.transport.new_name();
370                remote_subscriptions.insert(
371                    topic.clone(),
372                    UniqueSubscription {
373                        total_subscribers: 1,
374                        name: internal_sub_name.clone(),
375                    },
376                );
377
378                // new subscription is created, so the connection worker should be notified
379                self.message_to_stream(StreamCtrl::Subscribe((internal_sub_name, topic.clone())))?;
380            }
381        }
382
383        subscriptions.insert(subscription_name, topics);
384        drop(subscriptions);
385
386        Ok(RemoteActiveConsumer {
387            inner: self.inner_pubsub.subscribe(request)?,
388            previous_messages: previous_messages.into(),
389            consumer: self.clone(),
390        })
391    }
392}
393
394impl<T> Drop for Consumer<T>
395where
396    T: Transport + 'static,
397{
398    fn drop(&mut self) {
399        self.still_running
400            .store(false, std::sync::atomic::Ordering::Release);
401        if let Some(to_stream) = self.stream_ctrl.read().as_ref() {
402            let _ = to_stream.try_send(StreamCtrl::Stop).inspect_err(|err| {
403                tracing::error!("Failed to send message LongPoll::Stop due to {err:?}")
404            });
405        }
406    }
407}
408
409/// Subscribe Message
410pub type SubscribeMessage<S> = (<S as Spec>::SubscriptionId, <S as Spec>::Topic);
411
412/// Messages sent from the [`Consumer`] to the [`Transport`] background loop.
413#[allow(missing_debug_implementations)]
414pub enum StreamCtrl<S>
415where
416    S: Spec + 'static,
417{
418    /// Add a subscription
419    Subscribe(SubscribeMessage<S>),
420    /// Desuscribe
421    Unsubscribe(S::SubscriptionId),
422    /// Exit the loop
423    Stop,
424}
425
426impl<S> Clone for StreamCtrl<S>
427where
428    S: Spec + 'static,
429{
430    fn clone(&self) -> Self {
431        match self {
432            Self::Subscribe(s) => Self::Subscribe(s.clone()),
433            Self::Unsubscribe(u) => Self::Unsubscribe(u.clone()),
434            Self::Stop => Self::Stop,
435        }
436    }
437}
438
439/// Transport abstracts how the consumer talks to the remote pubsub.
440///
441/// Implement this on your HTTP/WebSocket client. The transport is responsible for:
442/// - creating unique subscription names,
443/// - keeping a long connection via `stream` **or** performing on-demand `poll`,
444/// - forwarding remote events to `InternalRelay`.
445///
446/// ```ignore
447/// struct WsTransport { /* ... */ }
448/// #[async_trait::async_trait]
449/// impl Transport for WsTransport {
450///     type Topic = MyTopic;
451///     fn new_name(&self) -> <Self::Topic as Topic>::SubscriptionName { 0 }
452///     async fn stream(/* ... */) -> Result<(), Error> { Ok(()) }
453///     async fn poll(/* ... */) -> Result<(), Error> { Ok(()) }
454/// }
455/// ```
456#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
457#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
458pub trait Transport: Send + Sync {
459    /// Spec
460    type Spec: Spec;
461
462    /// Create a new subscription name
463    fn new_name(&self) -> <Self::Spec as Spec>::SubscriptionId;
464
465    /// Opens a persistent connection and continuously streams events.
466    /// For protocols that support server push (e.g. WebSocket, SSE).
467    async fn stream(
468        &self,
469        subscribe_changes: mpsc::Receiver<StreamCtrl<Self::Spec>>,
470        topics: Vec<SubscribeMessage<Self::Spec>>,
471        reply_to: InternalRelay<Self::Spec>,
472    ) -> Result<(), Error>;
473
474    /// Performs a one-shot fetch of any currently available events.
475    /// Called repeatedly by the consumer when streaming is not available.
476    async fn poll(
477        &self,
478        topics: Vec<SubscribeMessage<Self::Spec>>,
479        reply_to: InternalRelay<Self::Spec>,
480    ) -> Result<(), Error>;
481}
482
483#[cfg(test)]
484mod tests {
485    use std::sync::atomic::{AtomicUsize, Ordering};
486    use std::sync::Arc;
487
488    use tokio::sync::{mpsc, Mutex};
489    use tokio::time::{timeout, Duration};
490
491    use super::{
492        InternalRelay, RemoteActiveConsumer, StreamCtrl, SubscribeMessage, Transport,
493        INTERNAL_POLL_SIZE,
494    };
495    use crate::pub_sub::remote_consumer::Consumer;
496    use crate::pub_sub::test::{CustomPubSub, IndexTest, Message};
497    use crate::pub_sub::{Error, Spec, SubscriptionRequest};
498
499    // ===== Test Event/Topic types =====
500
501    #[derive(Clone, Debug)]
502    enum SubscriptionReq {
503        Foo(String, u64),
504        Bar(String, u64),
505    }
506
507    impl SubscriptionRequest for SubscriptionReq {
508        type Topic = IndexTest;
509
510        type SubscriptionId = String;
511
512        fn try_get_topics(&self) -> Result<Vec<Self::Topic>, Error> {
513            Ok(vec![match self {
514                SubscriptionReq::Foo(_, n) => IndexTest::Foo(*n),
515                SubscriptionReq::Bar(_, n) => IndexTest::Bar(*n),
516            }])
517        }
518
519        fn subscription_name(&self) -> Arc<Self::SubscriptionId> {
520            Arc::new(match self {
521                SubscriptionReq::Foo(n, _) => n.to_string(),
522                SubscriptionReq::Bar(n, _) => n.to_string(),
523            })
524        }
525    }
526
527    // ===== A controllable in-memory Transport used by tests =====
528
529    /// TestTransport relays messages from a broadcast channel to the Consumer via `InternalRelay`.
530    /// It also forwards Subscribe/Unsubscribe/Stop signals to an observer channel so tests can assert them.
531    struct TestTransport {
532        name_ctr: AtomicUsize,
533        // We forward all transport-loop control messages here so tests can observe them.
534        observe_ctrl_tx: mpsc::Sender<StreamCtrl<CustomPubSub>>,
535        // Whether stream / poll are supported.
536        support_long: bool,
537        support_poll: bool,
538        rx: Mutex<mpsc::Receiver<Message>>,
539    }
540
541    struct FailingStreamTransport {
542        name_ctr: AtomicUsize,
543        attempts: Arc<AtomicUsize>,
544        terminal: bool,
545    }
546
547    impl TestTransport {
548        fn new(
549            support_long: bool,
550            support_poll: bool,
551        ) -> (
552            Self,
553            mpsc::Sender<Message>,
554            mpsc::Receiver<StreamCtrl<CustomPubSub>>,
555        ) {
556            let (events_tx, rx) = mpsc::channel::<Message>(INTERNAL_POLL_SIZE);
557            let (observe_ctrl_tx, observe_ctrl_rx) =
558                mpsc::channel::<StreamCtrl<_>>(INTERNAL_POLL_SIZE);
559
560            let t = TestTransport {
561                name_ctr: AtomicUsize::new(1),
562                rx: Mutex::new(rx),
563                observe_ctrl_tx,
564                support_long,
565                support_poll,
566            };
567
568            (t, events_tx, observe_ctrl_rx)
569        }
570    }
571
572    impl FailingStreamTransport {
573        fn new(terminal: bool) -> (Self, Arc<AtomicUsize>) {
574            let attempts = Arc::new(AtomicUsize::new(0));
575            (
576                Self {
577                    name_ctr: AtomicUsize::new(1),
578                    attempts: attempts.clone(),
579                    terminal,
580                },
581                attempts,
582            )
583        }
584    }
585
586    #[async_trait::async_trait]
587    impl Transport for TestTransport {
588        type Spec = CustomPubSub;
589
590        fn new_name(&self) -> <Self::Spec as Spec>::SubscriptionId {
591            format!("sub-{}", self.name_ctr.fetch_add(1, Ordering::Relaxed))
592        }
593
594        async fn stream(
595            &self,
596            mut subscribe_changes: mpsc::Receiver<StreamCtrl<Self::Spec>>,
597            topics: Vec<SubscribeMessage<Self::Spec>>,
598            reply_to: InternalRelay<Self::Spec>,
599        ) -> Result<(), Error> {
600            if !self.support_long {
601                return Err(Error::NotSupported);
602            }
603
604            // Each invocation creates a fresh broadcast receiver
605            let mut rx = self.rx.lock().await;
606            let observe = self.observe_ctrl_tx.clone();
607
608            for topic in topics {
609                observe.try_send(StreamCtrl::Subscribe(topic)).unwrap();
610            }
611
612            loop {
613                tokio::select! {
614                    // Forward any control (Subscribe/Unsubscribe/Stop) messages so the test can assert them.
615                    Some(ctrl) = subscribe_changes.recv() => {
616                        observe.try_send(ctrl.clone()).unwrap();
617                        if matches!(ctrl, StreamCtrl::Stop) {
618                            break;
619                        }
620                    }
621                    // Relay external events into the inner pubsub
622                    Some(msg) = rx.recv() => {
623                        reply_to.send(msg);
624                    }
625                }
626            }
627
628            Ok(())
629        }
630
631        async fn poll(
632            &self,
633            _topics: Vec<SubscribeMessage<Self::Spec>>,
634            reply_to: InternalRelay<Self::Spec>,
635        ) -> Result<(), Error> {
636            if !self.support_poll {
637                return Err(Error::NotSupported);
638            }
639
640            // On each poll call, drain anything currently pending and return.
641            // (The Consumer calls this repeatedly; first call happens immediately.)
642            let mut rx = self.rx.lock().await;
643            // Non-blocking drain pass: try a few times without sleeping to keep tests snappy
644            for _ in 0..32 {
645                match rx.try_recv() {
646                    Ok(msg) => reply_to.send(msg),
647                    Err(mpsc::error::TryRecvError::Empty) => continue,
648                    Err(mpsc::error::TryRecvError::Disconnected) => break,
649                }
650            }
651            Ok(())
652        }
653    }
654
655    #[async_trait::async_trait]
656    impl Transport for FailingStreamTransport {
657        type Spec = CustomPubSub;
658
659        fn new_name(&self) -> <Self::Spec as Spec>::SubscriptionId {
660            format!("sub-{}", self.name_ctr.fetch_add(1, Ordering::Relaxed))
661        }
662
663        async fn stream(
664            &self,
665            _subscribe_changes: mpsc::Receiver<StreamCtrl<Self::Spec>>,
666            _topics: Vec<SubscribeMessage<Self::Spec>>,
667            _reply_to: InternalRelay<Self::Spec>,
668        ) -> Result<(), Error> {
669            self.attempts.fetch_add(1, Ordering::Relaxed);
670            match self.terminal {
671                true => Err(Error::Terminal("permanent failure".to_string())),
672                false => Err(Error::InternalStr("temporary failure".to_string())),
673            }
674        }
675
676        async fn poll(
677            &self,
678            _topics: Vec<SubscribeMessage<Self::Spec>>,
679            _reply_to: InternalRelay<Self::Spec>,
680        ) -> Result<(), Error> {
681            Ok(())
682        }
683    }
684
685    // ===== Helpers =====
686
687    async fn recv_next<T: Transport>(
688        sub: &mut RemoteActiveConsumer<T>,
689        dur_ms: u64,
690    ) -> Option<<T::Spec as Spec>::Event> {
691        timeout(Duration::from_millis(dur_ms), sub.recv())
692            .await
693            .ok()
694            .flatten()
695    }
696
697    async fn expect_ctrl(
698        rx: &mut mpsc::Receiver<StreamCtrl<CustomPubSub>>,
699        dur_ms: u64,
700        pred: impl Fn(&StreamCtrl<CustomPubSub>) -> bool,
701    ) -> StreamCtrl<CustomPubSub> {
702        timeout(Duration::from_millis(dur_ms), async {
703            loop {
704                if let Some(msg) = rx.recv().await {
705                    if pred(&msg) {
706                        break msg;
707                    }
708                }
709            }
710        })
711        .await
712        .expect("timed out waiting for control message")
713    }
714
715    async fn wait_for_attempts(attempts: &AtomicUsize, expected: usize) {
716        for _ in 0..20 {
717            if attempts.load(Ordering::Relaxed) >= expected {
718                return;
719            }
720            tokio::task::yield_now().await;
721        }
722        panic!("timed out waiting for {expected} stream attempts");
723    }
724
725    // ===== Tests =====
726
727    #[tokio::test]
728    async fn stream_delivery_and_unsubscribe_on_drop() {
729        // stream supported, poll supported (doesn't matter; prefer long)
730        let (transport, events_tx, mut ctrl_rx) = TestTransport::new(true, true);
731
732        // prefer_polling = false so connection loop will try stream first.
733        let consumer = Consumer::new(transport, false, ());
734
735        // Subscribe to Foo(7)
736        let mut sub = consumer
737            .subscribe(SubscriptionReq::Foo("t".to_owned(), 7))
738            .expect("subscribe ok");
739
740        // We should see a Subscribe(name, topic) forwarded to transport
741        let ctrl = expect_ctrl(
742            &mut ctrl_rx,
743            1000,
744            |m| matches!(m, StreamCtrl::Subscribe((_, idx)) if *idx == IndexTest::Foo(7)),
745        )
746        .await;
747        match ctrl {
748            StreamCtrl::Subscribe((name, idx)) => {
749                assert_ne!(name, "t".to_owned());
750                assert_eq!(idx, IndexTest::Foo(7));
751            }
752            _ => unreachable!(),
753        }
754
755        // Send an event that matches Foo(7)
756        events_tx.send(Message { foo: 7, bar: 1 }).await.unwrap();
757        let got = recv_next::<TestTransport>(&mut sub, 1000)
758            .await
759            .expect("got event");
760        assert_eq!(got, Message { foo: 7, bar: 1 });
761
762        // Dropping the RemoteActiveConsumer should trigger an Unsubscribe(name)
763        drop(sub);
764        let _ctrl = expect_ctrl(&mut ctrl_rx, 1000, |m| {
765            matches!(m, StreamCtrl::Unsubscribe(_))
766        })
767        .await;
768
769        // Drop the Consumer -> Stop is sent so the transport loop exits cleanly
770        drop(consumer);
771        let _ = expect_ctrl(&mut ctrl_rx, 1000, |m| matches!(m, StreamCtrl::Stop)).await;
772    }
773
774    #[tokio::test]
775    async fn test_cache_and_invalation() {
776        // stream supported, poll supported (doesn't matter; prefer long)
777        let (transport, events_tx, mut ctrl_rx) = TestTransport::new(true, true);
778
779        // prefer_polling = false so connection loop will try stream first.
780        let consumer = Consumer::new(transport, false, ());
781
782        // Subscribe to Foo(7)
783        let mut sub_1 = consumer
784            .subscribe(SubscriptionReq::Foo("t".to_owned(), 7))
785            .expect("subscribe ok");
786
787        // We should see a Subscribe(name, topic) forwarded to transport
788        let ctrl = expect_ctrl(
789            &mut ctrl_rx,
790            1000,
791            |m| matches!(m, StreamCtrl::Subscribe((_, idx)) if *idx == IndexTest::Foo(7)),
792        )
793        .await;
794        match ctrl {
795            StreamCtrl::Subscribe((name, idx)) => {
796                assert_ne!(name, "t1".to_owned());
797                assert_eq!(idx, IndexTest::Foo(7));
798            }
799            _ => unreachable!(),
800        }
801
802        // Send an event that matches Foo(7)
803        events_tx.send(Message { foo: 7, bar: 1 }).await.unwrap();
804        let got = recv_next::<TestTransport>(&mut sub_1, 1000)
805            .await
806            .expect("got event");
807        assert_eq!(got, Message { foo: 7, bar: 1 });
808
809        // Subscribe to Foo(7), should receive the latest message and future messages
810        let mut sub_2 = consumer
811            .subscribe(SubscriptionReq::Foo("t2".to_owned(), 7))
812            .expect("subscribe ok");
813
814        let got = recv_next::<TestTransport>(&mut sub_2, 1000)
815            .await
816            .expect("got event");
817        assert_eq!(got, Message { foo: 7, bar: 1 });
818
819        // Dropping the RemoteActiveConsumer but not unsubscribe, since sub_2 is still active
820        drop(sub_1);
821
822        // Subscribe to Foo(7), should receive the latest message and future messages
823        let mut sub_3 = consumer
824            .subscribe(SubscriptionReq::Foo("t3".to_owned(), 7))
825            .expect("subscribe ok");
826
827        // receive cache message
828        let got = recv_next::<TestTransport>(&mut sub_3, 1000)
829            .await
830            .expect("got event");
831        assert_eq!(got, Message { foo: 7, bar: 1 });
832
833        // Send an event that matches Foo(7)
834        events_tx.send(Message { foo: 7, bar: 2 }).await.unwrap();
835
836        // receive new message
837        let got = recv_next::<TestTransport>(&mut sub_2, 1000)
838            .await
839            .expect("got event");
840        assert_eq!(got, Message { foo: 7, bar: 2 });
841
842        let got = recv_next::<TestTransport>(&mut sub_3, 1000)
843            .await
844            .expect("got event");
845        assert_eq!(got, Message { foo: 7, bar: 2 });
846
847        drop(sub_2);
848        drop(sub_3);
849
850        let _ctrl = expect_ctrl(&mut ctrl_rx, 1000, |m| {
851            matches!(m, StreamCtrl::Unsubscribe(_))
852        })
853        .await;
854
855        // The cache should be dropped, so no new messages
856        let mut sub_4 = consumer
857            .subscribe(SubscriptionReq::Foo("t4".to_owned(), 7))
858            .expect("subscribe ok");
859
860        assert!(
861            recv_next::<TestTransport>(&mut sub_4, 1000).await.is_none(),
862            "Should have not receive any update"
863        );
864
865        drop(sub_4);
866
867        // Drop the Consumer -> Stop is sent so the transport loop exits cleanly
868        let _ = expect_ctrl(&mut ctrl_rx, 2000, |m| matches!(m, StreamCtrl::Stop)).await;
869    }
870
871    #[tokio::test]
872    async fn cache_ignores_orphan_event_topics() {
873        let (transport, events_tx, mut ctrl_rx) = TestTransport::new(true, true);
874        let consumer = Consumer::new(transport, false, ());
875
876        let mut sub = consumer
877            .subscribe(SubscriptionReq::Foo("t".to_owned(), 7))
878            .expect("subscribe ok");
879        let _ = expect_ctrl(&mut ctrl_rx, 1000, |m| {
880            matches!(m, StreamCtrl::Subscribe(_))
881        })
882        .await;
883
884        events_tx.send(Message { foo: 7, bar: 99 }).await.unwrap();
885        let _ = recv_next::<TestTransport>(&mut sub, 1000)
886            .await
887            .expect("got event");
888
889        drop(sub);
890        let _ = expect_ctrl(&mut ctrl_rx, 1000, |m| {
891            matches!(m, StreamCtrl::Unsubscribe(_))
892        })
893        .await;
894
895        let cache = consumer.cached_events.read();
896        assert!(
897            cache.is_empty(),
898            "cache leaked entries for orphan topics: {:?}",
899            cache.keys().collect::<Vec<_>>()
900        );
901    }
902
903    #[tokio::test]
904    async fn falls_back_to_poll_when_stream_not_supported() {
905        // stream NOT supported, poll supported
906        let (transport, events_tx, _) = TestTransport::new(false, true);
907        // prefer_polling = true nudges the connection loop to poll first, but even if it
908        // tried stream, our transport returns NotSupported and the loop will use poll.
909        let consumer = Consumer::new(transport, true, ());
910
911        // Subscribe to Bar(5)
912        let mut sub = consumer
913            .subscribe(SubscriptionReq::Bar("t".to_owned(), 5))
914            .expect("subscribe ok");
915
916        // Inject an event; the poll path should relay it on the first poll iteration
917        events_tx.send(Message { foo: 9, bar: 5 }).await.unwrap();
918        let got = recv_next::<TestTransport>(&mut sub, 1500)
919            .await
920            .expect("event relayed via polling");
921        assert_eq!(got, Message { foo: 9, bar: 5 });
922    }
923
924    #[tokio::test(start_paused = true)]
925    async fn terminal_stream_failure_is_not_retried() {
926        let (transport, attempts) = FailingStreamTransport::new(true);
927        let consumer = Consumer::new(transport, false, ());
928        let _subscription = consumer
929            .subscribe(SubscriptionReq::Foo("t".to_owned(), 1))
930            .expect("subscribe");
931
932        wait_for_attempts(&attempts, 1).await;
933        tokio::time::advance(Duration::from_secs(60)).await;
934        tokio::task::yield_now().await;
935
936        assert_eq!(attempts.load(Ordering::Relaxed), 1);
937    }
938
939    #[tokio::test(start_paused = true)]
940    async fn transient_stream_failures_use_increasing_backoff() {
941        let (transport, attempts) = FailingStreamTransport::new(false);
942        let consumer = Consumer::new(transport, false, ());
943        let _subscription = consumer
944            .subscribe(SubscriptionReq::Foo("t".to_owned(), 1))
945            .expect("subscribe");
946
947        wait_for_attempts(&attempts, 1).await;
948
949        tokio::time::advance(Duration::from_secs(3)).await;
950        wait_for_attempts(&attempts, 2).await;
951
952        tokio::time::advance(Duration::from_secs(3)).await;
953        tokio::task::yield_now().await;
954        assert_eq!(attempts.load(Ordering::Relaxed), 2);
955
956        tokio::time::advance(Duration::from_secs(2)).await;
957        wait_for_attempts(&attempts, 3).await;
958    }
959
960    #[tokio::test]
961    async fn multiple_subscribers_share_single_remote_subscription() {
962        // This validates the "coalescing" behavior in Consumer::subscribe where multiple local
963        // subscribers to the same Topic should only create one remote subscription.
964        let (transport, events_tx, mut ctrl_rx) = TestTransport::new(true, true);
965        let consumer = Consumer::new(transport, false, ());
966
967        // Two local subscriptions to the SAME topic/name pair (different names)
968        let mut a = consumer
969            .subscribe(SubscriptionReq::Foo("t".to_owned(), 1))
970            .expect("subscribe A");
971        let _ = expect_ctrl(
972            &mut ctrl_rx,
973            1000,
974            |m| matches!(m, StreamCtrl::Subscribe((_, idx)) if  *idx == IndexTest::Foo(1)),
975        )
976        .await;
977
978        let mut b = consumer
979            .subscribe(SubscriptionReq::Foo("b".to_owned(), 1))
980            .expect("subscribe B");
981
982        // No second Subscribe should be forwarded for the same topic (coalesced).
983        // Give a little time; if one appears, we'll fail explicitly.
984        if let Ok(Some(StreamCtrl::Subscribe((_, idx)))) =
985            timeout(Duration::from_millis(400), ctrl_rx.recv()).await
986        {
987            assert_ne!(idx, IndexTest::Foo(1), "should not resubscribe same topic");
988        }
989
990        // Send one event and ensure BOTH local subscribers receive it.
991        events_tx.send(Message { foo: 1, bar: 42 }).await.unwrap();
992        let got_a = recv_next::<TestTransport>(&mut a, 1000)
993            .await
994            .expect("A got");
995        let got_b = recv_next::<TestTransport>(&mut b, 1000)
996            .await
997            .expect("B got");
998        assert_eq!(got_a, Message { foo: 1, bar: 42 });
999        assert_eq!(got_b, Message { foo: 1, bar: 42 });
1000
1001        // Drop B: no Unsubscribe should be sent yet (still one local subscriber).
1002        drop(b);
1003        if let Ok(Some(StreamCtrl::Unsubscribe(_))) =
1004            timeout(Duration::from_millis(400), ctrl_rx.recv()).await
1005        {
1006            panic!("Should NOT unsubscribe while another local subscriber exists");
1007        }
1008
1009        // Drop A: now remote unsubscribe should occur.
1010        drop(a);
1011        let _ = expect_ctrl(&mut ctrl_rx, 1000, |m| {
1012            matches!(m, StreamCtrl::Unsubscribe(_))
1013        })
1014        .await;
1015
1016        let _ = expect_ctrl(&mut ctrl_rx, 1000, |m| matches!(m, StreamCtrl::Stop)).await;
1017    }
1018}