#[derive(Clone, Copy, Debug)]
pub struct InputWindow {
pub max_late: u64,
pub max_early: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Submission {
Scheduled,
Late,
TickClosed,
TooFarAhead,
}
impl Submission {
pub fn accepted(&self) -> bool {
matches!(self, Submission::Scheduled | Submission::Late)
}
}
#[derive(Clone, Debug, Default)]
pub struct InputSchedule<Input> {
queue: Vec<(u64, Input)>,
last_executed: Option<u64>,
accepted: u64,
late: u64,
rejected_late: u64,
rejected_early: u64,
last_reject_margin: Option<i64>,
}
impl<Input> InputSchedule<Input> {
pub fn new() -> Self {
Self {
queue: Vec::new(),
last_executed: None,
accepted: 0,
late: 0,
rejected_late: 0,
rejected_early: 0,
last_reject_margin: None,
}
}
pub fn submit(&mut self, tick: u64, input: Input, current: u64, window: InputWindow) -> Submission {
if tick + window.max_late < current {
self.rejected_late += 1;
self.last_reject_margin = Some(tick as i64 - current as i64);
return Submission::TickClosed;
}
if tick > current + window.max_early {
self.rejected_early += 1;
self.last_reject_margin = Some(tick as i64 - current as i64);
return Submission::TooFarAhead;
}
let mut execute_at = tick;
if let Some(last) = self.last_executed {
execute_at = execute_at.max(last);
}
self.queue.push((execute_at, input));
self.accepted += 1;
if execute_at < current {
self.late += 1;
return Submission::Late;
}
Submission::Scheduled
}
pub fn execute_due(&mut self, current: u64) -> Option<Input> {
let mut newest = None;
for input in self.drain_due(current) {
newest = Some(input);
}
newest
}
pub fn drain_due(&mut self, current: u64) -> impl Iterator<Item = Input> + '_ {
self.queue.sort_by_key(|(at, _)| *at);
let due = self.queue.iter().take_while(|(at, _)| *at <= current).count();
if due > 0 {
self.last_executed = Some(current);
}
self.queue.drain(..due).map(|(_, input)| input)
}
pub fn clear(&mut self) {
self.queue.clear();
self.last_executed = None;
}
pub fn accepted(&self) -> u64 {
self.accepted
}
pub fn late(&self) -> u64 {
self.late
}
pub fn rejected(&self) -> u64 {
self.rejected_late + self.rejected_early
}
pub fn rejected_split(&self) -> (u64, u64) {
(self.rejected_late, self.rejected_early)
}
pub fn last_reject_margin(&self) -> Option<i64> {
self.last_reject_margin
}
}
#[cfg(test)]
mod tests {
use super::*;
const WINDOW: InputWindow = InputWindow { max_late: 4, max_early: 30 };
mod reading_the_rejections {
use super::*;
#[test]
fn the_split_says_which_way_a_client_is_lying() {
let mut s = InputSchedule::new();
assert_eq!(s.rejected_split(), (0, 0));
s.submit(10, "backdated", 20, WINDOW);
assert_eq!(s.rejected_split(), (1, 0), "one behind, none ahead");
s.submit(500, "from the future", 20, WINDOW);
assert_eq!(s.rejected_split(), (1, 1));
assert_eq!(s.rejected(), 2, "and the total is still the sum");
}
#[test]
fn the_margin_is_none_until_something_is_rejected() {
let mut s = InputSchedule::new();
assert_eq!(s.last_reject_margin(), None);
s.submit(10, "fine", 6, WINDOW);
assert_eq!(s.last_reject_margin(), None, "an accepted input is not a rejection");
}
#[test]
fn the_margin_is_signed_so_the_direction_survives() {
let mut s = InputSchedule::new();
s.submit(10, "backdated", 20, WINDOW);
let behind = s.last_reject_margin().expect("rejected");
assert!(behind < 0, "behind the simulation reads negative: {behind}");
s.submit(500, "from the future", 20, WINDOW);
let ahead = s.last_reject_margin().expect("rejected");
assert!(ahead > 0, "and ahead of it reads positive: {ahead}");
}
}
#[test]
fn an_input_waits_for_the_tick_it_names() {
let mut s = InputSchedule::new();
assert_eq!(s.submit(10, "go", 6, WINDOW), Submission::Scheduled);
assert_eq!(s.execute_due(8), None, "not its tick yet");
assert_eq!(s.execute_due(10), Some("go"));
}
#[test]
fn a_closed_tick_is_rejected_not_corrected() {
let mut s = InputSchedule::new();
assert_eq!(s.submit(10, "backdated", 20, WINDOW), Submission::TickClosed);
assert_eq!(s.execute_due(20), None, "dropped, not shifted");
assert_eq!(s.rejected(), 1);
assert_eq!(s.accepted(), 0);
}
#[test]
fn parking_an_input_in_the_future_is_rejected() {
let mut s = InputSchedule::new();
assert_eq!(s.submit(100, "parked", 10, WINDOW), Submission::TooFarAhead);
assert_eq!(s.execute_due(100), None);
}
#[test]
fn late_inside_the_window_executes_next_tick_and_is_counted() {
let mut s = InputSchedule::new();
assert_eq!(s.submit(10, "late", 12, WINDOW), Submission::Late);
assert_eq!(s.late(), 1);
assert_eq!(s.execute_due(12), Some("late"), "applied on the next tick to run");
}
#[test]
fn a_client_cannot_reorder_its_own_history() {
let mut s = InputSchedule::new();
let _ = s.submit(12, "first", 12, WINDOW);
assert_eq!(s.execute_due(12), Some("first"));
let _ = s.submit(10, "rewound", 12, WINDOW);
assert_eq!(s.execute_due(12), Some("rewound"), "clamped to the executed frontier, not earlier");
}
#[test]
fn within_one_step_the_newest_due_input_wins() {
let mut s = InputSchedule::new();
let _ = s.submit(10, "a", 9, WINDOW);
let _ = s.submit(11, "b", 9, WINDOW);
let _ = s.submit(12, "c", 9, WINDOW);
assert_eq!(s.execute_due(12), Some("c"));
assert_eq!(s.execute_due(13), None, "the superseded ones are gone, not deferred");
}
#[test]
fn discrete_actions_due_together_are_all_delivered_in_order() {
let mut s = InputSchedule::new();
let _ = s.submit(10, "fire", 9, WINDOW);
let _ = s.submit(11, "jump", 9, WINDOW);
let _ = s.submit(12, "fire", 9, WINDOW);
let due: Vec<_> = s.drain_due(12).collect();
assert_eq!(due, ["fire", "jump", "fire"], "every action, in the order intended");
assert_eq!(s.drain_due(13).count(), 0);
let _ = s.submit(10, "rewound", 12, WINDOW);
let late: Vec<_> = s.drain_due(12).collect();
assert_eq!(late, ["rewound"], "clamped to the executed frontier, not replayed into the past");
}
#[test]
fn clearing_a_vacated_seat_keeps_the_sessions_counters() {
let mut s = InputSchedule::new();
let _ = s.submit(10, "x", 10, WINDOW);
s.clear();
assert_eq!(s.execute_due(10), None);
assert_eq!(s.accepted(), 1, "counters describe the session, not the occupant");
}
}