Skip to main content

mecha10_core/priority_queue/
mod.rs

1//! Priority queue for message processing
2//!
3//! Enables quality-of-service (QoS) by prioritizing critical messages over
4//! low-priority ones. Useful for safety-critical systems where some messages
5//! (e.g., emergency stop) must be processed before others (e.g., telemetry).
6//!
7//! # Features
8//!
9//! - **Priority Levels**: Define message importance (Critical, High, Normal, Low)
10//! - **Fair Processing**: Round-robin within same priority level
11//! - **Starvation Prevention**: Ensures low-priority messages eventually process
12//! - **Backpressure Support**: Integrates with bounded channels
13//! - **Zero-Copy**: Messages moved, not copied
14//!
15//! # Example
16//!
17//! ```rust
18//! use mecha10_core::prelude::*;
19//! use mecha10_core::priority_queue::{PriorityReceiver, Priority};
20//! use mecha10_core::topics::Topic;
21//!
22//! #[derive(Debug, Deserialize)]
23//! struct Event {
24//!     value: f32,
25//! }
26//! impl Message for Event {}
27//!
28//! const EMERGENCY: Topic<Event> = Topic::new("/safety/emergency");
29//! const COMMAND: Topic<Event> = Topic::new("/motor/command");
30//! const TELEMETRY: Topic<Event> = Topic::new("/sensor/telemetry");
31//!
32//! # async fn handle_emergency(_msg: Event) -> Result<()> { Ok(()) }
33//! # async fn handle_command(_msg: Event) -> Result<()> { Ok(()) }
34//! # async fn handle_telemetry(_msg: Event) -> Result<()> { Ok(()) }
35//!
36//! # async fn example(ctx: &Context) -> Result<()> {
37//! // Create priority receiver
38//! let mut prio_rx = PriorityReceiver::new();
39//!
40//! // Subscribe to multiple topics with different priorities
41//! let emergency = ctx.subscribe(EMERGENCY).await?;
42//! let commands = ctx.subscribe(COMMAND).await?;
43//! let telemetry = ctx.subscribe(TELEMETRY).await?;
44//!
45//! // Add with priorities
46//! prio_rx.add_source(emergency, Priority::Critical);
47//! prio_rx.add_source(commands, Priority::High);
48//! prio_rx.add_source(telemetry, Priority::Low);
49//!
50//! // Process messages in priority order
51//! while let Some((msg, priority)) = prio_rx.recv().await {
52//!     match priority {
53//!         Priority::Critical => handle_emergency(msg).await?,
54//!         Priority::High => handle_command(msg).await?,
55//!         _ => handle_telemetry(msg).await?,
56//!     }
57//! }
58//! # Ok(())
59//! # }
60//! ```
61
62use crate::context::Receiver;
63use std::collections::VecDeque;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
66pub enum Priority {
67    Low = 0,
68    #[default]
69    Normal = 1,
70    High = 2,
71    Critical = 3,
72}
73
74#[derive(Debug, Clone)]
75pub struct PriorityQueueConfig {
76    pub max_per_priority: Option<usize>,
77
78    pub prevent_starvation: bool,
79
80    pub starvation_threshold: usize,
81}
82
83impl Default for PriorityQueueConfig {
84    fn default() -> Self {
85        Self {
86            max_per_priority: Some(1000),
87            prevent_starvation: true,
88            starvation_threshold: 100,
89        }
90    }
91}
92
93pub struct PriorityReceiver<T> {
94    sources: Vec<PrioritySource<T>>,
95    config: PriorityQueueConfig,
96    high_priority_count: usize,
97}
98
99struct PrioritySource<T> {
100    receiver: Receiver<T>,
101    priority: Priority,
102    buffer: VecDeque<T>,
103}
104
105impl<T> PriorityReceiver<T>
106where
107    T: Send + 'static,
108{
109    /// Create a new priority receiver with default configuration
110    pub fn new() -> Self {
111        Self::with_config(PriorityQueueConfig::default())
112    }
113
114    /// Create a new priority receiver with custom configuration
115    pub fn with_config(config: PriorityQueueConfig) -> Self {
116        Self {
117            sources: Vec::new(),
118            config,
119            high_priority_count: 0,
120        }
121    }
122
123    /// Add a message source with the specified priority
124    ///
125    /// # Example
126    ///
127    /// ```rust
128    /// # use mecha10_core::prelude::*;
129    /// # use mecha10_core::priority_queue::{PriorityReceiver, Priority};
130    /// # use mecha10_core::topics::Topic;
131    /// # #[derive(Debug, Deserialize)]
132    /// # struct Event { value: f32 }
133    /// # impl Message for Event {}
134    /// # const EMERGENCY: Topic<Event> = Topic::new("/safety/emergency");
135    /// # const TELEMETRY: Topic<Event> = Topic::new("/sensor/telemetry");
136    /// # async fn example(ctx: &Context) -> Result<()> {
137    /// let mut prio_rx = PriorityReceiver::new();
138    ///
139    /// let emergency = ctx.subscribe(EMERGENCY).await?;
140    /// prio_rx.add_source(emergency, Priority::Critical);
141    ///
142    /// let telemetry = ctx.subscribe(TELEMETRY).await?;
143    /// prio_rx.add_source(telemetry, Priority::Low);
144    /// # Ok(())
145    /// # }
146    /// ```
147    pub fn add_source(&mut self, receiver: Receiver<T>, priority: Priority) {
148        self.sources.push(PrioritySource {
149            receiver,
150            priority,
151            buffer: VecDeque::new(),
152        });
153    }
154
155    /// Receive the next message in priority order
156    ///
157    /// Returns `None` when all sources are closed.
158    /// Returns `Some((message, priority))` with the highest priority message available.
159    ///
160    /// # Starvation Prevention
161    ///
162    /// If configured, this method will occasionally process lower-priority messages
163    /// even when higher-priority messages are available, to prevent starvation.
164    pub async fn recv(&mut self) -> Option<(T, Priority)> {
165        if self.sources.is_empty() {
166            return None;
167        }
168
169        loop {
170            self.try_fill_buffers();
171
172            if self.config.prevent_starvation && self.high_priority_count >= self.config.starvation_threshold {
173                if let Some((msg, prio)) = self.take_lower_priority_message() {
174                    self.high_priority_count = 0;
175                    return Some((msg, prio));
176                }
177            }
178
179            if let Some((msg, prio)) = self.take_highest_priority_message() {
180                if prio >= Priority::High {
181                    self.high_priority_count += 1;
182                }
183                return Some((msg, prio));
184            }
185
186            if !self.wait_for_message().await {
187                return None;
188            }
189        }
190    }
191
192    /// Try to receive messages from all sources without blocking
193    fn try_fill_buffers(&mut self) {
194        for source in &mut self.sources {
195            let max_size = self.config.max_per_priority.unwrap_or(usize::MAX);
196
197            while source.buffer.len() < max_size {
198                match source.receiver.try_recv() {
199                    Ok(msg) => source.buffer.push_back(msg),
200                    Err(_) => break,
201                }
202            }
203        }
204    }
205
206    /// Wait for at least one message from any source
207    async fn wait_for_message(&mut self) -> bool {
208        if self.sources.is_empty() {
209            return false;
210        }
211
212        loop {
213            for source in &mut self.sources {
214                if let Some(msg) = source.receiver.recv().await {
215                    source.buffer.push_back(msg);
216                    return true;
217                }
218            }
219
220            self.sources.retain(|s| !s.buffer.is_empty() || !s.receiver.is_empty());
221
222            if self.sources.is_empty() {
223                return false;
224            }
225        }
226    }
227
228    /// Take the highest priority message from buffers
229    fn take_highest_priority_message(&mut self) -> Option<(T, Priority)> {
230        self.take_extreme_priority_message(|candidate, best| candidate > best)
231    }
232
233    /// Take a lower-priority message for starvation prevention
234    fn take_lower_priority_message(&mut self) -> Option<(T, Priority)> {
235        self.take_extreme_priority_message(|candidate, best| candidate < best)
236    }
237
238    /// Shared implementation behind `take_highest_priority_message`/`take_lower_priority_message`,
239    /// which differ only in which direction `is_better` compares priorities.
240    fn take_extreme_priority_message(
241        &mut self,
242        is_better: impl Fn(Priority, Priority) -> bool,
243    ) -> Option<(T, Priority)> {
244        let mut best: Option<(usize, Priority)> = None;
245
246        for (idx, source) in self.sources.iter().enumerate() {
247            if source.buffer.is_empty() {
248                continue;
249            }
250            match best {
251                None => best = Some((idx, source.priority)),
252                Some((_, best_priority)) if is_better(source.priority, best_priority) => {
253                    best = Some((idx, source.priority));
254                }
255                _ => {}
256            }
257        }
258
259        best.and_then(|(idx, _)| {
260            self.sources[idx]
261                .buffer
262                .pop_front()
263                .map(|msg| (msg, self.sources[idx].priority))
264        })
265    }
266
267    /// Get statistics about the priority queue
268    pub fn stats(&self) -> PriorityQueueStats {
269        let mut counts_by_priority = [0; 4];
270        let mut total_buffered = 0;
271
272        for source in &self.sources {
273            let idx = source.priority as usize;
274            counts_by_priority[idx] += source.buffer.len();
275            total_buffered += source.buffer.len();
276        }
277
278        PriorityQueueStats {
279            total_buffered,
280            critical_buffered: counts_by_priority[Priority::Critical as usize],
281            high_buffered: counts_by_priority[Priority::High as usize],
282            normal_buffered: counts_by_priority[Priority::Normal as usize],
283            low_buffered: counts_by_priority[Priority::Low as usize],
284            source_count: self.sources.len(),
285        }
286    }
287}
288
289impl<T> Default for PriorityReceiver<T>
290where
291    T: Send + 'static,
292{
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    fn make_source(priority: Priority, messages: Vec<u32>) -> PrioritySource<u32> {
303        let (_tx, rx) = tokio::sync::mpsc::unbounded_channel();
304        PrioritySource {
305            receiver: Receiver::from(rx),
306            priority,
307            buffer: messages.into_iter().collect(),
308        }
309    }
310
311    /// Regression test: when every buffered message is `Priority::Low` (the
312    /// sentinel `take_highest_priority_message` used to seed `best_priority`
313    /// with), the strict `>` comparison used to never select any of them,
314    /// even though they were the only messages present. This starved those
315    /// sources forever in `recv()`, since the underlying channel had already
316    /// been drained into the buffer via `try_recv` and `wait_for_message`
317    /// only awaits *new* channel messages, not buffered ones.
318    #[test]
319    fn take_highest_priority_message_selects_all_low_messages() {
320        let mut prio_rx: PriorityReceiver<u32> = PriorityReceiver::new();
321        prio_rx.sources.push(make_source(Priority::Low, vec![1, 2, 3]));
322
323        let (msg, priority) = prio_rx
324            .take_highest_priority_message()
325            .expect("a Low-priority message should still be returned when it's the only one buffered");
326        assert_eq!(msg, 1);
327        assert_eq!(priority, Priority::Low);
328
329        assert_eq!(prio_rx.take_highest_priority_message(), Some((2, Priority::Low)));
330        assert_eq!(prio_rx.take_highest_priority_message(), Some((3, Priority::Low)));
331        assert_eq!(prio_rx.take_highest_priority_message(), None);
332    }
333
334    /// Regression test: mirror of the above for `take_lower_priority_message`,
335    /// whose sentinel was `Priority::Critical` compared with strict `<`.
336    #[test]
337    fn take_lower_priority_message_selects_all_critical_messages() {
338        let mut prio_rx: PriorityReceiver<u32> = PriorityReceiver::new();
339        prio_rx.sources.push(make_source(Priority::Critical, vec![10, 20]));
340
341        let (msg, priority) = prio_rx
342            .take_lower_priority_message()
343            .expect("a Critical-priority message should still be returned when it's the only one buffered");
344        assert_eq!(msg, 10);
345        assert_eq!(priority, Priority::Critical);
346
347        assert_eq!(prio_rx.take_lower_priority_message(), Some((20, Priority::Critical)));
348        assert_eq!(prio_rx.take_lower_priority_message(), None);
349    }
350
351    /// End-to-end regression test through `recv()`: a single source producing
352    /// only `Priority::Low` messages must have those messages delivered
353    /// rather than hanging forever waiting on `wait_for_message`.
354    #[tokio::test]
355    async fn recv_delivers_messages_from_a_low_priority_only_source() {
356        let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<u32>();
357        let mut prio_rx: PriorityReceiver<u32> = PriorityReceiver::new();
358        prio_rx.add_source(Receiver::from(rx), Priority::Low);
359
360        tx.send(42).unwrap();
361        drop(tx);
362
363        let result = tokio::time::timeout(std::time::Duration::from_secs(5), prio_rx.recv())
364            .await
365            .expect("recv() should not hang starving a Low-only source");
366        assert_eq!(result, Some((42, Priority::Low)));
367    }
368
369    /// take_highest_priority_message should still prefer higher priorities
370    /// over lower ones when both are present.
371    #[test]
372    fn take_highest_priority_message_prefers_higher_priority() {
373        let mut prio_rx: PriorityReceiver<u32> = PriorityReceiver::new();
374        prio_rx.sources.push(make_source(Priority::Low, vec![1]));
375        prio_rx.sources.push(make_source(Priority::Critical, vec![2]));
376
377        assert_eq!(prio_rx.take_highest_priority_message(), Some((2, Priority::Critical)));
378    }
379}
380
381#[derive(Debug, Clone)]
382pub struct PriorityQueueStats {
383    pub total_buffered: usize,
384    pub critical_buffered: usize,
385    pub high_buffered: usize,
386    pub normal_buffered: usize,
387    pub low_buffered: usize,
388    pub source_count: usize,
389}