1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
//! Work for the server's game thread: on the next step, after a delay, or
//! repeatedly. Tasks run after a simulation step, where the game's own server
//! code runs, so they may call into the game.
//!
//! Delays and periods are wall-clock time: after a stall, a task whose time
//! passed runs on the next step. A wait that should stand still with the
//! server counts on the [`StepClock`](crate::time::StepClock) instead. A
//! panic in a task turns the mod off, as in any event handler.
use std::ops::ControlFlow;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock, PoisonError};
use std::time::{Duration, Instant};
/// A task: when it is due, its period when it repeats, and what it runs.
struct Task {
due: Instant,
period: Option<Duration>,
run: Box<dyn FnMut() -> ControlFlow<()> + Send>,
}
struct Queue {
/// Whether a tick handler runs the queue.
ticking: AtomicBool,
/// When the earliest task is due, in nanoseconds from `epoch`, or
/// [`IDLE`]; written under the `tasks` lock, so a step with nothing due
/// takes no lock.
next_due: AtomicU64,
epoch: Instant,
tasks: Mutex<Vec<Task>>,
/// The buffer due tasks run from, kept between runs so a run does not
/// allocate.
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)
}
/// Lowers `next_due` to `due`; called under the `tasks` lock.
fn due_by(&self, due: Instant) {
self.next_due.fetch_min(self.nanos(due), Ordering::AcqRel);
}
}
static QUEUE: OnceLock<Queue> = OnceLock::new();
/// Puts work on the server's game thread; get it from
/// [`Load::scheduler`](crate::Load::scheduler). Copy it anywhere, including
/// other threads.
///
/// # Examples
///
/// ```
/// use std::ops::ControlFlow;
/// use std::time::Duration;
///
/// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
/// let scheduler = load.scheduler()?;
/// scheduler.after(Duration::from_secs(5), || enshwick::info!("five seconds in"));
/// scheduler.every(Duration::from_secs(60), || {
/// enshwick::info!("a minute passed");
/// ControlFlow::Continue(())
/// });
/// Ok(())
/// }
/// ```
#[derive(Clone, Copy)]
pub struct Scheduler {
queue: &'static Queue,
}
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()
}
}
impl Scheduler {
/// The mod's scheduler, and whether the caller is to subscribe the tick
/// that runs it: true once, and again after
/// [`Scheduler::unsubscribed`].
pub(crate) fn get() -> (Self, bool) {
let queue = QUEUE.get_or_init(Queue::new);
let subscribe = !queue.ticking.swap(true, Ordering::AcqRel);
(Self { queue }, subscribe)
}
/// The tick could not be subscribed; the next [`Scheduler::get`] asks
/// for it again.
pub(crate) fn unsubscribed(self) {
self.queue.ticking.store(false, Ordering::Release);
}
/// Runs `task` after the next simulation step.
///
/// # Examples
///
/// See [`Scheduler`].
pub fn next_tick(self, task: impl FnOnce() + Send + 'static) {
self.after(Duration::ZERO, task);
}
/// Runs `task` after the first step at least `delay` from now.
///
/// # Examples
///
/// See [`Scheduler`].
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(())
});
}
/// Runs `task` about every `period`, the first time a period from now,
/// until it returns [`ControlFlow::Break`].
///
/// # Examples
///
/// See [`Scheduler`].
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,
) {
// A delay past what `Instant` holds is never due.
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
.lock()
.unwrap_or_else(PoisonError::into_inner);
self.queue.due_by(task.due);
tasks.push(task);
}
/// Runs the tasks that are due; called after each step.
#[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
.lock()
.unwrap_or_else(PoisonError::into_inner),
);
{
let mut tasks = self
.queue
.tasks
.lock()
.unwrap_or_else(PoisonError::into_inner);
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,
);
}
// Outside the lock: a task may schedule more.
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
.lock()
.unwrap_or_else(PoisonError::into_inner);
for task in &due {
self.queue.due_by(task.due);
}
tasks.append(&mut due);
}
*self
.queue
.spare
.lock()
.unwrap_or_else(PoisonError::into_inner) = 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();
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();
scheduler.every(Duration::ZERO, || ControlFlow::Continue(()));
scheduler.run_due();
let lock = |m: &'static Mutex<Vec<Task>>| m.lock().unwrap_or_else(PoisonError::into_inner);
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 a_delay_past_what_time_holds_never_runs() {
let (scheduler, _) = Scheduler::get();
scheduler.after(Duration::MAX, || unreachable!());
scheduler.every(Duration::MAX, || unreachable!());
scheduler.run_due();
}
}