use std::sync::{Condvar, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
pub(crate) mod wake {
pub(crate) const INTERVAL: u8 = 1;
pub(crate) const SIZE: u8 = 2;
pub(crate) const EXPLICIT: u8 = 4;
pub(crate) const TRANSPORT: u8 = 8;
pub(crate) const BACKGROUND: u8 = 16;
pub(crate) const STOP: u8 = 32;
pub(crate) const ATTACH: u8 = 64;
pub(crate) const FINAL: u8 = EXPLICIT | BACKGROUND | STOP;
}
#[derive(Debug, Default)]
struct State {
pending: u8,
stop: bool,
requested: u64,
completed: u64,
}
#[derive(Debug, Default)]
pub(crate) struct WakeSignal {
state: Mutex<State>,
cv: Condvar,
}
impl WakeSignal {
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn wake(&self, reason: u8) {
let mut state = self.lock();
state.pending |= reason;
drop(state);
self.cv.notify_all();
}
pub(crate) fn request_flush(&self, reason: u8) -> u64 {
let mut state = self.lock();
state.pending |= reason;
state.requested += 1;
let generation = state.requested;
drop(state);
self.cv.notify_all();
generation
}
pub(crate) fn request_stop(&self) {
let mut state = self.lock();
state.stop = true;
state.pending |= wake::STOP;
drop(state);
self.cv.notify_all();
}
pub(crate) fn wait(&self, deadline: Instant) -> (u8, u64) {
let mut state = self.lock();
loop {
if state.pending != 0 || state.stop {
break;
}
let now = Instant::now();
if now >= deadline {
state.pending |= wake::INTERVAL;
break;
}
let (guard, _) = self
.cv
.wait_timeout(state, deadline - now)
.unwrap_or_else(|e| e.into_inner());
state = guard;
}
let reasons = state.pending | if state.stop { wake::STOP } else { 0 };
state.pending = 0;
(reasons, state.requested)
}
pub(crate) fn complete(&self, generation: u64) {
let mut state = self.lock();
if generation > state.completed {
state.completed = generation;
}
drop(state);
self.cv.notify_all();
}
pub(crate) fn wait_completed(&self, generation: u64, deadline: Instant) -> bool {
let mut state = self.lock();
loop {
if state.completed >= generation {
return true;
}
let now = Instant::now();
if now >= deadline {
return false;
}
let (guard, _) = self
.cv
.wait_timeout(state, deadline - now)
.unwrap_or_else(|e| e.into_inner());
state = guard;
}
}
}
pub(crate) struct Scheduler {
handle: Option<JoinHandle<()>>,
}
impl Scheduler {
pub(crate) fn spawn<F>(body: F) -> std::io::Result<Self>
where
F: FnOnce() + Send + 'static,
{
let handle = std::thread::Builder::new()
.name("offline-protocol-telemetry".into())
.spawn(body)?;
Ok(Self {
handle: Some(handle),
})
}
#[cfg(test)]
pub(crate) fn is_finished(&self) -> bool {
self.handle.as_ref().is_none_or(JoinHandle::is_finished)
}
pub(crate) fn join_until(&mut self, deadline: Instant) -> bool {
let Some(handle) = self.handle.take() else {
return true;
};
while !handle.is_finished() {
if Instant::now() >= deadline {
drop(handle);
return false;
}
std::thread::sleep(Duration::from_millis(5));
}
let _ = handle.join();
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[cfg(target_os = "linux")]
fn current_tid() -> std::ffi::OsString {
std::fs::read_link("/proc/thread-self")
.expect("procfs")
.file_name()
.expect("a task id")
.to_owned()
}
#[test]
fn a_thousand_start_stop_cycles_leave_no_thread_behind() {
#[cfg(target_os = "linux")]
let tids = Arc::new(Mutex::new(Vec::new()));
for _ in 0..1_000 {
let signal = Arc::new(WakeSignal::default());
let worker_signal = signal.clone();
#[cfg(target_os = "linux")]
let worker_tids = tids.clone();
let mut scheduler = Scheduler::spawn(move || {
#[cfg(target_os = "linux")]
worker_tids.lock().expect("tids").push(current_tid());
loop {
let (reasons, gen) =
worker_signal.wait(Instant::now() + Duration::from_secs(60));
worker_signal.complete(gen);
if reasons & wake::STOP != 0 {
break;
}
}
})
.expect("spawn");
signal.request_stop();
assert!(scheduler.join_until(Instant::now() + Duration::from_secs(5)));
assert!(scheduler.is_finished());
}
#[cfg(target_os = "linux")]
{
let tids = tids.lock().expect("tids");
assert_eq!(tids.len(), 1_000, "every worker recorded its id");
let task = std::path::Path::new("/proc/self/task");
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let alive = tids.iter().filter(|tid| task.join(tid).exists()).count();
if alive == 0 {
break;
}
assert!(
Instant::now() < deadline,
"{alive} of the {} workers this test started are still running",
tids.len()
);
std::thread::sleep(Duration::from_millis(10));
}
}
}
#[test]
fn a_stop_while_the_worker_is_blocked_returns_at_the_deadline_and_detaches() {
let started = Arc::new(AtomicUsize::new(0));
let worker_started = started.clone();
let mut scheduler = Scheduler::spawn(move || {
worker_started.store(1, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(600));
})
.expect("spawn");
while started.load(Ordering::SeqCst) == 0 {
std::thread::sleep(Duration::from_millis(1));
}
let before = Instant::now();
let joined = scheduler.join_until(Instant::now() + Duration::from_millis(100));
assert!(!joined);
assert!(before.elapsed() < Duration::from_millis(300));
assert!(
scheduler.is_finished(),
"a detached handle reads as finished"
);
}
#[test]
fn wake_reasons_coalesce_and_a_deadline_is_an_interval_wake() {
let signal = WakeSignal::default();
signal.wake(wake::SIZE);
signal.wake(wake::TRANSPORT);
let (reasons, _) = signal.wait(Instant::now());
assert_eq!(reasons, wake::SIZE | wake::TRANSPORT);
let (reasons, _) = signal.wait(Instant::now());
assert_eq!(reasons, wake::INTERVAL);
}
#[test]
fn a_flush_generation_completes_or_times_out() {
let signal = WakeSignal::default();
let generation = signal.request_flush(wake::EXPLICIT);
assert!(!signal.wait_completed(generation, Instant::now() + Duration::from_millis(20)));
let (_, gen) = signal.wait(Instant::now());
signal.complete(gen);
assert!(signal.wait_completed(generation, Instant::now()));
}
}