use std::collections::VecDeque;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use super::facility::{recover, run_facility_loop, run_isolated};
use crate::runtime::task::{StackSizeClass, ThreadPriority, enter_ioc_thread};
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>,
overflows: u64,
new_overflow: bool,
shutdown: bool,
}
struct Inner {
capacity: usize,
state: Mutex<OnceState>,
wake: Condvar,
}
impl Inner {
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);
Ok(())
};
drop(st);
self.wake.notify_one();
result
}
}
const FACILITY: &str = "scanOnce worker";
pub const PERIODIC_SCAN_BAND_COUNT: usize = 7;
fn scan_once_priority() -> ThreadPriority {
ThreadPriority::Custom(ThreadPriority::ScanLow.value() + PERIODIC_SCAN_BAND_COUNT 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.scan_once(cb)
}
pub fn overflow_count(&self) -> u64 {
recover(FACILITY, self.inner.state.lock()).overflows
}
}
pub struct ScanOnceQueue {
inner: Arc<Inner>,
worker: Option<JoinHandle<()>>,
}
impl ScanOnceQueue {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_ONCE_QUEUE_SIZE)
}
pub fn with_capacity(capacity: usize) -> Self {
let inner = Arc::new(Inner {
capacity: capacity.max(1),
state: Mutex::new(OnceState {
queue: VecDeque::new(),
overflows: 0,
new_overflow: true,
shutdown: false,
}),
wake: Condvar::new(),
});
let worker_inner = Arc::clone(&inner);
let worker = std::thread::Builder::new()
.name("scanOnce".to_string())
.stack_size(StackSizeClass::Big.bytes())
.spawn(move || {
let _ = enter_ioc_thread(scan_once_priority());
run_facility_loop(
FACILITY,
|| once_loop(&worker_inner),
|| recover(FACILITY, worker_inner.state.lock()).shutdown = true,
);
})
.expect("failed to spawn scanOnce worker thread");
ScanOnceQueue {
inner,
worker: Some(worker),
}
}
pub fn handle(&self) -> ScanOnceHandle {
ScanOnceHandle {
inner: Arc::clone(&self.inner),
}
}
pub fn scan_once(&self, cb: OnceCallback) -> Result<(), ScanOnceOverflow> {
self.inner.scan_once(cb)
}
pub fn overflow_count(&self) -> u64 {
recover(FACILITY, self.inner.state.lock()).overflows
}
}
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();
if let Some(w) = self.worker.take() {
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() + PERIODIC_SCAN_BAND_COUNT as u8
);
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); }
}