use std::ops::ControlFlow;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{Duration, Instant};
use crate::abi::ModHandle;
use crate::locks::Relock;
struct Task {
due: Instant,
period: Option<Duration>,
run: Box<dyn FnMut() -> ControlFlow<()> + Send>,
}
struct Queue {
ticking: AtomicBool,
next_due: AtomicU64,
epoch: Instant,
tasks: Mutex<Vec<Task>>,
spare: Mutex<Vec<Task>>,
}
const IDLE: u64 = u64::MAX;
impl Queue {
fn new() -> Self {
Self {
ticking: AtomicBool::new(false),
next_due: AtomicU64::new(IDLE),
epoch: Instant::now(),
tasks: Mutex::new(Vec::new()),
spare: Mutex::new(Vec::new()),
}
}
fn nanos(&self, at: Instant) -> u64 {
u64::try_from(at.saturating_duration_since(self.epoch).as_nanos()).unwrap_or(IDLE - 1)
}
fn due_by(&self, due: Instant) {
self.next_due.fetch_min(self.nanos(due), Ordering::AcqRel);
}
}
static QUEUES: crate::fresh::Fresh<Mutex<Vec<(ModHandle, &'static Queue)>>> =
crate::fresh::Fresh::new();
#[derive(Clone, Copy)]
pub struct Scheduler {
queue: &'static Queue,
}
impl crate::script::Bound for Scheduler {
const NAME: &'static str = "scheduler";
const DOC: &'static str =
"Work for the server's game thread: on the next pass, after a delay, or repeatedly.";
const PARTS: &'static [&'static [crate::script::Function<Self>]] = &[Self::SCRIPT_TASKS];
fn resolve(load: &mut crate::Load<'_>) -> Result<Self, crate::Off> {
load.scheduler()
}
}
impl core::fmt::Debug for Scheduler {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Scheduler")
.field(
"pending",
&(self.queue.next_due.load(Ordering::Relaxed) != IDLE),
)
.finish()
}
}
#[crate::script(tasks)]
impl Scheduler {
pub(crate) fn get(me: ModHandle) -> (Self, bool) {
let queue = {
let mut queues = QUEUES.get_or_init(|| Mutex::new(Vec::new())).relock();
if let Some(&(_, queue)) = queues.iter().find(|(handle, _)| *handle == me) {
queue
} else {
let queue: &'static Queue = Box::leak(Box::new(Queue::new()));
queues.push((me, queue));
queue
}
};
let subscribe = !queue.ticking.swap(true, Ordering::AcqRel);
(Self { queue }, subscribe)
}
pub(crate) fn unsubscribed(self) {
self.queue.ticking.store(false, Ordering::Release);
}
#[script]
pub fn next_tick(self, task: impl FnOnce() + Send + 'static) {
self.after(Duration::ZERO, task);
}
#[script]
pub fn after(self, delay: Duration, task: impl FnOnce() + Send + 'static) {
let mut task = Some(task);
self.push(delay, None, move || {
if let Some(task) = task.take() {
task();
}
ControlFlow::Break(())
});
}
#[script]
pub fn every(self, period: Duration, task: impl FnMut() -> ControlFlow<()> + Send + 'static) {
self.push(period, Some(period), task);
}
fn push(
self,
delay: Duration,
period: Option<Duration>,
run: impl FnMut() -> ControlFlow<()> + Send + 'static,
) {
let Some(due) = Instant::now().checked_add(delay) else {
return;
};
let task = Task {
due,
period,
run: Box::new(run),
};
let mut tasks = self.queue.tasks.relock();
self.queue.due_by(task.due);
tasks.push(task);
}
#[inline]
pub(crate) fn run_due(self) {
let next_due = self.queue.next_due.load(Ordering::Acquire);
if next_due == IDLE {
return;
}
let now = Instant::now();
if self.queue.nanos(now) >= next_due {
self.run_due_at(now);
}
}
#[cold]
fn run_due_at(self, now: Instant) {
let mut due = core::mem::take(&mut *self.queue.spare.relock());
{
let mut tasks = self.queue.tasks.relock();
due.extend(tasks.extract_if(.., |task| task.due <= now));
let next = tasks.iter().map(|task| task.due).min();
self.queue.next_due.store(
next.map_or(IDLE, |at| self.queue.nanos(at)),
Ordering::Release,
);
}
due.retain_mut(|task| match ((task.run)(), task.period) {
(ControlFlow::Continue(()), Some(period)) => match now.checked_add(period) {
Some(at) => {
task.due = at;
true
}
None => false,
},
_ => false,
});
{
let mut tasks = self.queue.tasks.relock();
for task in &due {
self.queue.due_by(task.due);
}
tasks.append(&mut due);
}
*self.queue.spare.relock() = due;
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
use super::*;
#[test]
fn tasks_run_when_due_and_repeat_until_they_stop() {
let (scheduler, _) = Scheduler::get(ModHandle(1));
let once = Arc::new(AtomicU32::new(0));
let repeated = Arc::new(AtomicU32::new(0));
let later = Arc::new(AtomicU32::new(0));
let count = Arc::clone(&once);
scheduler.next_tick(move || {
count.fetch_add(1, Ordering::Relaxed);
});
let count = Arc::clone(&repeated);
scheduler.every(Duration::ZERO, move || {
if count.fetch_add(1, Ordering::Relaxed) + 1 < 3 {
ControlFlow::Continue(())
} else {
ControlFlow::Break(())
}
});
let count = Arc::clone(&later);
scheduler.after(Duration::from_secs(3600), move || {
count.fetch_add(1, Ordering::Relaxed);
});
for _ in 0..5 {
scheduler.run_due();
}
assert_eq!(once.load(Ordering::Relaxed), 1);
assert_eq!(repeated.load(Ordering::Relaxed), 3);
assert_eq!(later.load(Ordering::Relaxed), 0);
assert_ne!(scheduler.queue.next_due.load(Ordering::Relaxed), IDLE);
}
#[test]
fn runs_keep_their_buffers_between_steps() {
let (scheduler, _) = Scheduler::get(ModHandle(1));
scheduler.every(Duration::ZERO, || ControlFlow::Continue(()));
scheduler.run_due();
let lock = |m: &'static Mutex<Vec<Task>>| m.relock();
let spare = lock(&scheduler.queue.spare).capacity();
let tasks = lock(&scheduler.queue.tasks).capacity();
assert!(spare >= 1);
scheduler.run_due();
assert_eq!(lock(&scheduler.queue.spare).capacity(), spare);
assert_eq!(lock(&scheduler.queue.tasks).capacity(), tasks);
assert_eq!(lock(&scheduler.queue.tasks).len(), 1);
}
#[test]
fn each_mod_has_a_queue_of_its_own() {
let (first, subscribe_first) = Scheduler::get(ModHandle(7));
let (second, subscribe_second) = Scheduler::get(ModHandle(8));
assert!(subscribe_first && subscribe_second);
assert!(!core::ptr::eq(first.queue, second.queue));
let (again, subscribe_again) = Scheduler::get(ModHandle(7));
assert!(core::ptr::eq(first.queue, again.queue));
assert!(!subscribe_again);
}
#[test]
fn a_delay_past_what_time_holds_never_runs() {
let (scheduler, _) = Scheduler::get(ModHandle(1));
scheduler.after(Duration::MAX, || unreachable!());
scheduler.every(Duration::MAX, || unreachable!());
scheduler.run_due();
}
}