use std::collections::{BTreeMap, BTreeSet};
use std::future;
use std::sync::mpsc::{Receiver as StdReceiver, Sender as StdSender, TryRecvError};
use std::task::Poll;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use bamts_runtime::{TimerError, TimerProvider, TimerWakeup};
use tokio::runtime::Builder;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
use tokio_util::time::{DelayQueue, delay_queue};
enum Command {
Schedule {
id: u64,
deadline_ms: u64,
delay: Duration,
},
Cancel { id: u64 },
}
enum Event {
Command(Option<Command>),
Expired(Option<delay_queue::Expired<TimerWakeup>>),
}
struct Worker {
command_tx: Option<UnboundedSender<Command>>,
expiry_rx: StdReceiver<TimerWakeup>,
handle: Option<JoinHandle<()>>,
}
impl Worker {
fn spawn() -> Result<Self, TimerError> {
let (command_tx, command_rx) = unbounded_channel::<Command>();
let (expiry_tx, expiry_rx) = std::sync::mpsc::channel::<TimerWakeup>();
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<(), String>>();
let handle = std::thread::Builder::new()
.name("bamts-node-timers".to_owned())
.spawn(move || run_worker(command_rx, expiry_tx, &ready_tx))
.map_err(|error| {
TimerError::new(format!("timer worker thread failed to start: {error}"))
})?;
match ready_rx.recv() {
Ok(Ok(())) => Ok(Self {
command_tx: Some(command_tx),
expiry_rx,
handle: Some(handle),
}),
Ok(Err(message)) => {
let _ = handle.join();
Err(TimerError::new(message))
}
Err(_) => {
let _ = handle.join();
Err(TimerError::new(
"timer worker exited before signalling readiness",
))
}
}
}
fn send(&self, command: Command) -> Result<(), TimerError> {
self.command_tx
.as_ref()
.expect("an active worker retains its command sender")
.send(command)
.map_err(|_| TimerError::new("timer worker stopped accepting commands"))
}
}
impl Drop for Worker {
fn drop(&mut self) {
self.command_tx.take();
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
fn run_worker(
command_rx: UnboundedReceiver<Command>,
expiry_tx: StdSender<TimerWakeup>,
ready_tx: &StdSender<Result<(), String>>,
) {
let runtime = match Builder::new_current_thread().enable_time().build() {
Ok(runtime) => runtime,
Err(error) => {
let _ = ready_tx.send(Err(format!("timer runtime failed to build: {error}")));
return;
}
};
if ready_tx.send(Ok(())).is_err() {
return;
}
runtime.block_on(worker_loop(command_rx, expiry_tx));
}
async fn worker_loop(
mut command_rx: UnboundedReceiver<Command>,
expiry_tx: StdSender<TimerWakeup>,
) {
let mut queue: DelayQueue<TimerWakeup> = DelayQueue::new();
let mut keys: BTreeMap<u64, delay_queue::Key> = BTreeMap::new();
loop {
let event = future::poll_fn(|cx| {
if let Poll::Ready(command) = command_rx.poll_recv(cx) {
return Poll::Ready(Event::Command(command));
}
if !queue.is_empty()
&& let Poll::Ready(expired) = queue.poll_expired(cx)
{
return Poll::Ready(Event::Expired(expired));
}
Poll::Pending
})
.await;
match event {
Event::Command(Some(Command::Schedule {
id,
deadline_ms,
delay,
})) => {
let key = queue.insert(TimerWakeup { id, deadline_ms }, delay);
keys.insert(id, key);
}
Event::Command(Some(Command::Cancel { id })) => {
if let Some(key) = keys.remove(&id) {
queue.try_remove(&key);
}
}
Event::Command(None) => break,
Event::Expired(Some(expired)) => {
let wakeup = expired.into_inner();
keys.remove(&wakeup.id);
if expiry_tx.send(wakeup).is_err() {
break;
}
}
Event::Expired(None) => {}
}
}
}
pub(crate) struct NodeTimers {
base: Instant,
pending: BTreeSet<u64>,
worker: Option<Worker>,
}
impl NodeTimers {
pub(crate) fn new() -> Self {
Self {
base: Instant::now(),
pending: BTreeSet::new(),
worker: None,
}
}
fn deadline_ms(&self, delay_ms: u32) -> u64 {
let elapsed = u64::try_from(self.base.elapsed().as_millis()).unwrap_or(u64::MAX);
elapsed.saturating_add(u64::from(delay_ms))
}
fn worker(&mut self) -> Result<&Worker, TimerError> {
if self.worker.is_none() {
self.worker = Some(Worker::spawn()?);
}
Ok(self
.worker
.as_ref()
.expect("worker was just initialised above"))
}
#[cfg(test)]
pub(crate) fn worker_active(&self) -> bool {
self.worker.is_some()
}
}
impl TimerProvider for NodeTimers {
fn schedule(&mut self, id: u64, delay_ms: u32) -> Result<u64, TimerError> {
let deadline_ms = self.deadline_ms(delay_ms);
let worker = self.worker()?;
worker.send(Command::Schedule {
id,
deadline_ms,
delay: Duration::from_millis(u64::from(delay_ms)),
})?;
self.pending.insert(id);
Ok(deadline_ms)
}
fn cancel(&mut self, id: u64) -> Result<bool, TimerError> {
if !self.pending.remove(&id) {
return Ok(false);
}
if let Some(worker) = self.worker.as_ref() {
worker.send(Command::Cancel { id })?;
}
Ok(true)
}
fn poll_expired(&mut self, output: &mut Vec<TimerWakeup>) -> Result<(), TimerError> {
let Some(worker) = self.worker.as_ref() else {
return Ok(());
};
loop {
match worker.expiry_rx.try_recv() {
Ok(wakeup) => {
if self.pending.remove(&wakeup.id) {
output.push(wakeup);
}
}
Err(TryRecvError::Empty) => return Ok(()),
Err(TryRecvError::Disconnected) => {
return Err(TimerError::new("timer worker expiry channel disconnected"));
}
}
}
}
fn wait_expired(&mut self) -> Result<Option<TimerWakeup>, TimerError> {
loop {
if self.pending.is_empty() {
return Ok(None);
}
let Some(worker) = self.worker.as_ref() else {
return Ok(None);
};
match worker.expiry_rx.recv() {
Ok(wakeup) => {
if self.pending.remove(&wakeup.id) {
return Ok(Some(wakeup));
}
}
Err(_) => {
return Err(TimerError::new("timer worker expiry channel disconnected"));
}
}
}
}
fn has_pending(&self) -> bool {
!self.pending.is_empty()
}
}