Skip to main content

oximedia_distributed/
message_queue.rs

1#![allow(dead_code)]
2//! Distributed message queue primitives for `OxiMedia`.
3//!
4//! Provides a lightweight priority-based in-memory message queue suitable for
5//! inter-node communication in a distributed encoding cluster.
6
7use std::cmp::Ordering;
8use std::collections::BinaryHeap;
9use std::time::{Duration, Instant};
10
11/// Priority level of a message.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
13pub enum MessagePriority {
14    /// Background / bulk transfer
15    Low = 0,
16    /// Normal operational messages
17    Normal = 1,
18    /// Important control messages
19    High = 2,
20    /// Time-critical or error-recovery messages
21    Critical = 3,
22}
23
24impl MessagePriority {
25    /// Numeric representation (0 = lowest, 3 = highest).
26    #[must_use]
27    pub fn numeric_value(self) -> u8 {
28        self as u8
29    }
30
31    /// Return the next higher priority level, if one exists.
32    #[must_use]
33    pub fn escalate(self) -> Self {
34        match self {
35            MessagePriority::Low => MessagePriority::Normal,
36            MessagePriority::Normal => MessagePriority::High,
37            MessagePriority::High | MessagePriority::Critical => MessagePriority::Critical,
38        }
39    }
40}
41
42/// An envelope wrapping a message payload for the distributed queue.
43#[derive(Debug, Clone)]
44pub struct DistributedMessage {
45    /// Unique message identifier.
46    pub id: u64,
47    /// Priority of this message.
48    pub priority: MessagePriority,
49    /// Opaque byte payload.
50    pub payload: Vec<u8>,
51    /// When the message was enqueued.
52    pub enqueued_at: Instant,
53    /// Optional TTL: message is expired after this duration.
54    pub ttl: Option<Duration>,
55    /// Source node identifier.
56    pub source: String,
57    /// Destination node identifier.
58    pub destination: String,
59}
60
61impl DistributedMessage {
62    /// Create a new message.
63    #[must_use]
64    pub fn new(
65        id: u64,
66        priority: MessagePriority,
67        payload: Vec<u8>,
68        source: impl Into<String>,
69        destination: impl Into<String>,
70    ) -> Self {
71        Self {
72            id,
73            priority,
74            payload,
75            enqueued_at: Instant::now(),
76            ttl: None,
77            source: source.into(),
78            destination: destination.into(),
79        }
80    }
81
82    /// Attach a time-to-live to the message.
83    #[must_use]
84    pub fn with_ttl(mut self, ttl: Duration) -> Self {
85        self.ttl = Some(ttl);
86        self
87    }
88
89    /// Return `true` if the message's TTL has elapsed relative to `now`.
90    ///
91    /// Messages without a TTL never expire.
92    #[must_use]
93    pub fn is_expired(&self, now: Instant) -> bool {
94        match self.ttl {
95            None => false,
96            Some(ttl) => now.saturating_duration_since(self.enqueued_at) >= ttl,
97        }
98    }
99
100    /// Payload length in bytes.
101    #[must_use]
102    pub fn payload_len(&self) -> usize {
103        self.payload.len()
104    }
105}
106
107/// Wrapper that makes `DistributedMessage` orderable for `BinaryHeap`
108/// (max-heap by priority, then by insertion order — lower id = older = higher urgency).
109#[derive(Debug)]
110struct QueueEntry {
111    message: DistributedMessage,
112    seq: u64,
113}
114
115impl PartialEq for QueueEntry {
116    fn eq(&self, other: &Self) -> bool {
117        self.message.priority == other.message.priority && self.seq == other.seq
118    }
119}
120
121impl Eq for QueueEntry {}
122
123impl PartialOrd for QueueEntry {
124    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
125        Some(self.cmp(other))
126    }
127}
128
129impl Ord for QueueEntry {
130    fn cmp(&self, other: &Self) -> Ordering {
131        // Higher priority first; for equal priority, lower seq (older) first.
132        match self.message.priority.cmp(&other.message.priority) {
133            Ordering::Equal => other.seq.cmp(&self.seq),
134            ord => ord,
135        }
136    }
137}
138
139/// A bounded, priority-ordered distributed message queue.
140#[derive(Debug)]
141pub struct MessageQueue {
142    heap: BinaryHeap<QueueEntry>,
143    capacity: usize,
144    seq_counter: u64,
145    enqueued_total: u64,
146    dropped_total: u64,
147}
148
149impl MessageQueue {
150    /// Create a new queue with the given capacity.
151    #[must_use]
152    pub fn new(capacity: usize) -> Self {
153        Self {
154            heap: BinaryHeap::new(),
155            capacity,
156            seq_counter: 0,
157            enqueued_total: 0,
158            dropped_total: 0,
159        }
160    }
161
162    /// Number of messages currently in the queue.
163    #[must_use]
164    pub fn len(&self) -> usize {
165        self.heap.len()
166    }
167
168    /// Return `true` when the queue contains no messages.
169    #[must_use]
170    pub fn is_empty(&self) -> bool {
171        self.heap.is_empty()
172    }
173
174    /// Enqueue a message.
175    ///
176    /// Returns `false` and increments the dropped counter when the queue is
177    /// at capacity.
178    pub fn enqueue(&mut self, message: DistributedMessage) -> bool {
179        if self.heap.len() >= self.capacity {
180            self.dropped_total += 1;
181            return false;
182        }
183        let seq = self.seq_counter;
184        self.seq_counter += 1;
185        self.enqueued_total += 1;
186        self.heap.push(QueueEntry { message, seq });
187        true
188    }
189
190    /// Dequeue the highest-priority message, or `None` if the queue is empty.
191    pub fn dequeue(&mut self) -> Option<DistributedMessage> {
192        self.heap.pop().map(|e| e.message)
193    }
194
195    /// Peek at the priority of the next message without removing it.
196    #[must_use]
197    pub fn peek_priority(&self) -> Option<MessagePriority> {
198        self.heap.peek().map(|e| e.message.priority)
199    }
200
201    /// Total messages successfully enqueued since creation.
202    #[must_use]
203    pub fn enqueued_total(&self) -> u64 {
204        self.enqueued_total
205    }
206
207    /// Total messages dropped due to capacity overflow.
208    #[must_use]
209    pub fn dropped_total(&self) -> u64 {
210        self.dropped_total
211    }
212
213    /// Drain all expired messages (as of `now`) from the queue, returning the
214    /// count of messages removed.
215    pub fn drain_expired(&mut self, now: Instant) -> usize {
216        let before = self.heap.len();
217        let entries: Vec<QueueEntry> = self.heap.drain().collect();
218        for entry in entries {
219            if !entry.message.is_expired(now) {
220                self.heap.push(entry);
221            }
222        }
223        before - self.heap.len()
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use std::time::Duration;
231
232    fn msg(id: u64, priority: MessagePriority) -> DistributedMessage {
233        DistributedMessage::new(id, priority, vec![1, 2, 3], "src", "dst")
234    }
235
236    #[test]
237    fn test_priority_numeric_values() {
238        assert_eq!(MessagePriority::Low.numeric_value(), 0);
239        assert_eq!(MessagePriority::Normal.numeric_value(), 1);
240        assert_eq!(MessagePriority::High.numeric_value(), 2);
241        assert_eq!(MessagePriority::Critical.numeric_value(), 3);
242    }
243
244    #[test]
245    fn test_priority_ordering() {
246        assert!(MessagePriority::Critical > MessagePriority::High);
247        assert!(MessagePriority::High > MessagePriority::Normal);
248        assert!(MessagePriority::Normal > MessagePriority::Low);
249    }
250
251    #[test]
252    fn test_priority_escalate() {
253        assert_eq!(MessagePriority::Low.escalate(), MessagePriority::Normal);
254        assert_eq!(MessagePriority::Normal.escalate(), MessagePriority::High);
255        assert_eq!(MessagePriority::High.escalate(), MessagePriority::Critical);
256        assert_eq!(
257            MessagePriority::Critical.escalate(),
258            MessagePriority::Critical
259        );
260    }
261
262    #[test]
263    fn test_message_not_expired_without_ttl() {
264        let m = msg(1, MessagePriority::Normal);
265        assert!(!m.is_expired(Instant::now()));
266    }
267
268    #[test]
269    fn test_message_not_expired_within_ttl() {
270        let m = msg(1, MessagePriority::Normal).with_ttl(Duration::from_secs(60));
271        assert!(!m.is_expired(Instant::now()));
272    }
273
274    #[test]
275    fn test_message_is_expired_after_ttl() {
276        let past = Instant::now() - Duration::from_secs(10);
277        let mut m = msg(1, MessagePriority::Normal);
278        m.enqueued_at = past;
279        let m = m.with_ttl(Duration::from_secs(5));
280        assert!(m.is_expired(Instant::now()));
281    }
282
283    #[test]
284    fn test_message_payload_len() {
285        let m = DistributedMessage::new(1, MessagePriority::Low, vec![0u8; 100], "a", "b");
286        assert_eq!(m.payload_len(), 100);
287    }
288
289    #[test]
290    fn test_queue_enqueue_and_len() {
291        let mut q = MessageQueue::new(10);
292        assert!(q.is_empty());
293        q.enqueue(msg(1, MessagePriority::Low));
294        q.enqueue(msg(2, MessagePriority::High));
295        assert_eq!(q.len(), 2);
296    }
297
298    #[test]
299    fn test_queue_dequeue_priority_order() {
300        let mut q = MessageQueue::new(10);
301        q.enqueue(msg(1, MessagePriority::Low));
302        q.enqueue(msg(2, MessagePriority::Critical));
303        q.enqueue(msg(3, MessagePriority::Normal));
304
305        assert_eq!(
306            q.dequeue().expect("dequeue should return a task").priority,
307            MessagePriority::Critical
308        );
309        assert_eq!(
310            q.dequeue().expect("dequeue should return a task").priority,
311            MessagePriority::Normal
312        );
313        assert_eq!(
314            q.dequeue().expect("dequeue should return a task").priority,
315            MessagePriority::Low
316        );
317        assert!(q.dequeue().is_none());
318    }
319
320    #[test]
321    fn test_queue_peek_priority() {
322        let mut q = MessageQueue::new(5);
323        assert!(q.peek_priority().is_none());
324        q.enqueue(msg(1, MessagePriority::Low));
325        q.enqueue(msg(2, MessagePriority::High));
326        assert_eq!(q.peek_priority(), Some(MessagePriority::High));
327    }
328
329    #[test]
330    fn test_queue_capacity_overflow() {
331        let mut q = MessageQueue::new(2);
332        assert!(q.enqueue(msg(1, MessagePriority::Low)));
333        assert!(q.enqueue(msg(2, MessagePriority::Low)));
334        assert!(!q.enqueue(msg(3, MessagePriority::Low)));
335        assert_eq!(q.dropped_total(), 1);
336    }
337
338    #[test]
339    fn test_queue_enqueued_total() {
340        let mut q = MessageQueue::new(10);
341        q.enqueue(msg(1, MessagePriority::Normal));
342        q.enqueue(msg(2, MessagePriority::Normal));
343        assert_eq!(q.enqueued_total(), 2);
344    }
345
346    #[test]
347    fn test_drain_expired() {
348        let mut q = MessageQueue::new(10);
349        let past = Instant::now() - Duration::from_secs(10);
350
351        let mut expired_msg = msg(1, MessagePriority::Low);
352        expired_msg.enqueued_at = past;
353        let expired_msg = expired_msg.with_ttl(Duration::from_secs(5));
354        q.enqueue(expired_msg);
355
356        let fresh = msg(2, MessagePriority::High).with_ttl(Duration::from_secs(60));
357        q.enqueue(fresh);
358
359        let removed = q.drain_expired(Instant::now());
360        assert_eq!(removed, 1);
361        assert_eq!(q.len(), 1);
362    }
363}