use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use super::facility::{recover, run_facility_loop, run_isolated};
use crate::runtime::task::{MandatoryThread, StackSizeClass, ThreadPriority};
pub type OnceCallback = Box<dyn FnOnce() + Send + 'static>;
pub const DEFAULT_ONCE_QUEUE_SIZE: usize = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScanOnceOverflow;
struct OnceState {
queue: VecDeque<OnceCallback>,
high_water: usize,
overflows: u64,
new_overflow: bool,
shutdown: bool,
}
struct Inner {
capacity: usize,
state: Mutex<OnceState>,
wake: Condvar,
worker: Mutex<Option<JoinHandle<()>>>,
}
impl Inner {
fn ensure_worker(self: &Arc<Self>) {
let mut worker = recover(FACILITY, self.worker.lock());
if worker.is_some() {
return;
}
let worker_inner = Arc::clone(self);
*worker = Some(
MandatoryThread::new(
"scanOnce",
scan_once_priority(),
StackSizeClass::Big,
)
.spawn(move || {
let _watched = crate::runtime::taskwd::taskwd_insert(
"scanOnce",
crate::runtime::taskwd::CheckIn::Unbounded,
None,
);
run_facility_loop(
FACILITY,
|| once_loop(&worker_inner),
|| recover(FACILITY, worker_inner.state.lock()).shutdown = true,
);
}),
);
}
fn scan_once(&self, cb: OnceCallback) -> Result<(), ScanOnceOverflow> {
let mut st = recover(FACILITY, self.state.lock());
if st.shutdown {
drop(st);
tracing::trace!(
target: "epics_base_rs::runtime::scan_once",
"scanOnce after shutdown dropped"
);
return Ok(());
}
let result = if st.queue.len() >= self.capacity {
if st.new_overflow {
tracing::warn!(
target: "epics_base_rs::runtime::scan_once",
"WARNING scanOnce: Ring buffer overflow"
);
}
st.new_overflow = false; st.overflows += 1; Err(ScanOnceOverflow)
} else {
st.new_overflow = true; st.queue.push_back(cb);
st.high_water = st.high_water.max(st.queue.len());
Ok(())
};
drop(st);
self.wake.notify_one();
result
}
fn stats(&self, reset: bool) -> ScanOnceQueueStats {
let mut st = recover(FACILITY, self.state.lock());
let out = ScanOnceQueueStats {
size: self.capacity,
num_used: st.queue.len(),
max_used: st.high_water,
num_overflow: st.overflows,
};
if reset {
st.high_water = 0;
}
out
}
}
const FACILITY: &str = "scanOnce worker";
pub const DEFAULT_PERIODIC_SCAN_BAND_COUNT: usize = 7;
static PERIODIC_SCAN_BAND_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(DEFAULT_PERIODIC_SCAN_BAND_COUNT);
pub fn set_periodic_scan_band_count(n: usize) {
PERIODIC_SCAN_BAND_COUNT.store(n, std::sync::atomic::Ordering::Relaxed);
}
fn scan_once_priority() -> ThreadPriority {
let n = PERIODIC_SCAN_BAND_COUNT.load(std::sync::atomic::Ordering::Relaxed);
ThreadPriority::Custom(ThreadPriority::ScanLow.value() + n.min(u8::MAX as usize) as u8)
}
fn once_loop(inner: &Inner) {
loop {
let mut st = recover(FACILITY, inner.state.lock());
while st.queue.is_empty() && !st.shutdown {
st = recover(FACILITY, inner.wake.wait(st));
}
if st.queue.is_empty() {
return; }
let cb = st.queue.pop_front().unwrap();
drop(st);
run_isolated(FACILITY, cb);
}
}
#[derive(Clone)]
pub struct ScanOnceHandle {
inner: Arc<Inner>,
}
impl ScanOnceHandle {
pub fn scan_once(&self, cb: OnceCallback) -> Result<(), ScanOnceOverflow> {
self.inner.ensure_worker();
self.inner.scan_once(cb)
}
pub fn overflow_count(&self) -> u64 {
recover(FACILITY, self.inner.state.lock()).overflows
}
pub fn stats(&self, reset: bool) -> ScanOnceQueueStats {
self.inner.stats(reset)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScanOnceQueueStats {
pub size: usize,
pub num_used: usize,
pub max_used: usize,
pub num_overflow: u64,
}
static CONFIGURED_ONCE_QUEUE_SIZE: AtomicUsize = AtomicUsize::new(DEFAULT_ONCE_QUEUE_SIZE);
pub fn set_queue_size(size: usize) {
CONFIGURED_ONCE_QUEUE_SIZE.store(size.max(1), Ordering::Relaxed);
}
pub struct ScanOnceQueue {
inner: Arc<Inner>,
}
impl ScanOnceQueue {
pub fn new() -> Self {
Self::with_capacity(CONFIGURED_ONCE_QUEUE_SIZE.load(Ordering::Relaxed))
}
pub fn with_capacity(capacity: usize) -> Self {
let inner = Arc::new(Inner {
capacity: capacity.max(1),
state: Mutex::new(OnceState {
queue: VecDeque::new(),
high_water: 0,
overflows: 0,
new_overflow: true,
shutdown: false,
}),
wake: Condvar::new(),
worker: Mutex::new(None),
});
ScanOnceQueue { inner }
}
pub fn start(&self) {
self.inner.ensure_worker();
}
pub fn handle(&self) -> ScanOnceHandle {
ScanOnceHandle {
inner: Arc::clone(&self.inner),
}
}
pub fn scan_once(&self, cb: OnceCallback) -> Result<(), ScanOnceOverflow> {
self.inner.ensure_worker();
self.inner.scan_once(cb)
}
pub fn overflow_count(&self) -> u64 {
recover(FACILITY, self.inner.state.lock()).overflows
}
pub fn stats(&self, reset: bool) -> ScanOnceQueueStats {
self.inner.stats(reset)
}
}
impl Default for ScanOnceQueue {
fn default() -> Self {
Self::new()
}
}
impl Drop for ScanOnceQueue {
fn drop(&mut self) {
{
let mut st = recover(FACILITY, self.inner.state.lock());
st.shutdown = true;
}
self.inner.wake.notify_all();
let worker = recover(FACILITY, self.inner.worker.lock()).take();
if let Some(w) = worker {
let _ = w.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::time::Duration;
const T: Duration = Duration::from_secs(5);
#[test]
fn scan_once_band_is_scanlow_plus_n_periodic() {
assert_eq!(
scan_once_priority().value(),
ThreadPriority::ScanLow.value() + DEFAULT_PERIODIC_SCAN_BAND_COUNT as u8
);
assert_eq!(scan_once_priority().value(), 67);
}
#[test]
fn a_site_menu_moves_the_band_with_its_rate_count() {
set_periodic_scan_band_count(10);
assert_eq!(scan_once_priority().value(), 70);
set_periodic_scan_band_count(DEFAULT_PERIODIC_SCAN_BAND_COUNT);
assert_eq!(scan_once_priority().value(), 67);
}
#[test]
fn a_panicking_tail_does_not_stop_the_worker() {
let q = ScanOnceQueue::new();
q.scan_once(Box::new(|| panic!("a scanOnce tail panicked")))
.expect("enqueue the panicking tail");
let (tx, rx) = mpsc::channel();
q.scan_once(Box::new(move || tx.send(7u32).unwrap()))
.expect("enqueue the next tail");
assert_eq!(
rx.recv_timeout(T).unwrap(),
7,
"the tail after a panicking one never ran: the worker died with it"
);
}
#[test]
fn enqueue_returns_immediately_and_worker_drains() {
let q = ScanOnceQueue::new();
let (tx, rx) = mpsc::channel();
q.scan_once(Box::new(move || tx.send(7u32).unwrap()))
.unwrap();
assert_eq!(rx.recv_timeout(T).unwrap(), 7);
}
#[test]
fn overflow_latches_and_counts() {
let q = ScanOnceQueue::with_capacity(1);
let (started_tx, started_rx) = mpsc::channel();
let (gate_tx, gate_rx) = mpsc::channel::<()>();
q.scan_once(Box::new(move || {
started_tx.send(()).unwrap();
gate_rx.recv().unwrap();
}))
.unwrap();
started_rx.recv_timeout(T).unwrap();
q.scan_once(Box::new(|| {})).unwrap();
assert_eq!(q.scan_once(Box::new(|| {})), Err(ScanOnceOverflow));
assert_eq!(q.scan_once(Box::new(|| {})), Err(ScanOnceOverflow));
assert_eq!(q.overflow_count(), 2);
gate_tx.send(()).unwrap(); }
#[test]
fn scan_once_after_shutdown_is_silent_noop() {
let q = ScanOnceQueue::new();
let h = q.handle();
drop(q);
let ran = Arc::new(AtomicBool::new(false));
let r = Arc::clone(&ran);
let res = h.scan_once(Box::new(move || r.store(true, Ordering::SeqCst)));
assert_eq!(res, Ok(())); assert!(
!ran.load(Ordering::SeqCst),
"scanOnce tail ran after shutdown; it must be dropped, not processed"
);
assert_eq!(h.overflow_count(), 0); }
}