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 #[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
35pub struct Listening {
38 db: Db,
39 channel: Channel,
40 interval: Duration,
41 wakeups: Option<Box<dyn Wakeups>>,
42}
43
44impl Listening {
45 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
73async 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}