use std::collections::VecDeque;
use crate::ids::{EntityId, EventId, OwnerEpoch, StationId, Tick};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EventPriority {
Critical,
Important,
BestEffort,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EventKind {
Custom(u32),
HandoffPrepare {
entity_id: EntityId,
},
HandoffCommit {
entity_id: EntityId,
owner_epoch: OwnerEpoch,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StationEvent {
pub id: EventId,
pub source: StationId,
pub target: StationId,
pub source_tick: Tick,
pub target_tick: Tick,
pub priority: EventPriority,
pub kind: EventKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EventQueueLimits {
pub critical: usize,
pub important: usize,
pub best_effort: usize,
}
impl Default for EventQueueLimits {
fn default() -> Self {
Self {
critical: 1024,
important: 4096,
best_effort: 8192,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PushOutcome {
Accepted,
DroppedOldestBestEffort,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EventQueueError {
QueueFull(EventPriority),
}
impl core::fmt::Display for EventQueueError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::QueueFull(priority) => write!(f, "{priority:?} event queue is full"),
}
}
}
impl std::error::Error for EventQueueError {}
#[derive(Clone, Debug)]
pub struct EventQueues {
limits: EventQueueLimits,
critical: VecDeque<StationEvent>,
important: VecDeque<StationEvent>,
best_effort: VecDeque<StationEvent>,
}
impl EventQueues {
pub fn new(limits: EventQueueLimits) -> Self {
Self {
limits,
critical: VecDeque::new(),
important: VecDeque::new(),
best_effort: VecDeque::new(),
}
}
pub fn push(&mut self, event: StationEvent) -> Result<PushOutcome, EventQueueError> {
match event.priority {
EventPriority::Critical => {
if self.critical.len() >= self.limits.critical {
Err(EventQueueError::QueueFull(EventPriority::Critical))
} else {
self.critical.push_back(event);
Ok(PushOutcome::Accepted)
}
}
EventPriority::Important => {
if self.important.len() >= self.limits.important {
Err(EventQueueError::QueueFull(EventPriority::Important))
} else {
self.important.push_back(event);
Ok(PushOutcome::Accepted)
}
}
EventPriority::BestEffort => {
let outcome = if self.best_effort.len() >= self.limits.best_effort {
self.best_effort.pop_front();
PushOutcome::DroppedOldestBestEffort
} else {
PushOutcome::Accepted
};
self.best_effort.push_back(event);
Ok(outcome)
}
}
}
pub fn pop_next(&mut self) -> Option<StationEvent> {
self.critical
.pop_front()
.or_else(|| self.important.pop_front())
.or_else(|| self.best_effort.pop_front())
}
pub fn drain_ready_into(&mut self, current_tick: Tick, out: &mut Vec<StationEvent>) -> usize {
let before = out.len();
drain_priority_ready(&mut self.critical, current_tick, out);
drain_priority_ready(&mut self.important, current_tick, out);
drain_priority_ready(&mut self.best_effort, current_tick, out);
out.len() - before
}
pub fn len(&self) -> usize {
self.critical.len() + self.important.len() + self.best_effort.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn retained_capacity(&self, priority: EventPriority) -> usize {
match priority {
EventPriority::Critical => self.critical.capacity(),
EventPriority::Important => self.important.capacity(),
EventPriority::BestEffort => self.best_effort.capacity(),
}
}
pub fn total_retained_capacity(&self) -> usize {
self.critical
.capacity()
.saturating_add(self.important.capacity())
.saturating_add(self.best_effort.capacity())
}
}
fn drain_priority_ready(
queue: &mut VecDeque<StationEvent>,
current_tick: Tick,
out: &mut Vec<StationEvent>,
) {
let queued = queue.len();
for _ in 0..queued {
let event = queue
.pop_front()
.expect("initial queue length bounds the drain loop");
if event.target_tick <= current_tick {
out.push(event);
} else {
queue.push_back(event);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::EventId;
fn event(id: u64, priority: EventPriority, target_tick: u64) -> StationEvent {
StationEvent {
id: EventId::new(id),
source: StationId::new(1),
target: StationId::new(2),
source_tick: Tick::new(0),
target_tick: Tick::new(target_tick),
priority,
kind: EventKind::Custom(u32::try_from(id).expect("test id fits u32")),
}
}
#[test]
fn ready_drain_preserves_priority_fifo_and_delayed_order() {
let mut queues = EventQueues::new(EventQueueLimits {
critical: 8,
important: 8,
best_effort: 8,
});
for value in [
event(1, EventPriority::Critical, 3),
event(2, EventPriority::Critical, 1),
event(3, EventPriority::Important, 1),
event(4, EventPriority::Important, 4),
event(5, EventPriority::BestEffort, 1),
event(6, EventPriority::BestEffort, 5),
] {
queues.push(value).expect("test queue has capacity");
}
let mut ready = Vec::with_capacity(8);
assert_eq!(queues.drain_ready_into(Tick::new(1), &mut ready), 3);
assert_eq!(
ready.iter().map(|event| event.id).collect::<Vec<_>>(),
[EventId::new(2), EventId::new(3), EventId::new(5)]
);
assert_eq!(queues.len(), 3);
ready.clear();
assert_eq!(queues.drain_ready_into(Tick::new(5), &mut ready), 3);
assert_eq!(
ready.iter().map(|event| event.id).collect::<Vec<_>>(),
[EventId::new(1), EventId::new(4), EventId::new(6)]
);
assert!(queues.is_empty());
}
#[test]
fn event_queues_allocate_lazily_and_retain_peak_capacity() {
let mut queues = EventQueues::new(EventQueueLimits::default());
assert_eq!(queues.total_retained_capacity(), 0);
for (offset, priority) in [
EventPriority::Critical,
EventPriority::Important,
EventPriority::BestEffort,
]
.into_iter()
.enumerate()
{
for index in 0..8 {
queues
.push(event(
u64::try_from(offset * 8 + index).expect("test id fits u64"),
priority,
0,
))
.expect("event burst should queue");
}
assert!(queues.retained_capacity(priority) >= 8);
}
let peak = queues.total_retained_capacity();
while queues.pop_next().is_some() {}
assert_eq!(queues.total_retained_capacity(), peak);
}
}