use std::collections::VecDeque;
use crate::barrier::{BarrierState, CommandQueueMode};
use crate::ids::{ClientId, CommandId, EntityId, Tick};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommandPriority {
Normal,
High,
Low,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommandEnvelope {
pub id: CommandId,
pub client_id: ClientId,
pub entity_id: EntityId,
pub sequence: u64,
pub received_at: Tick,
pub kind: u32,
pub priority: CommandPriority,
pub payload: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CommandDecision {
Accept,
Reject {
reason: CommandRejectReason,
},
FlagSuspicious {
score: u32,
reason: CommandRejectReason,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommandRejectReason {
InvalidSchema,
RateLimited,
ReplayOrStale,
NotOwner,
GameRule,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CommandQueueLimits {
pub high: usize,
pub normal: usize,
pub low: usize,
}
impl Default for CommandQueueLimits {
fn default() -> Self {
Self {
high: 1024,
normal: 8192,
low: 4096,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CommandIngress {
pub barrier_state: BarrierState,
pub command_mode: CommandQueueMode,
}
impl CommandIngress {
pub const RUNNING: Self = Self {
barrier_state: BarrierState::Running,
command_mode: CommandQueueMode::Buffer,
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommandPushOutcome {
Queued,
BufferedByBarrier,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommandQueueError {
QueueFull(CommandPriority),
RejectedByBarrier(CommandQueueMode),
}
impl core::fmt::Display for CommandQueueError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::QueueFull(priority) => write!(f, "{priority:?} command queue is full"),
Self::RejectedByBarrier(mode) => {
write!(f, "command rejected by active barrier mode {mode:?}")
}
}
}
}
impl std::error::Error for CommandQueueError {}
#[derive(Clone, Debug)]
pub struct CommandQueues {
limits: CommandQueueLimits,
high: VecDeque<CommandEnvelope>,
normal: VecDeque<CommandEnvelope>,
low: VecDeque<CommandEnvelope>,
barrier_buffer: VecDeque<CommandEnvelope>,
}
impl CommandQueues {
pub fn new(limits: CommandQueueLimits) -> Self {
Self {
limits,
high: VecDeque::with_capacity(limits.high),
normal: VecDeque::with_capacity(limits.normal),
low: VecDeque::with_capacity(limits.low),
barrier_buffer: VecDeque::new(),
}
}
pub fn push(
&mut self,
command: CommandEnvelope,
ingress: CommandIngress,
) -> Result<CommandPushOutcome, CommandQueueError> {
match ingress.barrier_state {
BarrierState::Running | BarrierState::Resuming => {
self.push_ready(command)?;
Ok(CommandPushOutcome::Queued)
}
BarrierState::Requested | BarrierState::WaitingTickBoundary | BarrierState::Frozen => {
match ingress.command_mode {
CommandQueueMode::Buffer => {
if self.barrier_buffer.len() >= self.barrier_buffer_capacity() {
Err(CommandQueueError::QueueFull(command.priority))
} else {
self.barrier_buffer.push_back(command);
Ok(CommandPushOutcome::BufferedByBarrier)
}
}
CommandQueueMode::Reject | CommandQueueMode::Drain => {
Err(CommandQueueError::RejectedByBarrier(ingress.command_mode))
}
}
}
}
}
pub fn release_barrier_buffer(&mut self) -> Result<usize, CommandQueueError> {
let mut released = 0;
while let Some(priority) = self.barrier_buffer.front().map(|command| command.priority) {
if !self.has_ready_capacity(priority) {
return Err(CommandQueueError::QueueFull(priority));
}
let command = self
.barrier_buffer
.pop_front()
.expect("front command was checked above");
match priority {
CommandPriority::High => self.high.push_back(command),
CommandPriority::Normal => self.normal.push_back(command),
CommandPriority::Low => self.low.push_back(command),
}
released += 1;
}
Ok(released)
}
pub fn clear_barrier_buffer(&mut self) -> usize {
let dropped = self.barrier_buffer.len();
self.barrier_buffer.clear();
dropped
}
pub fn pop_next(&mut self) -> Option<CommandEnvelope> {
self.high
.pop_front()
.or_else(|| self.normal.pop_front())
.or_else(|| self.low.pop_front())
}
pub fn ready_len(&self) -> usize {
self.high.len() + self.normal.len() + self.low.len()
}
pub fn barrier_buffer_len(&self) -> usize {
self.barrier_buffer.len()
}
pub fn barrier_buffer_capacity(&self) -> usize {
self.limits
.high
.saturating_add(self.limits.normal)
.saturating_add(self.limits.low)
}
pub fn total_len(&self) -> usize {
self.ready_len() + self.barrier_buffer.len()
}
pub fn is_empty(&self) -> bool {
self.total_len() == 0
}
fn push_ready(&mut self, command: CommandEnvelope) -> Result<(), CommandQueueError> {
if !self.has_ready_capacity(command.priority) {
return Err(CommandQueueError::QueueFull(command.priority));
}
match command.priority {
CommandPriority::High => {
self.high.push_back(command);
}
CommandPriority::Normal => {
self.normal.push_back(command);
}
CommandPriority::Low => {
self.low.push_back(command);
}
}
Ok(())
}
fn has_ready_capacity(&self, priority: CommandPriority) -> bool {
match priority {
CommandPriority::High => self.high.len() < self.limits.high,
CommandPriority::Normal => self.normal.len() < self.limits.normal,
CommandPriority::Low => self.low.len() < self.limits.low,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn command(id: u64, priority: CommandPriority) -> CommandEnvelope {
CommandEnvelope {
id: CommandId::new(id),
client_id: ClientId::new(1),
entity_id: EntityId::new(10),
sequence: id,
received_at: Tick::new(0),
kind: 1,
priority,
payload: Vec::new(),
}
}
#[test]
fn command_queues_pop_by_priority() {
let mut queues = CommandQueues::new(CommandQueueLimits {
high: 2,
normal: 2,
low: 2,
});
queues
.push(command(1, CommandPriority::Low), CommandIngress::RUNNING)
.expect("low should queue");
queues
.push(command(2, CommandPriority::High), CommandIngress::RUNNING)
.expect("high should queue");
queues
.push(command(3, CommandPriority::Normal), CommandIngress::RUNNING)
.expect("normal should queue");
assert_eq!(queues.pop_next().expect("high").id, CommandId::new(2));
assert_eq!(queues.pop_next().expect("normal").id, CommandId::new(3));
assert_eq!(queues.pop_next().expect("low").id, CommandId::new(1));
}
#[test]
fn barrier_buffer_mode_holds_and_releases_commands() {
let mut queues = CommandQueues::new(CommandQueueLimits::default());
let ingress = CommandIngress {
barrier_state: BarrierState::Frozen,
command_mode: CommandQueueMode::Buffer,
};
let outcome = queues
.push(command(1, CommandPriority::Normal), ingress)
.expect("buffer should work");
assert_eq!(outcome, CommandPushOutcome::BufferedByBarrier);
assert_eq!(queues.ready_len(), 0);
assert_eq!(queues.barrier_buffer_len(), 1);
assert_eq!(
queues
.release_barrier_buffer()
.expect("release should work"),
1
);
assert_eq!(queues.ready_len(), 1);
}
#[test]
fn barrier_reject_mode_rejects_commands() {
let mut queues = CommandQueues::new(CommandQueueLimits::default());
let ingress = CommandIngress {
barrier_state: BarrierState::Frozen,
command_mode: CommandQueueMode::Reject,
};
let error = queues
.push(command(1, CommandPriority::Normal), ingress)
.expect_err("reject mode should reject");
assert_eq!(
error,
CommandQueueError::RejectedByBarrier(CommandQueueMode::Reject)
);
}
#[test]
fn barrier_buffer_is_bounded_by_ready_queue_limits() {
let mut queues = CommandQueues::new(CommandQueueLimits {
high: 1,
normal: 0,
low: 0,
});
let ingress = CommandIngress {
barrier_state: BarrierState::Frozen,
command_mode: CommandQueueMode::Buffer,
};
assert_eq!(queues.barrier_buffer_capacity(), 1);
queues
.push(command(1, CommandPriority::High), ingress)
.expect("first command should buffer");
let error = queues
.push(command(2, CommandPriority::High), ingress)
.expect_err("bounded barrier buffer should reject overflow");
assert_eq!(error, CommandQueueError::QueueFull(CommandPriority::High));
assert_eq!(queues.barrier_buffer_len(), 1);
}
#[test]
fn failed_barrier_release_keeps_blocked_command_buffered() {
let mut queues = CommandQueues::new(CommandQueueLimits {
high: 1,
normal: 0,
low: 0,
});
queues
.push(command(1, CommandPriority::High), CommandIngress::RUNNING)
.expect("ready queue should accept first command");
queues
.push(
command(2, CommandPriority::High),
CommandIngress {
barrier_state: BarrierState::Frozen,
command_mode: CommandQueueMode::Buffer,
},
)
.expect("barrier buffer should accept second command");
let error = queues
.release_barrier_buffer()
.expect_err("full ready queue should block release");
assert_eq!(error, CommandQueueError::QueueFull(CommandPriority::High));
assert_eq!(queues.barrier_buffer_len(), 1);
assert_eq!(
queues.pop_next().expect("first command should remain").id,
CommandId::new(1)
);
assert_eq!(queues.release_barrier_buffer(), Ok(1));
assert_eq!(
queues.pop_next().expect("second command should release").id,
CommandId::new(2)
);
}
}