use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch;
const PAUSE_ACK_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) struct ScanGate {
pause_tx: watch::Sender<bool>,
scanning_tx: watch::Sender<bool>,
lock: Arc<tokio::sync::Mutex<()>>,
}
impl ScanGate {
pub(crate) fn new() -> Arc<Self> {
Arc::new(Self {
pause_tx: watch::channel(false).0,
scanning_tx: watch::channel(false).0,
lock: Arc::new(tokio::sync::Mutex::new(())),
})
}
pub(crate) async fn pause(self: &Arc<Self>) -> ScanPauseGuard {
let permit = Arc::clone(&self.lock).lock_owned().await;
let guard = ScanPauseGuard {
gate: Arc::clone(self),
_permit: permit,
};
self.pause_tx.send_replace(true);
let mut scanning = self.scanning_tx.subscribe();
let _ = tokio::time::timeout(PAUSE_ACK_TIMEOUT, async {
while *scanning.borrow_and_update() {
if scanning.changed().await.is_err() {
break;
}
}
})
.await;
guard
}
pub(crate) fn pause_watch(&self) -> watch::Receiver<bool> {
self.pause_tx.subscribe()
}
pub(crate) fn set_scanning(&self, scanning: bool) {
self.scanning_tx.send_replace(scanning);
}
}
pub(crate) struct ScanPauseGuard {
gate: Arc<ScanGate>,
_permit: tokio::sync::OwnedMutexGuard<()>,
}
impl Drop for ScanPauseGuard {
fn drop(&mut self) {
self.gate.pause_tx.send_replace(false);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn pause_waits_for_scanner_ack_and_drop_resumes() {
let gate = ScanGate::new();
gate.set_scanning(true);
let scanner_gate = gate.clone();
let mut pause_rx = gate.pause_watch();
let scanner = tokio::spawn(async move {
while !*pause_rx.borrow_and_update() {
if pause_rx.changed().await.is_err() {
return;
}
}
scanner_gate.set_scanning(false);
});
let guard = gate.pause().await;
assert!(!*gate.scanning_tx.subscribe().borrow());
scanner.await.unwrap();
let mut pause_rx = gate.pause_watch();
assert!(*pause_rx.borrow_and_update());
drop(guard);
assert!(!*pause_rx.borrow_and_update());
}
#[tokio::test(start_paused = true)]
async fn pause_without_scanner_is_immediate() {
let gate = ScanGate::new();
let before = tokio::time::Instant::now();
let _guard = gate.pause().await;
assert_eq!(tokio::time::Instant::now(), before);
}
#[tokio::test(start_paused = true)]
async fn pause_times_out_on_wedged_scanner() {
let gate = ScanGate::new();
gate.set_scanning(true); let before = tokio::time::Instant::now();
let _guard = gate.pause().await;
assert!(tokio::time::Instant::now().duration_since(before) >= PAUSE_ACK_TIMEOUT);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn concurrent_pauses_serialize() {
let gate = ScanGate::new();
let g1 = gate.pause().await;
let gate2 = gate.clone();
let second = tokio::spawn(async move {
let _g2 = gate2.pause().await;
});
tokio::task::yield_now().await;
assert!(!second.is_finished());
drop(g1);
second.await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn cancelled_pause_resumes_the_scan_request() {
let gate = ScanGate::new();
gate.set_scanning(true); let cancelled = tokio::time::timeout(std::time::Duration::from_secs(1), gate.pause()).await;
assert!(cancelled.is_err());
assert!(!*gate.pause_watch().borrow());
gate.set_scanning(false);
let _guard = gate.pause().await;
}
}