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