use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
use crate::paths::Paths;
use crate::tui::data::{self, Snapshot};
pub struct Polled {
pub epoch: u64,
pub snapshot: Snapshot,
}
type OnScreen = Arc<Mutex<HashSet<String>>>;
pub struct Poller {
epoch: Arc<AtomicU64>,
on_screen: OnScreen,
stop: Option<mpsc::Sender<()>>,
rx: mpsc::Receiver<Polled>,
handle: Option<JoinHandle<()>>,
}
impl Poller {
pub fn start(paths: &Paths, interval: Duration) -> Self {
Self::start_with(interval, {
let paths = paths.clone();
move |on_screen| data::try_poll_focused(&paths, on_screen)
})
}
fn start_with<F>(interval: Duration, mut load: F) -> Self
where
F: FnMut(&HashSet<String>) -> Option<Snapshot> + Send + 'static,
{
let epoch = Arc::new(AtomicU64::new(0));
let on_screen: OnScreen = Arc::default();
let shown = on_screen.clone();
let (stop_tx, stop_rx) = mpsc::channel::<()>();
let (tx, rx) = mpsc::sync_channel(1);
let shared = epoch.clone();
let handle = std::thread::spawn(move || {
while let Err(mpsc::RecvTimeoutError::Timeout) = stop_rx.recv_timeout(interval) {
let epoch = shared.load(Ordering::Acquire);
let on_screen = shown.lock().unwrap_or_else(|e| e.into_inner()).clone();
let Some(snapshot) = load(&on_screen) else {
continue;
};
if let Err(mpsc::TrySendError::Disconnected(_)) =
tx.try_send(Polled { epoch, snapshot })
{
return;
}
}
});
Self {
epoch,
on_screen,
stop: Some(stop_tx),
rx,
handle: Some(handle),
}
}
pub fn set_epoch(&self, epoch: u64) {
self.epoch.store(epoch, Ordering::Release);
}
pub fn set_on_screen(&self, sources: HashSet<String>) {
*self.on_screen.lock().unwrap_or_else(|e| e.into_inner()) = sources;
}
pub fn latest(&self, epoch: u64) -> Option<Snapshot> {
self.rx
.try_iter()
.filter(|p| p.epoch == epoch)
.last()
.map(|p| p.snapshot)
}
}
impl Drop for Poller {
fn drop(&mut self) {
drop(self.stop.take());
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
use std::time::Instant;
fn snapshot(name: &str) -> Snapshot {
Snapshot {
source_names: vec![name.to_string()],
..Default::default()
}
}
fn wait_latest(p: &Poller, epoch: u64) -> Option<Snapshot> {
let start = Instant::now();
while start.elapsed() < Duration::from_secs(5) {
if let Some(s) = p.latest(epoch) {
return Some(s);
}
std::thread::sleep(Duration::from_millis(5));
}
None
}
fn gated_load(
started: mpsc::Sender<()>,
go: mpsc::Receiver<()>,
name: &'static str,
) -> impl FnMut(&HashSet<String>) -> Option<Snapshot> + Send + 'static {
move |_| {
let _ = started.send(());
go.recv().ok()?;
Some(snapshot(name))
}
}
#[test]
fn a_slow_load_does_not_block_the_caller() {
let (started_tx, started_rx) = mpsc::channel();
let (go_tx, go_rx) = mpsc::channel();
let p = Poller::start_with(
Duration::from_millis(1),
gated_load(started_tx, go_rx, "slow"),
);
started_rx.recv().unwrap();
assert!(p.latest(0).is_none());
go_tx.send(()).unwrap();
assert_eq!(wait_latest(&p, 0), Some(snapshot("slow")));
drop(go_tx);
}
#[test]
fn a_snapshot_from_an_earlier_epoch_is_dropped() {
let (started_tx, started_rx) = mpsc::channel();
let (go_tx, go_rx) = mpsc::channel();
let p = Poller::start_with(
Duration::from_millis(1),
gated_load(started_tx, go_rx, "stale"),
);
started_rx.recv().unwrap();
p.set_epoch(1);
go_tx.send(()).unwrap();
started_rx.recv().unwrap();
assert!(p.latest(1).is_none(), "an epoch-0 load must be dropped");
go_tx.send(()).unwrap();
assert_eq!(wait_latest(&p, 1), Some(snapshot("stale")));
drop(go_tx);
}
#[test]
fn each_load_is_given_the_sources_on_screen() {
let p = Poller::start_with(Duration::from_millis(1), |on_screen| {
let mut names: Vec<String> = on_screen.iter().cloned().collect();
names.sort();
Some(Snapshot {
source_names: names,
..Default::default()
})
});
p.set_on_screen(HashSet::from(["b".to_string(), "a".to_string()]));
let start = Instant::now();
let mut got = None;
while start.elapsed() < Duration::from_secs(5) {
got = p.latest(0).filter(|s| !s.source_names.is_empty());
if got.is_some() {
break;
}
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(
got.map(|s| s.source_names),
Some(vec!["a".into(), "b".into()])
);
}
#[test]
fn a_skipped_load_sends_nothing_and_dropping_stops_the_thread() {
let calls = Arc::new(AtomicUsize::new(0));
let seen = calls.clone();
let (started_tx, started_rx) = mpsc::channel();
let p = Poller::start_with(Duration::from_millis(1), move |_| {
seen.fetch_add(1, Ordering::SeqCst);
let _ = started_tx.send(());
None
});
for _ in 0..3 {
started_rx.recv().unwrap();
}
assert!(p.latest(0).is_none(), "a skipped load sends nothing");
drop(p);
let after = calls.load(Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(30));
assert_eq!(calls.load(Ordering::SeqCst), after, "no load after drop");
}
}