use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use tokio::{task::JoinHandle, sync::Notify};
pub struct BatchWorkerHandle {
running: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
notifier: Arc<Notify>,
}
impl BatchWorkerHandle {
pub fn new<F>(task: F) -> Self
where
F: FnOnce(Arc<AtomicBool>, Arc<Notify>) -> JoinHandle<()> + Send + 'static,
{
let running = Arc::new(AtomicBool::new(true));
let notifier = Arc::new(Notify::new());
let handle = task(running.clone(), notifier.clone());
Self {
running,
handle: Some(handle),
notifier,
}
}
pub fn notify(&self) {
self.notifier.notify_one();
}
#[allow(dead_code)]
pub fn running(&self) -> Arc<AtomicBool> {
self.running.clone()
}
#[allow(dead_code)]
pub fn notifier(&self) -> Arc<Notify> {
self.notifier.clone()
}
pub fn shutdown(&mut self) {
self.running.store(false, Ordering::SeqCst);
self.notifier.notify_one();
if let Some(handle) = self.handle.take() {
tokio::spawn(async move {
let _ = handle.await;
});
}
}
}
impl Drop for BatchWorkerHandle {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use std::time::Duration;
use tokio::time;
#[tokio::test]
async fn test_worker_starts_running() {
let worker = BatchWorkerHandle::new(|running, _notifier| {
tokio::spawn(async move {
while running.load(Ordering::SeqCst) {
time::sleep(Duration::from_millis(10)).await;
}
})
});
assert!(worker.running().load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_worker_notifies() {
let notification_count = Arc::new(Mutex::new(0));
let notification_count_clone = notification_count.clone();
let worker = BatchWorkerHandle::new(|running, notifier| {
tokio::spawn(async move {
while running.load(Ordering::SeqCst) {
notifier.notified().await;
let mut count = notification_count_clone.lock().unwrap();
*count += 1;
}
})
});
time::sleep(Duration::from_millis(50)).await;
worker.notify();
time::sleep(Duration::from_millis(50)).await;
assert_eq!(*notification_count.lock().unwrap(), 1);
worker.notify();
time::sleep(Duration::from_millis(50)).await;
assert_eq!(*notification_count.lock().unwrap(), 2);
}
#[tokio::test]
async fn test_worker_shutdown() {
let is_shutdown = Arc::new(AtomicBool::new(false));
let is_shutdown_clone = is_shutdown.clone();
let mut worker = BatchWorkerHandle::new(|running, notifier| {
tokio::spawn(async move {
while running.load(Ordering::SeqCst) {
notifier.notified().await;
}
is_shutdown_clone.store(true, Ordering::SeqCst);
})
});
assert!(worker.running().load(Ordering::SeqCst));
worker.notify();
time::sleep(Duration::from_millis(50)).await;
worker.shutdown();
time::sleep(Duration::from_millis(100)).await;
assert!(!worker.running().load(Ordering::SeqCst));
assert!(is_shutdown.load(Ordering::SeqCst));
assert!(worker.handle.is_none());
}
#[tokio::test]
async fn test_worker_drop_triggers_shutdown() {
let is_shutdown = Arc::new(AtomicBool::new(false));
let is_shutdown_clone = is_shutdown.clone();
{
let worker = BatchWorkerHandle::new(|running, notifier| {
tokio::spawn(async move {
while running.load(Ordering::SeqCst) {
notifier.notified().await;
}
is_shutdown_clone.store(true, Ordering::SeqCst);
})
});
worker.notify();
time::sleep(Duration::from_millis(50)).await;
}
time::sleep(Duration::from_millis(100)).await;
assert!(is_shutdown.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_worker_can_access_notifier() {
let notification_received = Arc::new(AtomicBool::new(false));
let notification_received_clone = notification_received.clone();
let worker = BatchWorkerHandle::new(|running, _notifier| {
let notifier_clone = Arc::clone(&_notifier);
tokio::spawn(async move {
while running.load(Ordering::SeqCst) {
notifier_clone.notified().await;
notification_received_clone.store(true, Ordering::SeqCst);
}
})
});
let notifier = worker.notifier();
notifier.notify_one();
time::sleep(Duration::from_millis(50)).await;
assert!(notification_received.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_shutdown_after_handle_already_taken() {
let mut worker = BatchWorkerHandle::new(|running, _notifier| {
tokio::spawn(async move {
while running.load(Ordering::SeqCst) {
time::sleep(Duration::from_millis(10)).await;
}
})
});
let _ = worker.handle.take();
worker.shutdown();
assert!(!worker.running().load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_multiple_shutdowns() {
let mut worker = BatchWorkerHandle::new(|running, _notifier| {
tokio::spawn(async move {
while running.load(Ordering::SeqCst) {
time::sleep(Duration::from_millis(10)).await;
}
})
});
worker.shutdown();
worker.shutdown();
worker.shutdown();
assert!(!worker.running().load(Ordering::SeqCst));
}
}