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