use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug, Default)]
pub(super) struct DrainProgress {
completed: AtomicU64,
in_flight_since: Mutex<Option<Instant>>,
queued: AtomicUsize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct ProgressSnapshot {
pub(super) completed: u64,
pub(super) in_flight_for: Option<Duration>,
pub(super) queued: usize,
}
impl DrainProgress {
pub(super) fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub(super) fn begin(&self) {
let mut in_flight = self
.in_flight_since
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*in_flight = Some(Instant::now());
}
pub(super) fn finish(&self) {
let mut in_flight = self
.in_flight_since
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*in_flight = None;
drop(in_flight);
self.completed.fetch_add(1, Ordering::AcqRel);
}
pub(super) fn enqueued(&self) {
self.queued.fetch_add(1, Ordering::AcqRel);
}
pub(super) fn dequeued(&self) {
let _ = self
.queued
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
current.checked_sub(1)
});
}
pub(super) fn queued(&self) -> usize {
self.queued.load(Ordering::Acquire)
}
pub(super) fn snapshot(&self) -> ProgressSnapshot {
let in_flight_for = self
.in_flight_since
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.map(|since| since.elapsed());
ProgressSnapshot {
completed: self.completed.load(Ordering::Acquire),
in_flight_for,
queued: self.queued(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct DrainTimedOut {
pub(super) bound: Duration,
pub(super) since_progress: Duration,
pub(super) queued: usize,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct DrainBound {
bound: Duration,
}
impl DrainBound {
pub(super) const fn new(bound: Duration) -> Self {
Self { bound }
}
pub(super) fn poll_interval(self) -> Duration {
(self.bound / 100).max(Duration::from_micros(1))
}
pub(super) fn progress_interval(self) -> Duration {
(self.bound / 4).max(self.poll_interval())
}
pub(super) fn wait(
self,
worker: &'static str,
stopped: &Receiver<()>,
progress: &DrainProgress,
) -> Result<(), DrainTimedOut> {
let poll = self.poll_interval();
let warn_every = self.progress_interval();
let mut last_completed = progress.snapshot().completed;
let mut last_progress_at = Instant::now();
let mut last_warned_at = last_progress_at;
loop {
match stopped.recv_timeout(poll) {
Ok(()) | Err(RecvTimeoutError::Disconnected) => return Ok(()),
Err(RecvTimeoutError::Timeout) => {}
}
let now = Instant::now();
let snapshot = progress.snapshot();
if snapshot.completed != last_completed {
last_completed = snapshot.completed;
last_progress_at = now;
}
let since_progress = now.duration_since(last_progress_at);
if since_progress >= self.bound {
tracing::error!(
worker,
bound_ms = self.bound.as_millis(),
since_progress_ms = since_progress.as_millis(),
in_flight_ms = snapshot.in_flight_for.map(|age| age.as_millis()),
queued = snapshot.queued,
"engine stop gave up draining: nothing completed for the whole bound"
);
return Err(DrainTimedOut {
bound: self.bound,
since_progress,
queued: snapshot.queued,
});
}
if now.duration_since(last_warned_at) >= warn_every {
last_warned_at = now;
tracing::warn!(
worker,
bound_ms = self.bound.as_millis(),
since_progress_ms = since_progress.as_millis(),
in_flight_ms = snapshot.in_flight_for.map(|age| age.as_millis()),
queued = snapshot.queued,
completed = snapshot.completed,
"engine stop still draining; the bound restarts on every completed job"
);
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Barrier, mpsc};
use std::thread;
use std::time::{Duration, Instant};
use super::{DrainBound, DrainProgress};
#[test]
fn a_progressing_worker_is_waited_out_past_the_total_bound()
-> Result<(), Box<dyn std::error::Error>> {
let bound = DrainBound::new(Duration::from_millis(80));
let progress = DrainProgress::new();
let (stopped_sender, stopped) = mpsc::sync_channel(1);
let worker_progress = std::sync::Arc::clone(&progress);
let rendezvous = Arc::new(Barrier::new(2));
let worker_rendezvous = Arc::clone(&rendezvous);
let worker = thread::spawn(move || {
for index in 0..5 {
worker_progress.begin();
if index == 0 {
worker_rendezvous.wait();
}
thread::sleep(Duration::from_millis(40));
worker_progress.finish();
}
let _ = stopped_sender.send(());
});
rendezvous.wait();
let started = Instant::now();
bound
.wait("test-worker", &stopped, &progress)
.map_err(|timed_out| {
format!("a progressing worker was called a wedge: {timed_out:?}")
})?;
assert!(
started.elapsed() >= Duration::from_millis(150),
"the drain waited for the whole queue, not one bound"
);
worker.join().map_err(|_| "worker panicked")?;
Ok(())
}
#[test]
fn a_blocked_worker_fails_bounded_from_its_last_progress()
-> Result<(), Box<dyn std::error::Error>> {
let bound = DrainBound::new(Duration::from_millis(60));
let progress = DrainProgress::new();
let (_stopped_sender, stopped) = mpsc::sync_channel::<()>(1);
progress.enqueued();
progress.enqueued();
progress.dequeued();
progress.begin();
let started = Instant::now();
let timed_out = bound
.wait("test-worker", &stopped, &progress)
.err()
.ok_or("a blocked worker must time the drain out")?;
let elapsed = started.elapsed();
assert!(
elapsed >= Duration::from_millis(60),
"not before the bound: {elapsed:?}"
);
assert!(
elapsed < Duration::from_millis(600),
"not long after it: {elapsed:?}"
);
assert_eq!(timed_out.bound, Duration::from_millis(60));
assert!(timed_out.since_progress >= Duration::from_millis(60));
assert_eq!(timed_out.queued, 1, "the queue depth rides the error");
Ok(())
}
#[test]
fn the_clock_restarts_on_progress_not_on_the_stop_request()
-> Result<(), Box<dyn std::error::Error>> {
let bound = DrainBound::new(Duration::from_millis(100));
let progress = DrainProgress::new();
let (_stopped_sender, stopped) = mpsc::sync_channel::<()>(1);
let worker_progress = std::sync::Arc::clone(&progress);
let rendezvous = Arc::new(Barrier::new(2));
let worker_rendezvous = Arc::clone(&rendezvous);
let worker = thread::spawn(move || {
worker_progress.begin();
worker_rendezvous.wait();
thread::sleep(Duration::from_millis(70));
worker_progress.finish();
worker_progress.begin();
thread::sleep(Duration::from_millis(250));
});
rendezvous.wait();
let started = Instant::now();
let timed_out = bound
.wait("test-worker", &stopped, &progress)
.err()
.ok_or("the blocked second job must time the drain out")?;
let elapsed = started.elapsed();
assert!(
elapsed >= Duration::from_millis(160),
"the bound restarted after the 70 ms job: {elapsed:?}"
);
assert!(timed_out.since_progress < Duration::from_millis(160));
worker.join().map_err(|_| "worker panicked")?;
Ok(())
}
#[test]
fn a_stopped_worker_returns_at_once() -> Result<(), Box<dyn std::error::Error>> {
let bound = DrainBound::new(Duration::from_secs(30));
let progress = DrainProgress::new();
let (stopped_sender, stopped) = mpsc::sync_channel(1);
stopped_sender.send(())?;
let started = Instant::now();
bound
.wait("test-worker", &stopped, &progress)
.map_err(|timed_out| format!("{timed_out:?}"))?;
assert!(started.elapsed() < Duration::from_secs(1));
Ok(())
}
#[test]
fn intervals_derive_from_the_bound_and_never_reach_zero() {
let bound = DrainBound::new(Duration::from_secs(30));
assert_eq!(bound.poll_interval(), Duration::from_millis(300));
assert_eq!(bound.progress_interval(), Duration::from_millis(7_500));
let tiny = DrainBound::new(Duration::from_nanos(1));
assert!(tiny.poll_interval() > Duration::ZERO);
assert!(tiny.progress_interval() >= tiny.poll_interval());
}
}