Skip to main content

oxirs_core/sla/
priority_dispatcher.rs

1//! Priority-based query dispatcher for SLA-aware scheduling.
2//!
3//! [`PriorityDispatcher`] is a max-heap where each entry carries the
4//! [`SlaClass::dispatch_priority`] of the originating tenant.  Dequeuing
5//! always returns the highest-priority pending query first:
6//!
7//! ```text
8//! Platinum (4) > Gold (3) > Silver (2) > Bronze (1)
9//! ```
10
11use std::cmp::Ordering;
12use std::collections::BinaryHeap;
13
14use super::class::SlaClass;
15
16// ─────────────────────────────────────────────────────────────────────────────
17// PrioritizedQuery
18// ─────────────────────────────────────────────────────────────────────────────
19
20/// A query payload annotated with its dispatch priority and originating tenant.
21pub struct PrioritizedQuery<T> {
22    /// Numeric priority — higher value means earlier dequeue.
23    pub priority: u8,
24    /// Identifier of the tenant that submitted the query.
25    pub tenant_id: String,
26    /// Monotonic insertion sequence used to break priority ties (lower comes first).
27    pub sequence: u64,
28    /// The query payload (type-erased by the caller).
29    pub payload: T,
30}
31
32// Manual trait impls so we can use PrioritizedQuery<T> in a BinaryHeap
33// without requiring T: Ord.
34
35impl<T> PartialEq for PrioritizedQuery<T> {
36    fn eq(&self, other: &Self) -> bool {
37        self.priority == other.priority && self.sequence == other.sequence
38    }
39}
40
41impl<T> Eq for PrioritizedQuery<T> {}
42
43impl<T> PartialOrd for PrioritizedQuery<T> {
44    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45        Some(self.cmp(other))
46    }
47}
48
49impl<T> Ord for PrioritizedQuery<T> {
50    fn cmp(&self, other: &Self) -> Ordering {
51        // Higher priority first, then earlier sequence first.
52        self.priority
53            .cmp(&other.priority)
54            .then_with(|| other.sequence.cmp(&self.sequence))
55    }
56}
57
58// ─────────────────────────────────────────────────────────────────────────────
59// PriorityDispatcher
60// ─────────────────────────────────────────────────────────────────────────────
61
62/// Priority dispatcher backed by a max-heap keyed on [`SlaClass::dispatch_priority`].
63///
64/// Enqueue from any tier; dequeue always returns the highest-priority item.
65/// Within the same priority, items are returned in FIFO order based on the
66/// monotonic insertion sequence.
67///
68/// ```rust
69/// use oxirs_core::sla::{SlaClass, PriorityDispatcher};
70///
71/// let mut dispatcher: PriorityDispatcher<&str> = PriorityDispatcher::new();
72/// dispatcher.enqueue("bronze_tenant".into(), SlaClass::Bronze, "low-pri query");
73/// dispatcher.enqueue("platinum_tenant".into(), SlaClass::Platinum, "high-pri query");
74///
75/// // Platinum is dequeued first
76/// let first = dispatcher.dequeue().expect("dispatcher has at least one entry");
77/// assert_eq!(first.payload, "high-pri query");
78/// ```
79pub struct PriorityDispatcher<T> {
80    heap: BinaryHeap<PrioritizedQuery<T>>,
81    next_sequence: u64,
82}
83
84impl<T> Default for PriorityDispatcher<T> {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl<T> PriorityDispatcher<T> {
91    /// Create an empty dispatcher.
92    pub fn new() -> Self {
93        PriorityDispatcher {
94            heap: BinaryHeap::new(),
95            next_sequence: 0,
96        }
97    }
98
99    /// Enqueue a query for `tenant_id` at the priority of `sla`.
100    pub fn enqueue(&mut self, tenant_id: String, sla: SlaClass, payload: T) {
101        let sequence = self.next_sequence;
102        self.next_sequence = self.next_sequence.wrapping_add(1);
103        self.heap.push(PrioritizedQuery {
104            priority: sla.dispatch_priority(),
105            tenant_id,
106            sequence,
107            payload,
108        });
109    }
110
111    /// Dequeue the highest-priority query, or `None` if the queue is empty.
112    pub fn dequeue(&mut self) -> Option<PrioritizedQuery<T>> {
113        self.heap.pop()
114    }
115
116    /// Peek at the highest-priority query without removing it.
117    pub fn peek(&self) -> Option<&PrioritizedQuery<T>> {
118        self.heap.peek()
119    }
120
121    /// Return the number of queued items.
122    pub fn len(&self) -> usize {
123        self.heap.len()
124    }
125
126    /// Return `true` when the queue has no items.
127    pub fn is_empty(&self) -> bool {
128        self.heap.is_empty()
129    }
130
131    /// Drain all items in priority order (highest first).
132    pub fn drain_ordered(&mut self) -> Vec<PrioritizedQuery<T>> {
133        let mut result = Vec::with_capacity(self.heap.len());
134        while let Some(item) = self.heap.pop() {
135            result.push(item);
136        }
137        result
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_platinum_dequeued_first() {
147        let mut d: PriorityDispatcher<&str> = PriorityDispatcher::new();
148        d.enqueue("t_bronze".into(), SlaClass::Bronze, "b");
149        d.enqueue("t_gold".into(), SlaClass::Gold, "g");
150        d.enqueue("t_platinum".into(), SlaClass::Platinum, "p");
151        d.enqueue("t_silver".into(), SlaClass::Silver, "s");
152
153        let first = d.dequeue().expect("non-empty");
154        assert_eq!(first.payload, "p", "Platinum must be first");
155        let second = d.dequeue().expect("non-empty");
156        assert_eq!(second.payload, "g", "Gold must be second");
157        let third = d.dequeue().expect("non-empty");
158        assert_eq!(third.payload, "s", "Silver must be third");
159        let fourth = d.dequeue().expect("non-empty");
160        assert_eq!(fourth.payload, "b", "Bronze must be last");
161        assert!(d.is_empty());
162    }
163
164    #[test]
165    fn test_dequeue_empty_returns_none() {
166        let mut d: PriorityDispatcher<u32> = PriorityDispatcher::new();
167        assert!(d.dequeue().is_none());
168    }
169
170    #[test]
171    fn test_len_and_is_empty() {
172        let mut d: PriorityDispatcher<i32> = PriorityDispatcher::new();
173        assert!(d.is_empty());
174        d.enqueue("t".into(), SlaClass::Silver, 42);
175        assert_eq!(d.len(), 1);
176        assert!(!d.is_empty());
177        d.dequeue();
178        assert!(d.is_empty());
179    }
180
181    #[test]
182    fn test_multiple_same_class_fifo_within_priority() {
183        let mut d: PriorityDispatcher<u32> = PriorityDispatcher::new();
184        for i in 0..5u32 {
185            d.enqueue("gold".into(), SlaClass::Gold, i);
186        }
187        assert_eq!(d.len(), 5);
188        // FIFO within same priority — should drain 0, 1, 2, 3, 4 in order.
189        let drained = d.drain_ordered();
190        let payloads: Vec<u32> = drained.iter().map(|q| q.payload).collect();
191        assert_eq!(payloads, vec![0, 1, 2, 3, 4]);
192    }
193
194    #[test]
195    fn test_peek_does_not_remove() {
196        let mut d: PriorityDispatcher<&str> = PriorityDispatcher::new();
197        d.enqueue("t".into(), SlaClass::Platinum, "hello");
198        assert!(d.peek().is_some());
199        assert_eq!(d.len(), 1); // peek did not remove
200        d.dequeue();
201        assert!(d.peek().is_none());
202    }
203
204    #[test]
205    fn test_drain_ordered_highest_first() {
206        let mut d: PriorityDispatcher<u8> = PriorityDispatcher::new();
207        d.enqueue("a".into(), SlaClass::Silver, 2);
208        d.enqueue("b".into(), SlaClass::Platinum, 4);
209        d.enqueue("c".into(), SlaClass::Bronze, 1);
210        d.enqueue("d".into(), SlaClass::Gold, 3);
211
212        let drained = d.drain_ordered();
213        let payloads: Vec<u8> = drained.iter().map(|q| q.payload).collect();
214        assert_eq!(payloads, vec![4, 3, 2, 1]);
215    }
216}