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
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct Topic(u64);
23
24impl Topic {
25 #[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 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 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 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 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#[must_use]
225pub fn committed_changes(db: &Db) -> broadcast::Receiver<Announced> {
226 db.changes.subscribe()
227}
228
229#[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, ¶ms).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
256fn 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 if std::mem::replace(&mut listened_before, true) {
323 let _ = here.send(Announced::MaybeMissed);
324 }
325 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}