use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::time::Duration;
use chrono::{DateTime, Utc};
use parking_lot::Mutex;
use super::algorithms::SchedulingAlgorithm;
use super::{NodeResources, Workload};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MissPolicy {
Late,
Drop,
Backpressure {
max_lag: Duration,
},
}
impl Default for MissPolicy {
fn default() -> Self {
MissPolicy::Late
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Eligibility {
Pending,
Eligible,
Late,
Backpressured,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TickOutcome {
Run,
DropFrame,
Hold,
}
#[derive(Debug, Clone)]
pub struct DeadlineEntry {
pub workload: Workload,
pub eligible_time: DateTime<Utc>,
pub virtual_deadline: DateTime<Utc>,
pub tick: Duration,
pub weight: u32,
pub sequence: u64,
}
impl DeadlineEntry {
pub fn new(
workload: Workload,
eligible_time: DateTime<Utc>,
virtual_deadline: DateTime<Utc>,
tick: Duration,
) -> Self {
let weight = (workload.priority.max(0) as u32).saturating_add(1);
Self {
workload,
eligible_time,
virtual_deadline,
tick,
weight,
sequence: 0,
}
}
pub fn from_tick(workload: Workload, next_deadline: DateTime<Utc>, tick: Duration) -> Self {
let tick_chrono = chrono::Duration::from_std(tick)
.unwrap_or_else(|_| chrono::Duration::milliseconds(0));
let eligible_time = next_deadline - tick_chrono;
Self::new(workload, eligible_time, next_deadline, tick)
}
pub fn lag_ms(&self, now: DateTime<Utc>) -> i64 {
let elapsed = (now - self.eligible_time).num_milliseconds();
elapsed.saturating_mul(self.weight as i64)
}
pub fn eligibility(&self, now: DateTime<Utc>, policy: MissPolicy) -> Eligibility {
if now < self.eligible_time {
return Eligibility::Pending;
}
if now <= self.virtual_deadline {
return Eligibility::Eligible;
}
if let MissPolicy::Backpressure { max_lag } = policy {
let max_lag_ms = chrono::Duration::from_std(max_lag)
.map(|d| d.num_milliseconds())
.unwrap_or(i64::MAX);
let overrun_ms = (now - self.virtual_deadline).num_milliseconds();
if overrun_ms > max_lag_ms {
return Eligibility::Backpressured;
}
}
Eligibility::Late
}
pub fn outcome(&self, now: DateTime<Utc>, policy: MissPolicy) -> TickOutcome {
match self.eligibility(now, policy) {
Eligibility::Eligible | Eligibility::Late => match policy {
MissPolicy::Drop if now > self.virtual_deadline => TickOutcome::DropFrame,
_ => TickOutcome::Run,
},
Eligibility::Backpressured => TickOutcome::Hold,
Eligibility::Pending => TickOutcome::Hold,
}
}
pub fn rearm(&mut self, now: DateTime<Utc>, policy: MissPolicy) {
let tick_chrono = chrono::Duration::from_std(self.tick)
.unwrap_or_else(|_| chrono::Duration::milliseconds(0));
match policy {
MissPolicy::Drop if self.tick.as_millis() > 0 && now > self.virtual_deadline => {
let behind_ms = (now - self.virtual_deadline).num_milliseconds().max(0);
let tick_ms = tick_chrono.num_milliseconds().max(1);
let k = behind_ms / tick_ms + 1;
self.virtual_deadline += tick_chrono * (k as i32);
self.eligible_time = self.virtual_deadline - tick_chrono;
}
_ => {
self.eligible_time = self.virtual_deadline;
self.virtual_deadline += tick_chrono;
}
}
}
}
#[derive(Debug, Clone)]
struct Earliest(DeadlineEntry);
impl PartialEq for Earliest {
fn eq(&self, other: &Self) -> bool {
self.0.virtual_deadline == other.0.virtual_deadline
&& self.0.weight == other.0.weight
&& self.0.sequence == other.0.sequence
}
}
impl Eq for Earliest {}
impl PartialOrd for Earliest {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Earliest {
fn cmp(&self, other: &Self) -> Ordering {
other
.0
.virtual_deadline
.cmp(&self.0.virtual_deadline)
.then_with(|| self.0.weight.cmp(&other.0.weight))
.then_with(|| other.0.sequence.cmp(&self.0.sequence))
}
}
pub struct DeadlineQueue {
heap: Mutex<BinaryHeap<Earliest>>,
sequence: Mutex<u64>,
policy: MissPolicy,
}
impl DeadlineQueue {
pub fn new(policy: MissPolicy) -> Self {
Self {
heap: Mutex::new(BinaryHeap::new()),
sequence: Mutex::new(0),
policy,
}
}
pub fn policy(&self) -> MissPolicy {
self.policy
}
pub fn enqueue(&self, mut entry: DeadlineEntry) {
let mut seq = self.sequence.lock();
entry.sequence = *seq;
*seq += 1;
drop(seq);
self.heap.lock().push(Earliest(entry));
}
pub fn len(&self) -> usize {
self.heap.lock().len()
}
pub fn is_empty(&self) -> bool {
self.heap.lock().is_empty()
}
pub fn peek_id(&self) -> Option<String> {
self.heap.lock().peek().map(|e| e.0.workload.id.clone())
}
pub fn pop_earliest(&self) -> Option<DeadlineEntry> {
self.heap.lock().pop().map(|e| e.0)
}
pub fn pop_runnable(&self, now: DateTime<Utc>) -> Option<(DeadlineEntry, TickOutcome)> {
let mut heap = self.heap.lock();
let mut deferred: Vec<Earliest> = Vec::new();
let result = loop {
match heap.pop() {
Some(top) => {
let outcome = top.0.outcome(now, self.policy);
match outcome {
TickOutcome::Hold => {
deferred.push(top);
}
TickOutcome::Run | TickOutcome::DropFrame => {
break Some((top.0, outcome));
}
}
}
None => break None,
}
};
for d in deferred {
heap.push(d);
}
result
}
}
pub struct TickDeadlineScheduler<A: SchedulingAlgorithm> {
inner: A,
urgency_weight: f64,
}
impl<A: SchedulingAlgorithm> TickDeadlineScheduler<A> {
pub fn new(inner: A) -> Self {
Self {
inner,
urgency_weight: 1.0,
}
}
pub fn with_urgency_weight(mut self, w: f64) -> Self {
self.urgency_weight = w;
self
}
}
impl<A: SchedulingAlgorithm> SchedulingAlgorithm for TickDeadlineScheduler<A> {
fn score(&self, workload: &Workload, node: &NodeResources) -> f64 {
let base = self.inner.score(workload, node);
let headroom = if node.cpu_capacity > 0 {
node.cpu_available() as f64 / node.cpu_capacity as f64
} else {
0.0
};
let urgency = (workload.priority.max(0) as f64 / 1000.0).min(1.0);
base + self.urgency_weight * urgency * headroom
}
fn name(&self) -> &str {
"tick-deadline"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scheduler::algorithms::BinPackScheduler;
use crate::types::NodeId;
fn at(ms: i64) -> DateTime<Utc> {
DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap()
+ chrono::Duration::milliseconds(ms)
}
fn entry(id: &str, deadline_ms: i64, priority: i32) -> DeadlineEntry {
let wl = Workload::new(id, id).with_priority(priority);
DeadlineEntry::from_tick(wl, at(deadline_ms), Duration::from_millis(50))
}
#[test]
fn pops_earliest_deadline_first() {
let q = DeadlineQueue::new(MissPolicy::Late);
q.enqueue(entry("c", 300, 0));
q.enqueue(entry("a", 100, 0));
q.enqueue(entry("b", 200, 0));
assert_eq!(q.pop_earliest().unwrap().workload.id, "a");
assert_eq!(q.pop_earliest().unwrap().workload.id, "b");
assert_eq!(q.pop_earliest().unwrap().workload.id, "c");
}
#[test]
fn equal_deadline_breaks_ties_by_weight_then_sequence() {
let q = DeadlineQueue::new(MissPolicy::Late);
q.enqueue(entry("low", 100, 1));
q.enqueue(entry("high", 100, 500));
q.enqueue(entry("low2", 100, 1));
assert_eq!(q.pop_earliest().unwrap().workload.id, "high");
assert_eq!(q.pop_earliest().unwrap().workload.id, "low");
assert_eq!(q.pop_earliest().unwrap().workload.id, "low2");
}
#[test]
fn eligibility_classifies_pending_eligible_late() {
let e = entry("x", 200, 0);
assert_eq!(e.eligibility(at(100), MissPolicy::Late), Eligibility::Pending);
assert_eq!(e.eligibility(at(175), MissPolicy::Late), Eligibility::Eligible);
assert_eq!(e.eligibility(at(200), MissPolicy::Late), Eligibility::Eligible);
assert_eq!(e.eligibility(at(250), MissPolicy::Late), Eligibility::Late);
}
#[test]
fn lag_is_weighted_and_bounded() {
let e0 = entry("a", 200, 0); let e3 = entry("b", 200, 3);
assert_eq!(e0.lag_ms(at(200)), 50);
assert_eq!(e3.lag_ms(at(200)), 200); assert!(e0.lag_ms(at(100)) < 0);
}
#[test]
fn pop_runnable_skips_pending_and_returns_eligible() {
let q = DeadlineQueue::new(MissPolicy::Late);
q.enqueue(entry("pending", 100, 0)); q.enqueue(entry("ready", 500, 0));
let q2 = DeadlineQueue::new(MissPolicy::Late);
let future = DeadlineEntry::from_tick(
Workload::new("future", "future"),
at(1000), Duration::from_millis(50),
);
let ready = DeadlineEntry::from_tick(
Workload::new("ready", "ready"),
at(500), Duration::from_millis(50),
);
q2.enqueue(future);
q2.enqueue(ready);
let (got, outcome) = q2.pop_runnable(at(470)).unwrap();
assert_eq!(got.workload.id, "ready");
assert_eq!(outcome, TickOutcome::Run);
assert_eq!(q2.len(), 1);
assert_eq!(q2.peek_id().as_deref(), Some("future"));
let _ = q.len();
}
#[test]
fn drop_policy_advances_past_missed_frames() {
let policy = MissPolicy::Drop;
let mut e = entry("d", 200, 0);
assert_eq!(e.outcome(at(380), policy), TickOutcome::DropFrame);
e.rearm(at(380), policy);
assert!(e.virtual_deadline >= at(380));
assert_eq!((e.virtual_deadline - at(0)).num_milliseconds(), 400);
assert_eq!((e.eligible_time - at(0)).num_milliseconds(), 350);
}
#[test]
fn late_policy_advances_exactly_one_tick() {
let policy = MissPolicy::Late;
let mut e = entry("l", 200, 0);
assert_eq!(e.outcome(at(280), policy), TickOutcome::Run);
e.rearm(at(280), policy);
assert_eq!((e.eligible_time - at(0)).num_milliseconds(), 200);
assert_eq!((e.virtual_deadline - at(0)).num_milliseconds(), 250);
}
#[test]
fn backpressure_holds_when_lag_exceeds_bound() {
let policy = MissPolicy::Backpressure {
max_lag: Duration::from_millis(100),
};
let e = entry("bp", 200, 0);
assert_eq!(e.eligibility(at(500), policy), Eligibility::Backpressured);
assert_eq!(e.outcome(at(500), policy), TickOutcome::Hold);
assert_eq!(e.eligibility(at(250), policy), Eligibility::Late);
assert_eq!(e.outcome(at(250), policy), TickOutcome::Run);
}
#[test]
fn tick_deadline_scheduler_prefers_emptier_node_for_urgent() {
let sched = TickDeadlineScheduler::new(BinPackScheduler::new());
let urgent = Workload::new("u", "u").with_priority(900);
let mut empty = NodeResources::new(NodeId::new(), 4000, 8192);
let mut busy = NodeResources::new(NodeId::new(), 4000, 8192);
busy.cpu_allocated = 3000;
empty.cpu_allocated = 0;
let s_empty = sched.score(&urgent, &empty);
let s_busy = sched.score(&urgent, &busy);
assert!(
s_empty > s_busy,
"urgent tick should prefer emptier node: empty={s_empty} busy={s_busy}"
);
assert_eq!(sched.name(), "tick-deadline");
}
}