1use std::time::Duration;
2
3use crate::{BoxFuture, Db, Wakeups};
4use sofie_core::Result;
5
6const 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
38pub struct Listening {
41 db: Db,
42 channel: Channel,
43 interval: Duration,
44 wakeups: Option<Box<dyn Wakeups>>,
45}
46
47impl Listening {
48 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
76async 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}