use std::collections::HashMap;
use std::sync::Mutex;
use tokio::sync::watch;
use crate::core::RunId;
#[derive(Debug, Default)]
pub struct AppendSignals {
runs: Mutex<HashMap<(String, RunId), watch::Sender<u64>>>,
polled: Mutex<std::collections::HashSet<(String, RunId)>>,
tenants: Mutex<HashMap<String, watch::Sender<u64>>>,
polled_all: Mutex<std::collections::HashSet<String>>,
}
impl AppendSignals {
pub fn subscribe(&self, tenant: &str, run: RunId) -> AppendWatch {
let mut runs = self
.runs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let rx = runs
.entry((tenant.to_owned(), run))
.or_insert_with(|| watch::channel(0).0)
.subscribe();
AppendWatch { rx }
}
pub fn raise(&self, tenant: &str, run: RunId, head: u64) {
self.raise_all(tenant);
let mut runs = self
.runs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let key = (tenant.to_owned(), run);
let Some(tx) = runs.get(&key) else { return };
if tx.receiver_count() == 0 {
runs.remove(&key);
return;
}
tx.send_if_modified(|seen| {
if head > *seen {
*seen = head;
true
} else {
false
}
});
}
}
impl AppendSignals {
pub fn subscribe_all(&self, tenant: &str) -> AppendWatch {
let rx = self
.tenants
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entry(tenant.to_owned())
.or_insert_with(|| watch::channel(0).0)
.subscribe();
AppendWatch { rx }
}
pub fn raise_all(&self, tenant: &str) {
let tenants = self
.tenants
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(tx) = tenants.get(tenant) {
tx.send_modify(|seen| *seen = seen.wrapping_add(1));
}
}
pub fn followed_all(&self, tenant: &str) -> bool {
self.tenants
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(tenant)
.is_some_and(|tx| tx.receiver_count() > 0)
}
pub fn claim_poll_all(&self, tenant: &str) -> bool {
self.polled_all
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(tenant.to_owned())
}
pub fn release_poll_all(&self, tenant: &str) {
self.polled_all
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(tenant);
}
pub fn followed(&self, tenant: &str, run: RunId) -> bool {
self.runs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&(tenant.to_owned(), run))
.is_some_and(|tx| tx.receiver_count() > 0)
}
pub fn claim_poll(&self, tenant: &str, run: RunId) -> bool {
self.polled
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert((tenant.to_owned(), run))
}
pub fn release_poll(&self, tenant: &str, run: RunId) {
self.polled
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&(tenant.to_owned(), run));
}
}
#[derive(Debug, Clone)]
pub struct AppendWatch {
rx: watch::Receiver<u64>,
}
impl AppendWatch {
pub async fn changed(&mut self) -> Option<u64> {
self.rx.changed().await.ok()?;
Some(*self.rx.borrow_and_update())
}
#[must_use]
pub fn head(&self) -> u64 {
*self.rx.borrow()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_run_nobody_follows_any_more_is_forgotten() {
let signals = AppendSignals::default();
let run = RunId::generate();
drop(signals.subscribe("t", run));
signals.raise("t", run, 1);
assert!(signals.runs.lock().expect("lock").is_empty());
}
}