use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
pub struct DispatcherGroup<'a> {
running: AtomicUsize,
done: &'a AtomicBool,
}
impl<'a> DispatcherGroup<'a> {
#[must_use]
pub const fn new(count: usize, done: &'a AtomicBool) -> Self {
Self {
running: AtomicUsize::new(count),
done,
}
}
#[must_use = "the dispatcher is counted out when the guard drops"]
pub const fn guard(&self) -> DispatcherGuard<'_, 'a> {
DispatcherGuard { group: self }
}
}
pub struct DispatcherGuard<'g, 'a> {
group: &'g DispatcherGroup<'a>,
}
impl Drop for DispatcherGuard<'_, '_> {
fn drop(&mut self) {
if self.group.running.fetch_sub(1, Ordering::AcqRel) == 1 {
self.group.done.store(true, Ordering::Release);
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RemoteStatus {
dispatchers: u32,
in_flight: u32,
title: String,
}
impl RemoteStatus {
pub const fn dispatcher_started(&mut self) {
self.dispatchers = self.dispatchers.saturating_add(1);
}
pub fn dispatcher_ended(&mut self) {
self.dispatchers = self.dispatchers.saturating_sub(1);
if self.dispatchers == 0 {
self.title.clear();
}
}
pub fn item_started(&mut self, title: &str) {
self.in_flight = self.in_flight.saturating_add(1);
title.clone_into(&mut self.title);
}
pub const fn item_ended(&mut self) {
self.in_flight = self.in_flight.saturating_sub(1);
}
pub fn note(&mut self, text: &str) {
text.clone_into(&mut self.title);
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.dispatchers > 0
}
#[must_use]
pub const fn in_flight(&self) -> u32 {
self.in_flight
}
#[must_use]
pub const fn title(&self) -> &str {
self.title.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gui::batch::batch_queue::WorkQueue;
use std::{
panic::{AssertUnwindSafe, catch_unwind},
sync::Mutex,
time::Duration,
};
#[test]
fn done_is_raised_only_when_the_last_dispatcher_ends() {
let done = AtomicBool::new(false);
let group = DispatcherGroup::new(3, &done);
let first = group.guard();
let second = group.guard();
let third = group.guard();
drop(first);
assert!(!done.load(Ordering::Acquire));
drop(second);
assert!(!done.load(Ordering::Acquire));
drop(third);
assert!(done.load(Ordering::Acquire));
}
#[test]
fn concurrent_dispatchers_raise_done_exactly_once_all_are_out() {
let done = AtomicBool::new(false);
let group = DispatcherGroup::new(8, &done);
std::thread::scope(|scope| {
for _ in 0..8 {
scope.spawn(|| {
let _guard = group.guard();
std::thread::sleep(std::time::Duration::from_millis(5));
});
}
});
assert!(done.load(Ordering::Acquire));
}
#[test]
fn a_panicking_dispatcher_still_counts_out() {
let done = AtomicBool::new(false);
let group = DispatcherGroup::new(1, &done);
let outcome = catch_unwind(AssertUnwindSafe(|| {
let _guard = group.guard();
panic!("dispatcher failed");
}));
assert!(outcome.is_err());
assert!(
done.load(Ordering::Acquire),
"the local lanes must not wait forever on a dead dispatcher"
);
}
#[test]
fn an_empty_group_never_raises_the_flag() {
let done = AtomicBool::new(false);
let _group = DispatcherGroup::new(0, &done);
assert!(!done.load(Ordering::Acquire));
}
#[test]
fn the_status_is_active_while_any_dispatcher_runs() {
let mut status = RemoteStatus::default();
assert!(!status.is_active());
status.dispatcher_started();
status.dispatcher_started();
assert!(status.is_active());
status.dispatcher_ended();
assert!(status.is_active(), "one dispatcher is still running");
status.dispatcher_ended();
assert!(!status.is_active());
status.dispatcher_ended();
assert!(!status.is_active(), "an extra end saturates at zero");
}
#[test]
fn items_in_flight_are_counted_and_the_title_follows_the_latest_start() {
let mut status = RemoteStatus::default();
status.dispatcher_started();
status.item_started("Round Brilliant");
status.item_started("Princess");
assert_eq!(status.in_flight(), 2);
assert_eq!(status.title(), "Princess");
status.item_ended();
assert_eq!(status.in_flight(), 1);
assert_eq!(
status.title(),
"Princess",
"an ending leaves the title alone"
);
status.item_ended();
status.item_ended();
assert_eq!(status.in_flight(), 0, "an extra end saturates at zero");
}
#[test]
fn the_last_dispatcher_out_clears_the_title() {
let mut status = RemoteStatus::default();
status.dispatcher_started();
status.dispatcher_started();
status.item_started("Oval");
status.dispatcher_ended();
assert_eq!(
status.title(),
"Oval",
"another dispatcher is still running"
);
status.dispatcher_ended();
assert_eq!(status.title(), "");
}
#[test]
fn the_local_lane_outlives_every_requeue_of_several_dispatchers() {
const ITEMS: u32 = 600;
let queue = WorkQueue::new(0..ITEMS);
let done = AtomicBool::new(false);
let group = DispatcherGroup::new(4, &done);
let remote_served = Mutex::new(Vec::new());
let local_served = Mutex::new(Vec::new());
std::thread::scope(|scope| {
for _ in 0..4 {
scope.spawn(|| {
let _guard = group.guard();
while let Some(item) = queue.claim_shared() {
if item % 3 == 0 {
std::thread::sleep(Duration::from_micros(40));
queue.return_to_local(item);
} else {
remote_served.lock().unwrap().push(item);
}
}
});
}
scope.spawn(|| {
loop {
let remote_finished = done.load(Ordering::Acquire);
match queue.claim_local() {
Some(item) => local_served.lock().unwrap().push(item),
None if remote_finished => break,
None => std::thread::yield_now(),
}
}
});
});
let mut served = remote_served.into_inner().unwrap();
served.extend(local_served.into_inner().unwrap());
served.sort_unstable();
assert_eq!(served, (0..ITEMS).collect::<Vec<_>>());
}
#[test]
fn a_notice_replaces_the_title_without_counting_an_item() {
let mut status = RemoteStatus::default();
status.dispatcher_started();
status.note("Remote paused after 2 failure(s) -- retrying in 2s");
assert_eq!(status.in_flight(), 0);
assert!(status.title().starts_with("Remote paused"));
}
}