Skip to main content

sofie_db/
listening.rs

1use std::time::Duration;
2
3use crate::{BoxFuture, Db, Wakeups};
4use sofie_core::Result;
5
6/// Soon enough that a dropped listener misses little, slow enough that a proxy closing every
7/// listening connection does not turn resubscribing into a tight loop.
8const RESUBSCRIBE_AFTER: Duration = Duration::from_secs(1);
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Channel {
12    Messages,
13    #[cfg(feature = "shield")]
14    ShieldBlocks,
15}
16
17impl Channel {
18    fn name(self) -> &'static str {
19        match self {
20            Self::Messages => "messages",
21            #[cfg(feature = "shield")]
22            Self::ShieldBlocks => "shield_blocks",
23        }
24    }
25
26    fn subscribe(self, db: &Db) -> Option<BoxFuture<'_, Result<Box<dyn Wakeups>>>> {
27        match self {
28            Self::Messages => db.driver().wakeups(),
29            #[cfg(feature = "shield")]
30            Self::ShieldBlocks => crate::SharedBlocks::of(db).wakeups(),
31        }
32    }
33}
34
35/// Without a subscription it polls every `interval` and tries to subscribe again; a renewed
36/// subscription ends the wait at once, since a change made while nobody listened woke nobody.
37pub struct Listening {
38    db: Db,
39    channel: Channel,
40    interval: Duration,
41    wakeups: Option<Box<dyn Wakeups>>,
42}
43
44impl Listening {
45    /// Subscribes before returning, so a change committed after a read that follows is heard.
46    pub async fn start(db: Db, channel: Channel, interval: Duration) -> Self {
47        let wakeups = subscribed(&db, channel).await;
48        Self {
49            db,
50            channel,
51            interval,
52            wakeups,
53        }
54    }
55
56    pub async fn changed(&mut self) {
57        let pause = match self.wakeups.as_mut() {
58            None => self.interval,
59            Some(wakeups) => match tokio::time::timeout(self.interval, wakeups.next()).await {
60                Ok(Ok(())) | Err(_) => return,
61                Ok(Err(error)) => {
62                    tracing::warn!(error = %error, listen.channel = self.channel.name(), "listening for changes failed; polling until it resumes");
63                    RESUBSCRIBE_AFTER
64                }
65            },
66        };
67        self.wakeups = None;
68        tokio::time::sleep(pause).await;
69        self.wakeups = subscribed(&self.db, self.channel).await;
70    }
71}
72
73/// Takes no `&Listening`, whose `Wakeups` are not `Sync`, so its future stays `Send`.
74async fn subscribed(db: &Db, channel: Channel) -> Option<Box<dyn Wakeups>> {
75    match channel.subscribe(db)?.await {
76        Ok(wakeups) => Some(wakeups),
77        Err(error) => {
78            tracing::warn!(error = %error, listen.channel = channel.name(), "subscribing to changes failed; polling instead");
79            None
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use std::future::pending;
87    use std::sync::Arc;
88    use std::sync::atomic::{AtomicUsize, Ordering};
89
90    use sofie_core::{Error, SqlValue};
91
92    use super::*;
93    use crate::aggregate::tests::NUMBERED;
94    use crate::{Dialect, Driver, RowSink, Step};
95
96    const INTERVAL: Duration = Duration::from_secs(10);
97
98    struct Dropped;
99
100    impl Wakeups for Dropped {
101        fn next(&mut self) -> BoxFuture<'_, Result<()>> {
102            Box::pin(async { Err(Error::internal_message("the listening connection dropped")) })
103        }
104    }
105
106    struct Silent;
107
108    impl Wakeups for Silent {
109        fn next(&mut self) -> BoxFuture<'_, Result<()>> {
110            Box::pin(pending())
111        }
112    }
113
114    #[derive(Clone, Default)]
115    struct Flaky {
116        subscriptions: Arc<AtomicUsize>,
117        refusing: bool,
118    }
119
120    impl Driver for Flaky {
121        fn dialect(&self) -> &dyn Dialect {
122            &NUMBERED
123        }
124
125        fn query<'a>(
126            &'a self,
127            _: &'a str,
128            _: &'a [SqlValue],
129            _: &'a mut dyn RowSink,
130        ) -> BoxFuture<'a, Result<()>> {
131            unimplemented!("listening only subscribes")
132        }
133
134        fn transaction(&self, _: Vec<Step>) -> BoxFuture<'_, Result<()>> {
135            unimplemented!("listening only subscribes")
136        }
137
138        fn wakeups(&self) -> Option<BoxFuture<'_, Result<Box<dyn Wakeups>>>> {
139            let subscription = self.subscriptions.fetch_add(1, Ordering::SeqCst);
140            let refusing = self.refusing;
141            Some(Box::pin(async move {
142                if refusing {
143                    return Err(Error::internal_message("too many connections"));
144                }
145                if subscription == 0 {
146                    return Ok(Box::new(Dropped) as Box<dyn Wakeups>);
147                }
148                Ok(Box::new(Silent) as Box<dyn Wakeups>)
149            }))
150        }
151    }
152
153    #[tokio::test(start_paused = true)]
154    async fn a_dropped_subscription_is_renewed_and_ends_the_wait() {
155        let driver = Flaky::default();
156        let mut listening =
157            Listening::start(Db::new(driver.clone()), Channel::Messages, INTERVAL).await;
158
159        let waited = tokio::time::timeout(RESUBSCRIBE_AFTER * 2, listening.changed()).await;
160
161        assert!(waited.is_ok(), "the renewed subscription ended the wait");
162        assert_eq!(driver.subscriptions.load(Ordering::SeqCst), 2);
163        assert!(listening.wakeups.is_some());
164    }
165
166    #[tokio::test(start_paused = true)]
167    async fn a_renewed_subscription_is_kept_across_waits() {
168        let driver = Flaky::default();
169        let mut listening =
170            Listening::start(Db::new(driver.clone()), Channel::Messages, INTERVAL).await;
171        listening.changed().await;
172
173        for _ in 0..3 {
174            listening.changed().await;
175        }
176
177        assert_eq!(driver.subscriptions.load(Ordering::SeqCst), 2);
178    }
179
180    #[tokio::test(start_paused = true)]
181    async fn a_refused_subscription_is_tried_again_once_per_interval() {
182        let driver = Flaky {
183            refusing: true,
184            ..Flaky::default()
185        };
186        let mut listening =
187            Listening::start(Db::new(driver.clone()), Channel::Messages, INTERVAL).await;
188
189        let started = tokio::time::Instant::now();
190        for _ in 0..3 {
191            listening.changed().await;
192        }
193
194        assert_eq!(started.elapsed(), INTERVAL * 3);
195        assert_eq!(driver.subscriptions.load(Ordering::SeqCst), 4);
196    }
197}