use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Priority {
Low = 0,
Normal = 1,
High = 2,
Critical = 3,
}
impl Priority {
fn rank(self) -> u8 {
self as u8
}
}
impl PartialOrd for Priority {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Priority {
fn cmp(&self, other: &Self) -> Ordering {
self.rank().cmp(&other.rank())
}
}
#[derive(Debug, Clone)]
pub struct RequestInfo {
pub id: String,
pub estimated_bytes: usize,
pub priority: Priority,
submission_order: u64,
}
impl RequestInfo {
pub fn new(id: impl Into<String>, estimated_bytes: usize, priority: Priority) -> Self {
Self {
id: id.into(),
estimated_bytes,
priority,
submission_order: 0, }
}
}
impl PartialEq for RequestInfo {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for RequestInfo {}
impl PartialOrd for RequestInfo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RequestInfo {
fn cmp(&self, other: &Self) -> Ordering {
self.priority
.cmp(&other.priority)
.then(other.submission_order.cmp(&self.submission_order)) }
}
#[derive(Debug, Clone)]
pub struct SlotHandle {
pub id: String,
pub reserved_bytes: usize,
}
#[derive(Debug)]
pub struct MemoryScheduler {
total_budget: usize,
used: usize,
active: HashMap<String, usize>,
wait_queue: BinaryHeap<RequestInfo>,
pressure_threshold: f64,
next_order: u64,
}
impl MemoryScheduler {
pub fn new(total_budget: usize) -> Self {
Self {
total_budget,
used: 0,
active: HashMap::new(),
wait_queue: BinaryHeap::new(),
pressure_threshold: 0.8,
next_order: 0,
}
}
pub fn with_pressure_threshold(mut self, threshold: f64) -> Self {
assert!(
(0.0..=1.0).contains(&threshold),
"threshold must be in [0.0, 1.0]"
);
self.pressure_threshold = threshold;
self
}
pub fn try_admit(&mut self, mut req: RequestInfo) -> Option<SlotHandle> {
req.submission_order = self.next_order;
self.next_order += 1;
let under_pressure = self.usage_fraction() >= self.pressure_threshold;
if under_pressure && req.priority < Priority::High {
self.wait_queue.push(req);
return None;
}
if self.used + req.estimated_bytes > self.total_budget {
self.wait_queue.push(req);
return None;
}
let handle = SlotHandle {
id: req.id.clone(),
reserved_bytes: req.estimated_bytes,
};
self.used += req.estimated_bytes;
self.active.insert(req.id, req.estimated_bytes);
Some(handle)
}
pub fn release(&mut self, id: &str) -> bool {
if let Some(bytes) = self.active.remove(id) {
self.used = self.used.saturating_sub(bytes);
true
} else {
false
}
}
pub fn drain_queue(&mut self) -> Vec<SlotHandle> {
let mut admitted = Vec::new();
let mut remaining = BinaryHeap::new();
while let Some(req) = self.wait_queue.pop() {
let under_pressure = self.usage_fraction() >= self.pressure_threshold;
let pressure_blocked = under_pressure && req.priority < Priority::High;
if !pressure_blocked && self.used + req.estimated_bytes <= self.total_budget {
let handle = SlotHandle {
id: req.id.clone(),
reserved_bytes: req.estimated_bytes,
};
self.used += req.estimated_bytes;
self.active.insert(req.id, req.estimated_bytes);
admitted.push(handle);
} else {
remaining.push(req);
}
}
self.wait_queue = remaining;
admitted
}
pub fn update_usage(&mut self, id: &str, new_bytes: usize) -> bool {
if let Some(old_bytes) = self.active.get_mut(id) {
let old = *old_bytes;
let new_used = self.used - old + new_bytes;
if new_used > self.total_budget {
return false;
}
self.used = new_used;
*old_bytes = new_bytes;
true
} else {
false
}
}
pub fn active_count(&self) -> usize {
self.active.len()
}
pub fn queued_count(&self) -> usize {
self.wait_queue.len()
}
pub fn used_bytes(&self) -> usize {
self.used
}
pub fn total_budget(&self) -> usize {
self.total_budget
}
pub fn remaining_bytes(&self) -> usize {
self.total_budget.saturating_sub(self.used)
}
pub fn usage_fraction(&self) -> f64 {
if self.total_budget == 0 {
return 1.0;
}
self.used as f64 / self.total_budget as f64
}
pub fn under_pressure(&self) -> bool {
self.usage_fraction() >= self.pressure_threshold
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_admit_and_release() {
let mut sched = MemoryScheduler::new(1000);
let handle = sched.try_admit(RequestInfo::new("r1", 300, Priority::Normal));
assert!(handle.is_some());
assert_eq!(sched.active_count(), 1);
assert_eq!(sched.used_bytes(), 300);
assert!(sched.release("r1"));
assert_eq!(sched.active_count(), 0);
assert_eq!(sched.used_bytes(), 0);
}
#[test]
fn budget_overflow_queues() {
let mut sched = MemoryScheduler::new(500);
let h1 = sched.try_admit(RequestInfo::new("r1", 300, Priority::Normal));
assert!(h1.is_some());
let h2 = sched.try_admit(RequestInfo::new("r2", 300, Priority::Normal));
assert!(h2.is_none());
assert_eq!(sched.queued_count(), 1);
}
#[test]
fn drain_queue_on_release() {
let mut sched = MemoryScheduler::new(500);
sched.try_admit(RequestInfo::new("r1", 300, Priority::Normal));
sched.try_admit(RequestInfo::new("r2", 300, Priority::Normal));
assert_eq!(sched.queued_count(), 1);
sched.release("r1");
let admitted = sched.drain_queue();
assert_eq!(admitted.len(), 1);
assert_eq!(admitted[0].id, "r2");
assert_eq!(sched.queued_count(), 0);
}
#[test]
fn pressure_gates_low_priority() {
let mut sched = MemoryScheduler::new(1000).with_pressure_threshold(0.5);
sched.try_admit(RequestInfo::new("r1", 600, Priority::Normal));
let h = sched.try_admit(RequestInfo::new("r2", 100, Priority::Normal));
assert!(h.is_none());
assert_eq!(sched.queued_count(), 1);
let h = sched.try_admit(RequestInfo::new("r3", 100, Priority::High));
assert!(h.is_some());
}
#[test]
fn critical_always_admitted_under_pressure() {
let mut sched = MemoryScheduler::new(1000).with_pressure_threshold(0.5);
sched.try_admit(RequestInfo::new("r1", 800, Priority::Normal));
let h = sched.try_admit(RequestInfo::new("r2", 100, Priority::Critical));
assert!(h.is_some());
}
#[test]
fn priority_ordering_in_queue() {
let mut sched = MemoryScheduler::new(100);
sched.try_admit(RequestInfo::new("active", 100, Priority::Normal));
sched.try_admit(RequestInfo::new("low", 50, Priority::Low));
sched.try_admit(RequestInfo::new("high", 50, Priority::High));
sched.try_admit(RequestInfo::new("normal", 50, Priority::Normal));
sched.release("active");
let admitted = sched.drain_queue();
assert_eq!(admitted[0].id, "high");
}
#[test]
fn update_usage() {
let mut sched = MemoryScheduler::new(1000);
sched.try_admit(RequestInfo::new("r1", 200, Priority::Normal));
assert!(sched.update_usage("r1", 400));
assert_eq!(sched.used_bytes(), 400);
assert!(!sched.update_usage("r1", 1200));
assert_eq!(sched.used_bytes(), 400); }
#[test]
fn usage_fraction_and_remaining() {
let mut sched = MemoryScheduler::new(1000);
sched.try_admit(RequestInfo::new("r1", 250, Priority::Normal));
assert!((sched.usage_fraction() - 0.25).abs() < 1e-10);
assert_eq!(sched.remaining_bytes(), 750);
}
#[test]
fn release_nonexistent() {
let mut sched = MemoryScheduler::new(1000);
assert!(!sched.release("nope"));
}
#[test]
fn zero_budget() {
let mut sched = MemoryScheduler::new(0);
assert!(sched.under_pressure());
let h = sched.try_admit(RequestInfo::new("r1", 1, Priority::Critical));
assert!(h.is_none()); }
#[test]
fn fifo_within_same_priority() {
let mut sched = MemoryScheduler::new(100);
sched.try_admit(RequestInfo::new("active", 100, Priority::Normal));
sched.try_admit(RequestInfo::new("first", 50, Priority::Normal));
sched.try_admit(RequestInfo::new("second", 50, Priority::Normal));
sched.release("active");
let admitted = sched.drain_queue();
assert_eq!(admitted[0].id, "first");
}
}