use std::collections::BTreeMap;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crate::config::ProxyConfig;
use crate::error::Error;
use crate::watch::{BackendHealth, Shared, ThreadGuard, WatchOptions, fatal_watch_error};
static NEXT_ID: AtomicI64 = AtomicI64::new(1);
static SLOTS: Mutex<BTreeMap<i64, SyncSender<()>>> = Mutex::new(BTreeMap::new());
#[derive(Debug)]
pub(crate) struct WakeSlot(i64);
impl WakeSlot {
pub(crate) fn new(wake: SyncSender<()>) -> Self {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
slots().insert(id, wake);
Self(id)
}
pub(crate) fn id(&self) -> i64 {
self.0
}
}
impl Drop for WakeSlot {
fn drop(&mut self) {
slots().remove(&self.0);
}
}
pub(crate) fn wake(id: i64) {
if let Some(wake) = slots().get(&id) {
let _ = wake.try_send(());
}
}
fn slots() -> MutexGuard<'static, BTreeMap<i64, SyncSender<()>>> {
SLOTS.lock().unwrap_or_else(PoisonError::into_inner)
}
#[derive(Debug)]
pub(crate) struct Watch {
interval: Option<Duration>,
wake: Option<SyncSender<()>>,
woken: Option<Receiver<()>>,
thread: Option<JoinHandle<()>>,
}
impl Watch {
#[cfg_attr(target_os = "android", allow(dead_code))]
pub(crate) fn armed(options: &WatchOptions) -> Result<Self, Error> {
if options.poll_interval.is_none() {
return Err(fatal_watch_error(
"registering for proxy change notifications",
"this platform delivers none to a library",
Error::Unsupported,
));
}
Ok(Self::new(options))
}
pub(crate) fn new(options: &WatchOptions) -> Self {
const CEILING: Duration = Duration::from_secs(24 * 60 * 60);
let (wake, woken) = mpsc::sync_channel::<()>(1);
Self {
interval: options
.poll_interval
.map(|requested| crate::watch::effective_poll_interval(requested).min(CEILING)),
wake: Some(wake),
woken: Some(woken),
thread: None,
}
}
pub(crate) fn waker(&self) -> SyncSender<()> {
self.wake
.clone()
.expect("a Watch holds its sender until Drop")
}
#[cfg(any(target_os = "android", target_os = "ios"))]
pub(crate) fn spawn(
&mut self,
options: &WatchOptions,
shared: Arc<Shared>,
) -> Result<(), Error> {
let read_options = options.clone();
self.spawn_reading(options, shared, move || super::read_config(&read_options))
}
fn spawn_reading(
&mut self,
options: &WatchOptions,
shared: Arc<Shared>,
mut read: impl FnMut() -> Result<ProxyConfig, Error> + Send + 'static,
) -> Result<(), Error> {
let woken = self
.woken
.take()
.expect("Watch::spawn must be called exactly once");
let interval = self.interval;
let debounce = crate::watch::effective_debounce(options.debounce);
let thread = thread::Builder::new()
.name("proxy-watch-poll".to_owned())
.spawn(move || {
let _guard = ThreadGuard::new(Arc::clone(&shared));
loop {
match read() {
Ok(config) => shared.emit(config),
Err(error) => shared.fail(error),
}
let waited = match interval {
Some(interval) => woken.recv_timeout(interval),
None => woken.recv().map_err(|_| RecvTimeoutError::Disconnected),
};
match waited {
Ok(()) => {
let until = Instant::now() + debounce;
loop {
let left = until.saturating_duration_since(Instant::now());
if left.is_zero() {
break;
}
match woken.recv_timeout(left) {
Ok(()) => {}
Err(RecvTimeoutError::Timeout) => break,
Err(RecvTimeoutError::Disconnected) => return,
}
}
}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => return,
}
}
})
.map_err(|source| Error::io("spawning the proxy-watch polling thread", source))?;
self.thread = Some(thread);
Ok(())
}
#[cfg_attr(target_os = "android", allow(dead_code))]
pub(crate) fn health(&self) -> BackendHealth {
BackendHealth {
degraded: Vec::new(),
has_live_notifications: false,
}
}
pub(crate) fn poll_now(&self) {
if let Some(wake) = &self.wake {
let _ = wake.try_send(());
}
}
}
impl Drop for Watch {
fn drop(&mut self) {
drop(self.wake.take());
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicUsize;
use super::*;
fn options(poll_interval: Option<Duration>, debounce: Duration) -> WatchOptions {
let mut options = WatchOptions::new();
options.poll_interval = poll_interval;
options.debounce = debounce;
options
}
fn settle(reads: &AtomicUsize, want: usize) -> usize {
let until = Instant::now() + Duration::from_secs(5);
while reads.load(Ordering::SeqCst) < want && Instant::now() < until {
thread::sleep(Duration::from_millis(5));
}
reads.load(Ordering::SeqCst)
}
fn counting(
debounce: Duration,
fail: impl Fn(usize) -> bool + Send + 'static,
) -> (Watch, Arc<AtomicUsize>) {
let options = options(None, debounce);
let mut watch = Watch::new(&options);
let reads = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&reads);
watch
.spawn_reading(
&options,
Arc::new(Shared::new(ProxyConfig::direct())),
move || {
let n = counter.fetch_add(1, Ordering::SeqCst) + 1;
if fail(n) {
Err(Error::Unsupported)
} else {
Ok(ProxyConfig::direct())
}
},
)
.unwrap();
(watch, reads)
}
#[test]
fn a_storm_of_wake_ups_is_one_read_after_the_window_and_the_next_storm_another() {
let (watch, reads) = counting(Duration::from_millis(100), |_| false);
assert_eq!(settle(&reads, 1), 1, "the opening read");
for _ in 0..20 {
watch.poll_now();
}
assert_eq!(settle(&reads, 2), 2);
thread::sleep(Duration::from_millis(300));
assert_eq!(
reads.load(Ordering::SeqCst),
2,
"the storm read more than once"
);
watch.poll_now();
assert_eq!(
settle(&reads, 3),
3,
"a wake-up after the window is read again"
);
drop(watch);
}
#[test]
fn wake_ups_that_never_stop_still_end_each_window_in_a_read() {
let (watch, reads) = counting(Duration::from_millis(20), |_| false);
assert_eq!(settle(&reads, 1), 1, "the opening read");
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let senders: Vec<_> = (0..4)
.map(|_| {
let wake = watch.waker();
let stop = Arc::clone(&stop);
thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
let _ = wake.try_send(());
}
})
})
.collect();
let seen = settle(&reads, 3);
stop.store(true, Ordering::Relaxed);
for sender in senders {
sender.join().unwrap();
}
assert!(
seen >= 3,
"only {seen} reads while the wake-ups kept coming"
);
}
#[test]
fn a_failed_read_leaves_the_loop_waiting_for_the_next_wake_up() {
let (watch, reads) = counting(Duration::from_millis(10), |n| n == 1);
assert_eq!(settle(&reads, 1), 1);
watch.poll_now();
assert_eq!(settle(&reads, 2), 2);
}
#[test]
fn dropping_the_watch_ends_the_thread_once_no_waker_is_left() {
let (watch, reads) = counting(Duration::from_millis(10), |_| false);
assert_eq!(settle(&reads, 1), 1);
drop(watch);
let after = reads.load(Ordering::SeqCst);
thread::sleep(Duration::from_millis(50));
assert_eq!(reads.load(Ordering::SeqCst), after);
}
#[test]
fn a_slot_wakes_while_it_is_filed_and_not_after() {
let (wake_tx, woken) = mpsc::sync_channel(1);
let slot = WakeSlot::new(wake_tx);
let id = slot.id();
wake(id);
assert!(woken.try_recv().is_ok());
drop(slot);
wake(id);
assert_eq!(woken.try_recv(), Err(mpsc::TryRecvError::Disconnected));
}
#[test]
fn the_interval_is_clamped_to_the_floor_and_the_ceiling() {
let interval = |requested| Watch::new(&options(Some(requested), Duration::ZERO)).interval;
assert_eq!(
interval(Duration::from_millis(1)),
Some(crate::watch::MIN_POLL_INTERVAL)
);
assert_eq!(
interval(Duration::from_secs(48 * 60 * 60)),
Some(Duration::from_secs(24 * 60 * 60))
);
assert_eq!(Watch::new(&options(None, Duration::ZERO)).interval, None);
}
#[test]
fn a_watch_with_no_notifier_refuses_to_arm_without_an_interval() {
let error = Watch::armed(&options(None, Duration::ZERO)).unwrap_err();
assert!(error.to_string().contains("poll_interval"), "{error}");
assert!(Watch::armed(&options(Some(Duration::from_secs(1)), Duration::ZERO)).is_ok());
}
}