use std::{
cell::{Cell, RefCell},
fmt::Debug,
future::Future,
pin::Pin,
sync::Arc,
};
use async_channel::{Receiver, Sender};
use crate::error::Error;
#[allow(async_fn_in_trait)]
pub trait QueuedOperation: Debug {
async fn perform(&self);
}
pub trait ErasedQueuedOperation: Debug {
fn perform<'op>(&'op self) -> Pin<Box<dyn Future<Output = ()> + 'op>>;
}
impl<T> ErasedQueuedOperation for T
where
T: QueuedOperation,
{
fn perform<'op>(&'op self) -> Pin<Box<dyn Future<Output = ()> + 'op>> {
Box::pin(self.perform())
}
}
pub struct OperationQueue {
channel_sender: Sender<Box<dyn ErasedQueuedOperation>>,
channel_receiver: Receiver<Box<dyn ErasedQueuedOperation>>,
runners: RefCell<Vec<Arc<Runner>>>,
spawn_task: fn(fut: Pin<Box<dyn Future<Output = ()>>>),
}
impl OperationQueue {
pub fn new(spawn_task: fn(fut: Pin<Box<dyn Future<Output = ()>>>)) -> OperationQueue {
let (snd, rcv) = async_channel::unbounded();
OperationQueue {
channel_sender: snd,
channel_receiver: rcv,
runners: RefCell::new(Vec::new()),
spawn_task,
}
}
pub fn start(&self, runners: u32) -> Result<(), Error> {
if self.channel_sender.is_closed() {
return Err(Error::Stopped);
}
for i in 0..runners {
let runner = Runner::new(i, self.channel_receiver.clone());
(self.spawn_task)(Box::pin(runner.clone().run()));
self.runners.borrow_mut().push(runner);
}
Ok(())
}
pub async fn enqueue(&self, op: Box<dyn ErasedQueuedOperation>) -> Result<(), Error> {
self.channel_sender.send(op).await?;
Ok(())
}
pub async fn stop(&self) {
if !self.channel_sender.close() {
log::warn!("request queue: attempted to close channel that's already closed");
}
self.runners.borrow_mut().clear();
}
pub fn running(&self) -> bool {
let active_runners =
self.count_matching_runners(|runner| !matches!(runner.state(), RunnerState::Stopped));
log::debug!("{active_runners} runner(s) currently active");
active_runners > 0
}
pub fn idle(&self) -> bool {
let idle_runners =
self.count_matching_runners(|runner| matches!(runner.state(), RunnerState::Waiting));
log::debug!("{idle_runners} runner(s) currently idle");
idle_runners == self.runners.borrow().len()
}
fn count_matching_runners<PredicateT>(&self, predicate: PredicateT) -> usize
where
PredicateT: FnMut(&&Arc<Runner>) -> bool,
{
self.runners.borrow().iter().filter(predicate).count()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum RunnerState {
Pending,
Waiting,
Running,
Stopped,
}
struct Runner {
receiver: Receiver<Box<dyn ErasedQueuedOperation>>,
state: Cell<RunnerState>,
id: u32,
}
impl Runner {
#[allow(clippy::arc_with_non_send_sync)]
fn new(id: u32, receiver: Receiver<Box<dyn ErasedQueuedOperation>>) -> Arc<Runner> {
Arc::new(Runner {
id,
receiver,
state: Cell::new(RunnerState::Pending),
})
}
async fn run(self: Arc<Runner>) {
loop {
self.state.replace(RunnerState::Waiting);
let op = match self.receiver.recv().await {
Ok(op) => op,
Err(_) => {
log::info!(
"request queue: channel has closed (likely due to client shutdown), exiting the loop"
);
self.state.replace(RunnerState::Stopped);
return;
}
};
self.state.replace(RunnerState::Running);
log::info!(
"operation_queue::Runner: runner {} performing op: {op:?}",
self.id
);
op.perform().await;
}
}
fn state(&self) -> RunnerState {
self.state.get()
}
}
#[cfg(test)]
#[allow(unexpected_cfgs)]
mod tests {
use super::*;
use async_channel::Sender;
use tokio::time::Duration;
fn new_queue() -> OperationQueue {
OperationQueue::new(|fut| {
_ = tokio::task::spawn_local(fut);
})
}
#[tokio::test(flavor = "local")]
async fn start_queue() {
let queue = new_queue();
queue.start(5).unwrap();
assert_eq!(queue.runners.borrow().len(), 5);
tokio::time::sleep(Duration::from_millis(0)).await;
assert!(queue.idle());
}
#[tokio::test(flavor = "local")]
async fn stop_queue() {
let queue = new_queue();
queue.start(5).unwrap();
tokio::time::sleep(Duration::from_millis(0)).await;
assert!(queue.idle());
queue.stop().await;
assert!(!queue.running());
assert!(queue.channel_receiver.is_closed());
match queue.start(1) {
Ok(_) => panic!("we should not be able to start the queue after stopping it"),
Err(Error::Stopped) => (),
Err(_) => panic!("unexpected error"),
}
#[derive(Debug)]
struct Operation {}
impl QueuedOperation for Operation {
async fn perform(&self) {}
}
let op = Box::new(Operation {});
match queue.enqueue(op).await {
Ok(_) => panic!("we should not be able to enqueue operations after stopping the queue"),
Err(Error::Sender) => (),
Err(_) => panic!("unexpected error"),
}
}
#[tokio::test(flavor = "local")]
async fn operation_order() {
#[derive(Debug)]
struct Operation {
id: u8,
sender: Sender<u8>,
}
impl QueuedOperation for Operation {
async fn perform(&self) {
self.sender.send(self.id).await.unwrap();
}
}
let queue = new_queue();
let (sender, receiver) = async_channel::unbounded();
queue
.enqueue(Box::new(Operation {
id: 1,
sender: sender.clone(),
}))
.await
.unwrap();
queue
.enqueue(Box::new(Operation {
id: 2,
sender: sender.clone(),
}))
.await
.unwrap();
queue.start(1).unwrap();
tokio::time::sleep(Duration::from_millis(0)).await;
let id = receiver.recv().await.unwrap();
assert_eq!(id, 1);
let id = receiver.recv().await.unwrap();
assert_eq!(id, 2);
assert!(queue.idle());
}
}