Skip to main content

oximedia_distributed/
task_priority_queue.rs

1#![allow(dead_code)]
2//! Priority-based task scheduling queue.
3//!
4//! A multi-level priority queue that orders tasks by priority, deadline, and
5//! submission time, ensuring critical work is scheduled first while preventing
6//! starvation of lower-priority tasks through an aging mechanism.
7
8use std::cmp::Ordering;
9use std::collections::BinaryHeap;
10use std::fmt;
11
12/// Priority level for tasks.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum Priority {
15    /// Background / best-effort.
16    Low,
17    /// Default priority.
18    Normal,
19    /// Elevated priority.
20    High,
21    /// Must be processed immediately.
22    Critical,
23}
24
25impl Priority {
26    /// Numeric weight (higher = more urgent).
27    fn weight(self) -> u32 {
28        match self {
29            Self::Low => 0,
30            Self::Normal => 1,
31            Self::High => 2,
32            Self::Critical => 3,
33        }
34    }
35}
36
37impl fmt::Display for Priority {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::Low => write!(f, "Low"),
41            Self::Normal => write!(f, "Normal"),
42            Self::High => write!(f, "High"),
43            Self::Critical => write!(f, "Critical"),
44        }
45    }
46}
47
48/// A task entry in the priority queue.
49#[derive(Debug, Clone)]
50pub struct PriorityTask {
51    /// Unique task identifier.
52    pub task_id: String,
53    /// Base priority.
54    pub priority: Priority,
55    /// Submission timestamp (ms since epoch).
56    pub submitted_at: u64,
57    /// Optional deadline (ms since epoch). `None` means no deadline.
58    pub deadline: Option<u64>,
59    /// Number of aging bumps applied.
60    pub age_bumps: u32,
61    /// Estimated processing time in milliseconds.
62    pub estimated_duration_ms: u64,
63}
64
65impl PriorityTask {
66    /// Create a new task.
67    pub fn new(task_id: impl Into<String>, priority: Priority, submitted_at: u64) -> Self {
68        Self {
69            task_id: task_id.into(),
70            priority,
71            submitted_at,
72            deadline: None,
73            age_bumps: 0,
74            estimated_duration_ms: 0,
75        }
76    }
77
78    /// Set a deadline.
79    #[must_use]
80    pub fn with_deadline(mut self, deadline_ms: u64) -> Self {
81        self.deadline = Some(deadline_ms);
82        self
83    }
84
85    /// Set estimated duration.
86    #[must_use]
87    pub fn with_estimated_duration(mut self, ms: u64) -> Self {
88        self.estimated_duration_ms = ms;
89        self
90    }
91
92    /// Effective priority weight including aging.
93    fn effective_weight(&self) -> u32 {
94        self.priority.weight() + self.age_bumps
95    }
96
97    /// Effective sort key: (`effective_weight`, `has_deadline`, `inverse_deadline`, `earlier_submit`).
98    ///
99    /// Higher weight first; among equal weight, deadline tasks first (earlier deadline wins);
100    /// among equal, earlier submission wins.
101    fn sort_key(&self) -> (u32, bool, u64, u64) {
102        let has_dl = self.deadline.is_some();
103        let inverse_dl = self.deadline.map_or(0, |d| u64::MAX - d);
104        let inverse_submit = u64::MAX - self.submitted_at;
105        (self.effective_weight(), has_dl, inverse_dl, inverse_submit)
106    }
107}
108
109impl PartialEq for PriorityTask {
110    fn eq(&self, other: &Self) -> bool {
111        self.task_id == other.task_id
112    }
113}
114
115impl Eq for PriorityTask {}
116
117impl PartialOrd for PriorityTask {
118    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
119        Some(self.cmp(other))
120    }
121}
122
123impl Ord for PriorityTask {
124    fn cmp(&self, other: &Self) -> Ordering {
125        self.sort_key().cmp(&other.sort_key())
126    }
127}
128
129/// A priority queue for tasks.
130#[derive(Debug)]
131pub struct TaskPriorityQueue {
132    /// The underlying max-heap.
133    heap: BinaryHeap<PriorityTask>,
134    /// Maximum capacity (0 = unlimited).
135    capacity: usize,
136    /// Number of age bumps to apply per aging cycle.
137    age_bump_amount: u32,
138}
139
140impl TaskPriorityQueue {
141    /// Create a new unbounded queue.
142    #[must_use]
143    pub fn new() -> Self {
144        Self {
145            heap: BinaryHeap::new(),
146            capacity: 0,
147            age_bump_amount: 1,
148        }
149    }
150
151    /// Create a queue with a maximum capacity.
152    #[must_use]
153    pub fn with_capacity(capacity: usize) -> Self {
154        Self {
155            heap: BinaryHeap::with_capacity(capacity),
156            capacity,
157            age_bump_amount: 1,
158        }
159    }
160
161    /// Set the aging bump amount.
162    pub fn set_age_bump_amount(&mut self, amount: u32) {
163        self.age_bump_amount = amount;
164    }
165
166    /// Push a task into the queue.
167    ///
168    /// Returns `false` if the queue is at capacity.
169    pub fn push(&mut self, task: PriorityTask) -> bool {
170        if self.capacity > 0 && self.heap.len() >= self.capacity {
171            return false;
172        }
173        self.heap.push(task);
174        true
175    }
176
177    /// Pop the highest-priority task.
178    pub fn pop(&mut self) -> Option<PriorityTask> {
179        self.heap.pop()
180    }
181
182    /// Peek at the highest-priority task without removing it.
183    #[must_use]
184    pub fn peek(&self) -> Option<&PriorityTask> {
185        self.heap.peek()
186    }
187
188    /// Number of tasks in the queue.
189    #[must_use]
190    pub fn len(&self) -> usize {
191        self.heap.len()
192    }
193
194    /// Whether the queue is empty.
195    #[must_use]
196    pub fn is_empty(&self) -> bool {
197        self.heap.is_empty()
198    }
199
200    /// Clear all tasks.
201    pub fn clear(&mut self) {
202        self.heap.clear();
203    }
204
205    /// Apply aging: bump the priority of all non-critical tasks.
206    ///
207    /// This prevents starvation of low-priority tasks by gradually
208    /// increasing their effective priority.
209    pub fn apply_aging(&mut self) {
210        let bump = self.age_bump_amount;
211        let items: Vec<PriorityTask> = self.heap.drain().collect();
212        for mut task in items {
213            if task.priority != Priority::Critical {
214                task.age_bumps += bump;
215            }
216            self.heap.push(task);
217        }
218    }
219
220    /// Remove all tasks that have passed their deadline.
221    ///
222    /// `now_ms` is the current timestamp in milliseconds.
223    /// Returns the expired tasks.
224    pub fn remove_expired(&mut self, now_ms: u64) -> Vec<PriorityTask> {
225        let mut expired = Vec::new();
226        let mut kept = Vec::new();
227        for task in self.heap.drain() {
228            if let Some(dl) = task.deadline {
229                if dl < now_ms {
230                    expired.push(task);
231                    continue;
232                }
233            }
234            kept.push(task);
235        }
236        for task in kept {
237            self.heap.push(task);
238        }
239        expired
240    }
241
242    /// Drain all tasks with a given priority.
243    pub fn drain_priority(&mut self, priority: Priority) -> Vec<PriorityTask> {
244        let mut matched = Vec::new();
245        let mut rest = Vec::new();
246        for task in self.heap.drain() {
247            if task.priority == priority {
248                matched.push(task);
249            } else {
250                rest.push(task);
251            }
252        }
253        for task in rest {
254            self.heap.push(task);
255        }
256        matched
257    }
258
259    /// Get queue capacity (0 = unlimited).
260    #[must_use]
261    pub fn capacity(&self) -> usize {
262        self.capacity
263    }
264
265    /// Attempt to preempt (replace) the lowest-priority task if `incoming`
266    /// has strictly higher effective weight.
267    ///
268    /// Returns the displaced task if preemption occurred, or `None` when the
269    /// queue is not full, the queue is empty, or `incoming` does not outrank
270    /// the current minimum.
271    ///
272    /// Preemption is only meaningful when the queue is at capacity; callers
273    /// should call `push` directly when capacity has not been reached.
274    pub fn try_preempt(&mut self, incoming: PriorityTask) -> Option<PriorityTask> {
275        if self.capacity == 0 || self.heap.len() < self.capacity {
276            // Not at capacity — no preemption needed.
277            return None;
278        }
279        // Find the minimum-priority task in the heap.
280        let min_weight = self.heap.iter().map(|t| t.effective_weight()).min()?;
281
282        if incoming.effective_weight() <= min_weight {
283            // Incoming task is not strictly better — do not preempt.
284            return None;
285        }
286
287        // Extract all tasks, remove the first minimum-weight one, put the
288        // rest back, then add the incoming task.
289        let mut items: Vec<PriorityTask> = self.heap.drain().collect();
290        let victim_idx = items
291            .iter()
292            .position(|t| t.effective_weight() == min_weight)?;
293        let victim = items.remove(victim_idx);
294        for task in items {
295            self.heap.push(task);
296        }
297        self.heap.push(incoming);
298        Some(victim)
299    }
300
301    /// Check whether the given task would preempt the current lowest-priority
302    /// occupant without actually performing the preemption.
303    #[must_use]
304    pub fn would_preempt(&self, incoming: &PriorityTask) -> bool {
305        if self.capacity == 0 || self.heap.len() < self.capacity {
306            return false;
307        }
308        let min_weight = self.heap.iter().map(|t| t.effective_weight()).min();
309        min_weight.is_some_and(|m| incoming.effective_weight() > m)
310    }
311}
312
313impl Default for TaskPriorityQueue {
314    fn default() -> Self {
315        Self::new()
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_priority_weight() {
325        assert!(Priority::Critical.weight() > Priority::High.weight());
326        assert!(Priority::High.weight() > Priority::Normal.weight());
327        assert!(Priority::Normal.weight() > Priority::Low.weight());
328    }
329
330    #[test]
331    fn test_priority_display() {
332        assert_eq!(Priority::Low.to_string(), "Low");
333        assert_eq!(Priority::Critical.to_string(), "Critical");
334    }
335
336    #[test]
337    fn test_new_queue_is_empty() {
338        let q = TaskPriorityQueue::new();
339        assert!(q.is_empty());
340        assert_eq!(q.len(), 0);
341    }
342
343    #[test]
344    fn test_push_and_pop_single() {
345        let mut q = TaskPriorityQueue::new();
346        let task = PriorityTask::new("t1", Priority::Normal, 100);
347        assert!(q.push(task));
348        assert_eq!(q.len(), 1);
349        let popped = q.pop().expect("pop should return a value");
350        assert_eq!(popped.task_id, "t1");
351        assert!(q.is_empty());
352    }
353
354    #[test]
355    fn test_pop_order_by_priority() {
356        let mut q = TaskPriorityQueue::new();
357        q.push(PriorityTask::new("low", Priority::Low, 100));
358        q.push(PriorityTask::new("high", Priority::High, 100));
359        q.push(PriorityTask::new("normal", Priority::Normal, 100));
360        assert_eq!(q.pop().expect("pop should return a value").task_id, "high");
361        assert_eq!(
362            q.pop().expect("pop should return a value").task_id,
363            "normal"
364        );
365        assert_eq!(q.pop().expect("pop should return a value").task_id, "low");
366    }
367
368    #[test]
369    fn test_same_priority_earlier_submit_first() {
370        let mut q = TaskPriorityQueue::new();
371        q.push(PriorityTask::new("later", Priority::Normal, 200));
372        q.push(PriorityTask::new("earlier", Priority::Normal, 100));
373        assert_eq!(
374            q.pop().expect("pop should return a value").task_id,
375            "earlier"
376        );
377    }
378
379    #[test]
380    fn test_deadline_tasks_preferred() {
381        let mut q = TaskPriorityQueue::new();
382        q.push(PriorityTask::new("no_dl", Priority::Normal, 100));
383        q.push(PriorityTask::new("with_dl", Priority::Normal, 100).with_deadline(5000));
384        assert_eq!(
385            q.pop().expect("pop should return a value").task_id,
386            "with_dl"
387        );
388    }
389
390    #[test]
391    fn test_earlier_deadline_first() {
392        let mut q = TaskPriorityQueue::new();
393        q.push(PriorityTask::new("late_dl", Priority::Normal, 100).with_deadline(9000));
394        q.push(PriorityTask::new("early_dl", Priority::Normal, 100).with_deadline(3000));
395        assert_eq!(
396            q.pop().expect("pop should return a value").task_id,
397            "early_dl"
398        );
399    }
400
401    #[test]
402    fn test_capacity_limit() {
403        let mut q = TaskPriorityQueue::with_capacity(2);
404        assert!(q.push(PriorityTask::new("t1", Priority::Normal, 100)));
405        assert!(q.push(PriorityTask::new("t2", Priority::Normal, 200)));
406        assert!(!q.push(PriorityTask::new("t3", Priority::Normal, 300)));
407        assert_eq!(q.len(), 2);
408    }
409
410    #[test]
411    fn test_peek() {
412        let mut q = TaskPriorityQueue::new();
413        q.push(PriorityTask::new("t1", Priority::High, 100));
414        let peeked = q.peek().expect("peek should return a value");
415        assert_eq!(peeked.task_id, "t1");
416        assert_eq!(q.len(), 1); // not removed
417    }
418
419    #[test]
420    fn test_clear() {
421        let mut q = TaskPriorityQueue::new();
422        q.push(PriorityTask::new("t1", Priority::Normal, 100));
423        q.push(PriorityTask::new("t2", Priority::Normal, 200));
424        q.clear();
425        assert!(q.is_empty());
426    }
427
428    #[test]
429    fn test_apply_aging() {
430        let mut q = TaskPriorityQueue::new();
431        q.push(PriorityTask::new("low", Priority::Low, 100));
432        q.push(PriorityTask::new("crit", Priority::Critical, 100));
433        // After 3 aging cycles, low's effective weight = 0 + 3 = 3
434        q.apply_aging();
435        q.apply_aging();
436        q.apply_aging();
437        // Low now has same effective weight as critical (3), but critical is not aged
438        let first = q.pop().expect("pop should return a value");
439        // Both have weight 3, tied; order depends on secondary criteria
440        assert!(first.task_id == "low" || first.task_id == "crit");
441    }
442
443    #[test]
444    fn test_remove_expired() {
445        let mut q = TaskPriorityQueue::new();
446        q.push(PriorityTask::new("expired", Priority::Normal, 100).with_deadline(500));
447        q.push(PriorityTask::new("active", Priority::Normal, 100).with_deadline(2000));
448        q.push(PriorityTask::new("no_dl", Priority::Normal, 100));
449        let expired = q.remove_expired(1000);
450        assert_eq!(expired.len(), 1);
451        assert_eq!(expired[0].task_id, "expired");
452        assert_eq!(q.len(), 2);
453    }
454
455    #[test]
456    fn test_drain_priority() {
457        let mut q = TaskPriorityQueue::new();
458        q.push(PriorityTask::new("h1", Priority::High, 100));
459        q.push(PriorityTask::new("n1", Priority::Normal, 100));
460        q.push(PriorityTask::new("h2", Priority::High, 200));
461        let high_tasks = q.drain_priority(Priority::High);
462        assert_eq!(high_tasks.len(), 2);
463        assert_eq!(q.len(), 1);
464    }
465
466    #[test]
467    fn test_with_estimated_duration() {
468        let task = PriorityTask::new("t1", Priority::Normal, 100).with_estimated_duration(5000);
469        assert_eq!(task.estimated_duration_ms, 5000);
470    }
471
472    // ── Preemption ───────────────────────────────────────────────────────
473
474    #[test]
475    fn test_preempt_replaces_lowest_priority() {
476        let mut q = TaskPriorityQueue::with_capacity(2);
477        q.push(PriorityTask::new("low", Priority::Low, 100));
478        q.push(PriorityTask::new("normal", Priority::Normal, 100));
479        assert_eq!(q.len(), 2);
480
481        // A Critical task should preempt the Low task.
482        let victim = q
483            .try_preempt(PriorityTask::new("crit", Priority::Critical, 200))
484            .expect("preemption should succeed");
485        assert_eq!(victim.task_id, "low");
486        assert_eq!(q.len(), 2);
487        // Queue should now contain normal + crit.
488        let first = q.pop().expect("pop should return a value");
489        assert_eq!(first.task_id, "crit");
490    }
491
492    #[test]
493    fn test_preempt_not_triggered_when_not_at_capacity() {
494        let mut q = TaskPriorityQueue::with_capacity(5);
495        q.push(PriorityTask::new("t1", Priority::Low, 100));
496
497        // Queue has space; preemption should not fire.
498        let result = q.try_preempt(PriorityTask::new("crit", Priority::Critical, 200));
499        assert!(result.is_none());
500    }
501
502    #[test]
503    fn test_preempt_not_triggered_equal_priority() {
504        let mut q = TaskPriorityQueue::with_capacity(1);
505        q.push(PriorityTask::new("existing", Priority::High, 100));
506
507        // Incoming has same weight as minimum — should not preempt.
508        let result = q.try_preempt(PriorityTask::new("newcomer", Priority::High, 200));
509        assert!(result.is_none());
510    }
511
512    #[test]
513    fn test_would_preempt_logic() {
514        let mut q = TaskPriorityQueue::with_capacity(1);
515        q.push(PriorityTask::new("low", Priority::Low, 100));
516
517        let crit = PriorityTask::new("crit", Priority::Critical, 200);
518        let low2 = PriorityTask::new("low2", Priority::Low, 300);
519
520        assert!(q.would_preempt(&crit));
521        assert!(!q.would_preempt(&low2));
522    }
523
524    #[test]
525    fn test_preempt_unlimited_queue_returns_none() {
526        let mut q = TaskPriorityQueue::new(); // no capacity limit
527        q.push(PriorityTask::new("t1", Priority::Low, 100));
528
529        let result = q.try_preempt(PriorityTask::new("crit", Priority::Critical, 200));
530        assert!(result.is_none());
531    }
532}