Skip to main content

sofie_db/
live.rs

1use std::cell::RefCell;
2use std::collections::BTreeSet;
3use std::fmt;
4use std::hash::{BuildHasher, RandomState};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex, OnceLock, Weak};
7use std::time::Duration;
8
9use sofie_core::{SqlValue, Uuid, unpoisoned};
10use tokio::sync::{broadcast, mpsc};
11
12use crate::arrivals::coalesced;
13use crate::{Aggregate, Db, Driver, Notifications, Reach, query_values};
14
15const BACKLOG: usize = 256;
16/// Postgres refuses a payload of 8000 bytes or more.
17const PAYLOAD_LIMIT: usize = 7900;
18const LISTEN_RETRY: Duration = Duration::from_secs(5);
19const ALIVE_CHECK: Duration = Duration::from_secs(30);
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct Topic(u64);
23
24impl Topic {
25    /// FNV-1a, stable across builds, so instances running different versions agree.
26    #[must_use]
27    pub fn named(schema: &str, table: &str) -> Self {
28        let hash = sofie_core::Fnv1a64::new()
29            .write(schema.as_bytes())
30            .write(b".")
31            .write(table.as_bytes());
32        Self(hash.finish())
33    }
34
35    #[must_use]
36    pub fn of<A: Aggregate>() -> Self {
37        Self::named(A::MODULE, A::NAME)
38    }
39
40    #[must_use]
41    pub fn parse(hex: &str) -> Option<Self> {
42        if hex.len() != 16 {
43            return None;
44        }
45        u64::from_str_radix(hex, 16).ok().map(Self)
46    }
47}
48
49impl fmt::Display for Topic {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(formatter, "{:016x}", self.0)
52    }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
56pub enum Readers {
57    Anyone,
58    Owner(Uuid),
59    /// An owned record whose owner the write did not name: only an unrestricted reach hears it.
60    Unrestricted,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub struct TableChange {
65    pub topic: Topic,
66    pub readers: Readers,
67}
68
69impl TableChange {
70    pub(crate) fn of<A: Aggregate>(owner: Option<Uuid>, reach: Reach) -> Self {
71        let readers = match (A::OWNER, owner, reach) {
72            (None, _, _) => Readers::Anyone,
73            (Some(_), Some(owner), _) | (Some(_), None, Reach::OwnedBy(owner)) => {
74                Readers::Owner(owner)
75            }
76            (Some(_), None, Reach::All | Reach::AllAs(_)) => Readers::Unrestricted,
77        };
78        Self {
79            topic: Topic::of::<A>(),
80            readers,
81        }
82    }
83
84    #[must_use]
85    pub fn reaches(&self, reach: Reach) -> bool {
86        match (self.readers, reach) {
87            (_, Reach::All | Reach::AllAs(_)) | (Readers::Anyone, _) => true,
88            (Readers::Owner(owner), Reach::OwnedBy(principal)) => owner == principal,
89            (Readers::Unrestricted, Reach::OwnedBy(_)) => false,
90        }
91    }
92}
93
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub enum Topics {
96    Every,
97    Of(BTreeSet<Topic>),
98}
99
100impl Topics {
101    #[must_use]
102    pub fn covers(&self, topic: Topic) -> bool {
103        match self {
104            Self::Every => true,
105            Self::Of(topics) => topics.contains(&topic),
106        }
107    }
108}
109
110tokio::task_local! {
111    static HEARD: RefCell<Topics>;
112}
113
114pub async fn track_topics<F: Future>(future: F) -> (F::Output, Topics) {
115    HEARD
116        .scope(RefCell::new(Topics::Of(BTreeSet::new())), async {
117            let output = future.await;
118            let topics = HEARD.with(|heard| heard.borrow().clone());
119            (output, topics)
120        })
121        .await
122}
123
124pub(crate) fn tracking() -> bool {
125    HEARD.try_with(|_| ()).is_ok()
126}
127
128pub(crate) fn read_tables<'t>(tables: impl IntoIterator<Item = (&'t str, &'t str)>) {
129    let _ = HEARD.try_with(|heard| {
130        if let Topics::Of(heard) = &mut *heard.borrow_mut() {
131            heard.extend(
132                tables
133                    .into_iter()
134                    .map(|(schema, table)| Topic::named(schema, table)),
135            );
136        }
137    });
138}
139
140#[cfg(any(feature = "raw-sql", test))]
141pub(crate) fn read_anything() {
142    let _ = HEARD.try_with(|heard| *heard.borrow_mut() = Topics::Every);
143}
144
145#[derive(Clone, Debug, PartialEq, Eq)]
146pub enum Announced {
147    Changed(Arc<[TableChange]>),
148    /// Listening to other instances was down, so any change of theirs may have been missed.
149    MaybeMissed,
150}
151
152impl Announced {
153    #[must_use]
154    pub fn concerns(&self, topics: &Topics, reach: Reach) -> bool {
155        match self {
156            Self::Changed(changes) => changes
157                .iter()
158                .any(|change| topics.covers(change.topic) && change.reaches(reach)),
159            Self::MaybeMissed => true,
160        }
161    }
162}
163
164pub(crate) struct Changes {
165    driver: Weak<dyn Driver>,
166    here: broadcast::Sender<Announced>,
167    instance: u64,
168    outgoing: Arc<Mutex<Vec<TableChange>>>,
169    sending: OnceLock<mpsc::Sender<()>>,
170    listening: OnceLock<()>,
171    /// Always set where the database tells no other instance; elsewhere only while subscribed.
172    hearing: Arc<AtomicBool>,
173}
174
175impl Changes {
176    pub(crate) fn new(driver: &Arc<dyn Driver>) -> Self {
177        Self {
178            driver: Arc::downgrade(driver),
179            here: broadcast::channel(BACKLOG).0,
180            instance: RandomState::new().hash_one(std::process::id()),
181            outgoing: Arc::default(),
182            sending: OnceLock::new(),
183            listening: OnceLock::new(),
184            hearing: Arc::new(AtomicBool::new(driver.change_notifications().is_none())),
185        }
186    }
187
188    pub(crate) fn announce(&self, driver: &Arc<dyn Driver>, changes: Vec<TableChange>) {
189        if changes.is_empty() {
190            return;
191        }
192        let changes = distinct(changes);
193        // Fails only while no screen listens, which is no failure.
194        let _ = self.here.send(Announced::Changed(changes.clone().into()));
195        let Some(notify) = driver.dialect().live_notify() else {
196            return;
197        };
198        unpoisoned::lock(&self.outgoing).extend(changes);
199        let sender = self.sending.get_or_init(|| {
200            let (outgoing, instance) = (Arc::clone(&self.outgoing), self.instance);
201            coalesced(Arc::downgrade(driver), move |driver| {
202                send(driver, Arc::clone(&outgoing), notify, instance)
203            })
204        });
205        if sender.try_send(()).is_err() {
206            tracing::trace!("a notification of changes is already pending");
207        }
208    }
209
210    fn subscribe(&self) -> broadcast::Receiver<Announced> {
211        self.listening.get_or_init(|| {
212            tokio::spawn(listen_elsewhere(
213                Weak::clone(&self.driver),
214                self.here.clone(),
215                Arc::clone(&self.hearing),
216                self.instance,
217            ));
218        });
219        self.here.subscribe()
220    }
221}
222
223/// Every change committed from now on, here or by another instance.
224#[must_use]
225pub fn committed_changes(db: &Db) -> broadcast::Receiver<Announced> {
226    db.changes.subscribe()
227}
228
229/// Whether another instance's commits reach `committed_changes` now: always where the
230/// database tells no other instance (SQLite), elsewhere only while the listener the first
231/// `committed_changes` started is subscribed. A cache trusts what it holds only while true.
232#[must_use]
233pub fn hears_other_instances(db: &Db) -> bool {
234    db.changes.hearing.load(Ordering::Acquire)
235}
236
237async fn send(
238    driver: Arc<dyn Driver>,
239    outgoing: Arc<Mutex<Vec<TableChange>>>,
240    notify: &'static str,
241    instance: u64,
242) {
243    let changes = distinct(std::mem::take(&mut *unpoisoned::lock(&outgoing)));
244    for payload in payloads(instance, &changes) {
245        let params = [SqlValue::Text(payload)];
246        if let Err(error) = query_values(driver.as_ref(), notify, &params).await {
247            tracing::warn!(
248                error.kind = "notify",
249                error = %error,
250                "telling other instances of changes failed; their open screens miss them"
251            );
252        }
253    }
254}
255
256/// `<instance> <topic>:<readers> …`, readers `*` for anyone, `!` for an unrestricted reach.
257fn distinct(mut changes: Vec<TableChange>) -> Vec<TableChange> {
258    changes.sort_unstable();
259    changes.dedup();
260    changes
261}
262
263fn payloads(instance: u64, changes: &[TableChange]) -> Vec<String> {
264    let start = format!("{instance:016x}");
265    let mut payloads = Vec::new();
266    let mut payload = start.clone();
267    for change in changes {
268        let entry = match change.readers {
269            Readers::Anyone => format!(" {}:*", change.topic),
270            Readers::Unrestricted => format!(" {}:!", change.topic),
271            Readers::Owner(owner) => format!(" {}:{owner}", change.topic),
272        };
273        if payload.len() + entry.len() > PAYLOAD_LIMIT {
274            payloads.push(std::mem::replace(&mut payload, start.clone()));
275        }
276        payload.push_str(&entry);
277    }
278    payloads.push(payload);
279    payloads
280}
281
282fn parsed(payload: &str) -> Option<(u64, Vec<TableChange>)> {
283    let mut parts = payload.split(' ');
284    let instance = u64::from_str_radix(parts.next()?, 16).ok()?;
285    let changes = parts
286        .map(|entry| {
287            let (topic, readers) = entry.split_once(':')?;
288            let readers = match readers {
289                "*" => Readers::Anyone,
290                "!" => Readers::Unrestricted,
291                owner => Readers::Owner(owner.parse().ok()?),
292            };
293            Some(TableChange {
294                topic: Topic::parse(topic)?,
295                readers,
296            })
297        })
298        .collect::<Option<Vec<TableChange>>>()?;
299    Some((instance, changes))
300}
301
302async fn listen_elsewhere(
303    driver: Weak<dyn Driver>,
304    here: broadcast::Sender<Announced>,
305    hearing: Arc<AtomicBool>,
306    instance: u64,
307) {
308    let mut listened_before = false;
309    loop {
310        let subscribed = {
311            let Some(driver) = driver.upgrade() else {
312                return;
313            };
314            let Some(subscribing) = driver.change_notifications() else {
315                return;
316            };
317            subscribing.await
318        };
319        match subscribed {
320            Ok(notices) => {
321                // Nothing was missed before the first subscription: no screen was listening.
322                if std::mem::replace(&mut listened_before, true) {
323                    let _ = here.send(Announced::MaybeMissed);
324                }
325                // After `MaybeMissed`, so a reader seeing it set finds that queued first.
326                hearing.store(true, Ordering::Release);
327                let error = follow(notices, &driver, &here, instance).await;
328                hearing.store(false, Ordering::Release);
329                let Some(error) = error else {
330                    return;
331                };
332                tracing::warn!(error = %error, "listening for other instances' changes failed; listening again");
333            }
334            Err(error) => {
335                tracing::warn!(error = %error, "listening for other instances' changes failed; trying again");
336            }
337        }
338        tokio::time::sleep(LISTEN_RETRY).await;
339    }
340}
341
342async fn follow(
343    mut notices: Box<dyn Notifications>,
344    driver: &Weak<dyn Driver>,
345    here: &broadcast::Sender<Announced>,
346    instance: u64,
347) -> Option<sofie_core::Error> {
348    loop {
349        if driver.strong_count() == 0 {
350            return None;
351        }
352        let payloads = match tokio::time::timeout(ALIVE_CHECK, notices.next()).await {
353            Err(_) => continue,
354            Ok(Err(error)) => return Some(error),
355            Ok(Ok(payloads)) => payloads,
356        };
357        for payload in payloads {
358            match parsed(&payload) {
359                Some((sender, _)) if sender == instance => {}
360                Some((_, changes)) => drop(here.send(Announced::Changed(changes.into()))),
361                None => tracing::warn!(
362                    live.payload_bytes = payload.len(),
363                    "a change notification this version cannot read was skipped"
364                ),
365            }
366        }
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    const OWNER: Uuid = Uuid::from_u128(0x0190_a7d2_0000_7000_8000_0000_0000_0001);
375    const OTHER: Uuid = Uuid::from_u128(0x0190_a7d2_0000_7000_8000_0000_0000_0002);
376
377    #[test]
378    fn a_topic_is_fnv_1a_of_the_qualified_table_as_every_version_names_it() {
379        assert_eq!(
380            Topic::named("shop", "order").to_string(),
381            "814cce8dc5ccd897"
382        );
383    }
384
385    fn change(readers: Readers) -> TableChange {
386        TableChange {
387            topic: Topic::named("shop", "order"),
388            readers,
389        }
390    }
391
392    #[test]
393    fn changes_of_interleaved_owners_are_announced_once_each() {
394        let changes = [OWNER, OTHER, OWNER, OTHER]
395            .map(|owner| change(Readers::Owner(owner)))
396            .to_vec();
397
398        assert_eq!(
399            distinct(changes),
400            [change(Readers::Owner(OWNER)), change(Readers::Owner(OTHER))]
401        );
402    }
403
404    #[test]
405    fn a_change_reaches_only_those_who_may_read_the_record() {
406        let owned = change(Readers::Owner(OWNER));
407
408        assert!(owned.reaches(Reach::All));
409        assert!(owned.reaches(Reach::OwnedBy(OWNER)));
410        assert!(!owned.reaches(Reach::OwnedBy(OTHER)));
411        assert!(change(Readers::Anyone).reaches(Reach::OwnedBy(OTHER)));
412        assert!(change(Readers::Unrestricted).reaches(Reach::All));
413        assert!(change(Readers::Unrestricted).reaches(Reach::AllAs(OTHER)));
414        assert!(!change(Readers::Unrestricted).reaches(Reach::OwnedBy(OWNER)));
415    }
416
417    #[test]
418    fn payloads_read_back_as_written_and_stay_under_the_limit() {
419        let changes: Vec<TableChange> = (0..400_u32)
420            .map(|index| TableChange {
421                topic: Topic::named("shop", &index.to_string()),
422                readers: match index % 3 {
423                    0 => Readers::Anyone,
424                    1 => Readers::Unrestricted,
425                    _ => Readers::Owner(OWNER),
426                },
427            })
428            .collect();
429
430        let written = payloads(7, &changes);
431
432        assert!(written.len() > 1);
433        assert!(written.iter().all(|payload| payload.len() <= PAYLOAD_LIMIT));
434        let read: Vec<TableChange> = written
435            .iter()
436            .flat_map(|payload| {
437                let (instance, changes) = parsed(payload).unwrap();
438                assert_eq!(instance, 7);
439                changes
440            })
441            .collect();
442        assert_eq!(read, changes);
443        assert_eq!(parsed("7 nonsense"), None);
444    }
445
446    #[test]
447    fn a_topic_is_stable_and_round_trips_as_hex() {
448        let topic = Topic::named("shop", "order");
449
450        assert_eq!(topic, Topic::named("shop", "order"));
451        assert_ne!(topic, Topic::named("shop", "orders"));
452        assert_eq!(Topic::parse(&topic.to_string()), Some(topic));
453        assert_eq!(Topic::parse("xyz"), None);
454    }
455
456    #[tokio::test]
457    async fn reads_are_heard_until_one_cannot_name_its_tables() {
458        let order = Topic::named("shop", "order");
459        let ((), named) = track_topics(async { read_tables([("shop", "order")]) }).await;
460        let ((), unnamed) = track_topics(async {
461            read_tables([("shop", "order")]);
462            read_anything();
463        })
464        .await;
465
466        assert_eq!(named, Topics::Of(BTreeSet::from([order])));
467        assert_eq!(unnamed, Topics::Every);
468        assert!(unnamed.covers(Topic::named("other", "table")));
469        read_anything();
470    }
471}