use std::sync::{Mutex, OnceLock, PoisonError};
use ruststream_sqlx_dialect::{Role, TableSpec};
use tokio::sync::Notify;
use super::table_of;
#[derive(Debug)]
pub(crate) struct TableWake {
table: Box<str>,
grouped: bool,
first: OnceLock<&'static Entry>,
}
#[derive(Debug)]
struct Entry {
group: Box<str>,
notify: Notify,
next: OnceLock<&'static Self>,
}
impl Entry {
fn leak(group: &str) -> &'static Self {
Box::leak(Box::new(Self {
group: group.into(),
notify: Notify::new(),
next: OnceLock::new(),
}))
}
}
impl TableWake {
pub(crate) fn subscribe(&self, name: &str) -> &'static Notify {
let group = if self.grouped { name } else { "" };
let mut entry = *self.first.get_or_init(|| Entry::leak(group));
loop {
if &*entry.group == group {
return &entry.notify;
}
entry = *entry.next.get_or_init(|| Entry::leak(group));
}
}
pub(crate) fn wake(&self, name: &str) {
let mut next = self.first.get();
while let Some(entry) = next {
if !self.grouped || &*entry.group == name {
entry.notify.notify_one();
return;
}
next = entry.next.get();
}
}
#[cfg_attr(
not(any(test, feature = "postgres")),
expect(dead_code, reason = "the Postgres listener wakes every table")
)]
pub(crate) fn wake_all(&self) {
let mut next = self.first.get();
while let Some(entry) = next {
entry.notify.notify_one();
next = entry.next.get();
}
}
pub(crate) fn table(&self) -> &str {
&self.table
}
}
#[derive(Debug, Default)]
pub(crate) struct Wakes {
tables: Mutex<Vec<&'static TableWake>>,
}
impl Wakes {
pub(crate) fn table(&self, spec: &TableSpec<'_>) -> &'static TableWake {
let table = table_of(spec);
let mut tables = self.tables.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(found) = tables.iter().find(|wake| *wake.table == *table) {
return found;
}
let wake: &'static TableWake = Box::leak(Box::new(TableWake {
table: table.into(),
grouped: spec.column(Role::Group).is_some(),
first: OnceLock::new(),
}));
tables.push(wake);
wake
}
}
#[cfg(test)]
mod tests {
use std::ptr;
use futures::FutureExt;
use ruststream_sqlx_dialect::{Column, Form, TableSpec};
use tokio::sync::Notify;
use super::Wakes;
const GROUPED: TableSpec<'static> =
TableSpec::new("jobs", Column::new("id"), Form::RowLock).group(Column::new("name"));
const SINGLE: TableSpec<'static> =
TableSpec::new("plain", Column::new("id"), Form::RowLock).within("ops");
fn woken(notify: &Notify) -> bool {
notify.notified().now_or_never().is_some()
}
#[test]
fn a_publish_wakes_only_its_groups_subscription() {
let wakes = Wakes::default();
let table = wakes.table(&GROUPED);
let (a, b) = (table.subscribe("a"), table.subscribe("b"));
table.wake("b");
assert!(!woken(a), "`a` sleeps on");
assert!(woken(b), "`b` claims");
assert!(!woken(b), "one write, one wake-up");
table.wake("c");
assert!(!woken(a) && !woken(b), "a group nobody reads wakes nobody");
}
#[test]
fn a_table_without_groups_is_one_queue_whatever_the_name() {
let wakes = Wakes::default();
let table = wakes.table(&SINGLE);
let queue = table.subscribe("plain");
assert!(ptr::eq(queue, table.subscribe("another")));
table.wake("anything");
assert!(woken(queue));
}
#[test]
fn a_group_reopened_keeps_its_wake_up_and_every_struct_of_a_table_shares_one() {
let wakes = Wakes::default();
let table = wakes.table(&GROUPED);
assert!(ptr::eq(
table,
wakes.table(&GROUPED.attempt(Column::new("attempt")))
));
assert!(ptr::eq(table.subscribe("a"), table.subscribe("a")));
assert!(!ptr::eq(table, wakes.table(&SINGLE)));
}
#[test]
fn the_listener_names_a_table_qualified_and_wakes_all_its_groups() {
let wakes = Wakes::default();
let grouped = wakes.table(&GROUPED);
assert_eq!(wakes.table(&SINGLE).table(), "ops.plain");
assert_eq!(grouped.table(), "jobs");
let (a, b) = (grouped.subscribe("a"), grouped.subscribe("b"));
grouped.wake_all();
assert!(woken(a) && woken(b));
}
}