Skip to main content

ractor/factory/
queues.rs

1// Copyright (c) Sean Lawlor
2//
3// This source code is licensed under both the MIT license found in the
4// LICENSE-MIT file in the root directory of this source tree.
5
6//! Queue implementations for Factories
7
8use std::collections::VecDeque;
9use std::fmt::Debug;
10use std::marker::PhantomData;
11use std::sync::Arc;
12
13use crate::factory::DiscardHandler;
14use crate::factory::DiscardReason;
15use crate::factory::Job;
16use crate::factory::JobKey;
17use crate::Message;
18
19/// Implementation of backing queue for factory messages when workers are
20/// all busy
21pub trait Queue<TKey, TMsg>: Send + 'static
22where
23    TKey: JobKey,
24    TMsg: Message,
25{
26    /// Retrieve the size of the factory's queue
27    fn len(&self) -> usize;
28
29    /// Check if the queue is empty
30    fn is_empty(&self) -> bool;
31
32    /// Pop the next message from the front of the queue
33    fn pop_front(&mut self) -> Option<Job<TKey, TMsg>>;
34
35    /// Try and discard a message according to the queue semantics
36    /// in an overload scenario (e.g. lowest priority if priority
37    /// queueing). In a basic queueing scenario, this is equivalent
38    /// to `pop_front`
39    fn discard_oldest(&mut self) -> Option<Job<TKey, TMsg>>;
40
41    /// Peek an item from the head of the queue
42    fn peek(&self) -> Option<&Job<TKey, TMsg>>;
43
44    /// Push an item to the back of the queue
45    fn push_back(&mut self, job: Job<TKey, TMsg>);
46
47    /// Remove expired items from the queue
48    ///
49    /// * `discard_handler` - The handler to call for each discarded job. Will be called
50    ///   with [DiscardReason::TtlExpired].
51    ///
52    /// Returns the number of elements removed from the queue
53    fn remove_expired_items(
54        &mut self,
55        discard_handler: &Option<Arc<dyn DiscardHandler<TKey, TMsg>>>,
56    ) -> usize;
57
58    /// Determine if a given job can be discarded. Default is [true] for all jobs.
59    ///
60    /// This can be overridden to customize discard semantics.
61    fn is_job_discardable(&self, _key: &TKey) -> bool {
62        true
63    }
64}
65
66/// Priority trait which denotes the usize value of a [Priority]
67pub trait Priority: Default + From<usize> + Send + 'static {
68    /// Retrieve the index for the Priority value. This should be
69    /// contiguous from 0, 0 being the highest priority.
70    fn get_index(&self) -> usize;
71}
72
73/// Basic 5-category priority definition. This is probably flexible enough
74/// for most use-cases
75#[derive(strum::FromRepr, Default, Debug, Clone, Copy, Eq, PartialEq, Hash)]
76#[repr(usize)]
77pub enum StandardPriority {
78    /// Most important
79    Highest = 0,
80    /// High
81    High = 1,
82    /// Important
83    Important = 2,
84    /// Normal
85    #[default]
86    Normal = 3,
87    /// Low/best-effort priority
88    BestEffort = 4,
89}
90
91#[cfg(feature = "cluster")]
92impl crate::BytesConvertable for StandardPriority {
93    fn from_bytes(bytes: Vec<u8>) -> Self {
94        (u64::from_bytes(bytes) as usize).into()
95    }
96    fn into_bytes(self) -> Vec<u8> {
97        (self as u64).into_bytes()
98    }
99}
100
101impl StandardPriority {
102    /// Retrieve the number of variants of this enum, as a constant
103    pub const fn size() -> usize {
104        5
105    }
106}
107
108impl Priority for StandardPriority {
109    fn get_index(&self) -> usize {
110        *self as usize
111    }
112}
113
114impl From<usize> for StandardPriority {
115    fn from(value: usize) -> Self {
116        Self::from_repr(value).unwrap_or_default()
117    }
118}
119
120/// The [PriorityManager] is responsible for extracting the job priority from
121/// a given job's key (`TKey`). Additionally in some scenarios  some jobs may be non-discardable,
122/// i.e. can be enqueued regardless of the backpressure status of the factory. This is also
123/// responsible for determining if a job can be loadshed.
124pub trait PriorityManager<TKey, TPriority>: Send + Sync + 'static
125where
126    TKey: JobKey,
127    TPriority: Priority,
128{
129    /// Determine if this job can be discarded under load.
130    ///
131    /// Returns [true] if the job can be discarded, [false] otherwise.
132    fn is_discardable(&self, job: &TKey) -> bool;
133
134    /// Retrieve the job's priority.
135    ///
136    /// Returns [`None`] if the job does not have a priority, `Some(TPriority)` otherwise.
137    fn get_priority(&self, job: &TKey) -> Option<TPriority>;
138}
139
140// =============== Default Queue ================= //
141/// A simple, no-priority queue
142///
143/// Equivalent to a [VecDeque]
144pub struct DefaultQueue<TKey, TMsg>
145where
146    TKey: JobKey,
147    TMsg: Message,
148{
149    q: VecDeque<Job<TKey, TMsg>>,
150}
151
152impl<TKey, TMsg> Debug for DefaultQueue<TKey, TMsg>
153where
154    TKey: JobKey,
155    TMsg: Message,
156{
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        write!(f, "DefaultQueue({} items)", self.q.len())
159    }
160}
161
162impl<TKey, TMsg> Default for DefaultQueue<TKey, TMsg>
163where
164    TKey: JobKey,
165    TMsg: Message,
166{
167    fn default() -> Self {
168        Self { q: VecDeque::new() }
169    }
170}
171
172impl<TKey, TMsg> Queue<TKey, TMsg> for DefaultQueue<TKey, TMsg>
173where
174    TKey: JobKey,
175    TMsg: Message,
176{
177    /// Retrieve the size of the factory's queue
178    fn len(&self) -> usize {
179        self.q.len()
180    }
181
182    /// Check if the queue is empty
183    fn is_empty(&self) -> bool {
184        self.q.is_empty()
185    }
186
187    /// Pop the next message from the front of the queue
188    fn pop_front(&mut self) -> Option<Job<TKey, TMsg>> {
189        self.q.pop_front()
190    }
191
192    fn discard_oldest(&mut self) -> Option<Job<TKey, TMsg>> {
193        self.pop_front()
194    }
195
196    fn peek(&self) -> Option<&Job<TKey, TMsg>> {
197        self.q.front()
198    }
199
200    /// Push an item to the back of the queue, with the given priority
201    fn push_back(&mut self, job: Job<TKey, TMsg>) {
202        self.q.push_back(job)
203    }
204
205    /// Remove expired items from the queue
206    fn remove_expired_items(
207        &mut self,
208        discard_handler: &Option<Arc<dyn DiscardHandler<TKey, TMsg>>>,
209    ) -> usize {
210        let before = self.q.len();
211        // scan backlog for expired jobs and pop, discard, and drop them
212        self.q.retain_mut(|queued_item| {
213            if queued_item.is_expired() {
214                if let Some(handler) = discard_handler {
215                    handler.discard(DiscardReason::TtlExpired, queued_item);
216                }
217                false
218            } else {
219                true
220            }
221        });
222        before - self.q.len()
223    }
224}
225
226// =============== Priority Queue ================= //
227/// A queue with `NUM_PRIORITIES` priorities
228///
229/// It requires a [PriorityManager] implementation associated with it in order to
230/// determine the priorities of given jobs and inform discard semantics.
231pub struct PriorityQueue<TKey, TMsg, TPriority, TPriorityManager, const NUM_PRIORITIES: usize>
232where
233    TKey: JobKey,
234    TMsg: Message,
235    TPriority: Priority,
236    TPriorityManager: PriorityManager<TKey, TPriority>,
237{
238    queues: [VecDeque<Job<TKey, TMsg>>; NUM_PRIORITIES],
239    priority_manager: TPriorityManager,
240    _p: PhantomData<fn() -> TPriority>,
241}
242
243impl<TKey, TMsg, TPriority, TPriorityManager, const NUM_PRIORITIES: usize> Debug
244    for PriorityQueue<TKey, TMsg, TPriority, TPriorityManager, NUM_PRIORITIES>
245where
246    TKey: JobKey,
247    TMsg: Message,
248    TPriority: Priority,
249    TPriorityManager: PriorityManager<TKey, TPriority>,
250{
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        write!(f, "PriorityQueue({} items)", self.len())
253    }
254}
255
256impl<TKey, TMsg, TPriority, TPriorityManager, const NUM_PRIORITIES: usize>
257    PriorityQueue<TKey, TMsg, TPriority, TPriorityManager, NUM_PRIORITIES>
258where
259    TKey: JobKey,
260    TMsg: Message,
261    TPriority: Priority,
262    TPriorityManager: PriorityManager<TKey, TPriority>,
263{
264    /// Construct a new [PriorityQueue] instance with the supplied [PriorityManager]
265    /// implementation.
266    pub fn new(priority_manager: TPriorityManager) -> Self {
267        Self {
268            _p: PhantomData,
269            priority_manager,
270            queues: [(); NUM_PRIORITIES].map(|_| VecDeque::new()),
271        }
272    }
273}
274
275impl<TKey, TMsg, TPriority, TPriorityManager, const NUM_PRIORITIES: usize> Queue<TKey, TMsg>
276    for PriorityQueue<TKey, TMsg, TPriority, TPriorityManager, NUM_PRIORITIES>
277where
278    TKey: JobKey,
279    TMsg: Message,
280    TPriority: Priority,
281    TPriorityManager: PriorityManager<TKey, TPriority>,
282{
283    /// Retrieve the size of the factory's queue
284    fn len(&self) -> usize {
285        self.queues.iter().map(|q| q.len()).sum()
286    }
287
288    /// Check if the queue is empty
289    fn is_empty(&self) -> bool {
290        self.queues.iter().all(|q| q.is_empty())
291    }
292
293    /// Pop the next message from the front of the queue
294    fn pop_front(&mut self) -> Option<Job<TKey, TMsg>> {
295        for i in 0..NUM_PRIORITIES {
296            if let Some(r) = self.queues[i].pop_front() {
297                return Some(r);
298            }
299        }
300        None
301    }
302
303    fn discard_oldest(&mut self) -> Option<Job<TKey, TMsg>> {
304        for i in (0..NUM_PRIORITIES).rev() {
305            if let Some(r) = self.queues[i].pop_front() {
306                return Some(r);
307            }
308        }
309        None
310    }
311
312    fn peek(&self) -> Option<&Job<TKey, TMsg>> {
313        for i in 0..NUM_PRIORITIES {
314            let maybe = self.queues[i].front();
315            if maybe.is_some() {
316                return maybe;
317            }
318        }
319        None
320    }
321
322    /// Push an item to the back of the queue
323    fn push_back(&mut self, job: Job<TKey, TMsg>) {
324        let priority = self
325            .priority_manager
326            .get_priority(&job.key)
327            .unwrap_or_else(Default::default);
328        let idx = priority.get_index();
329        self.queues[idx].push_back(job);
330    }
331
332    /// Remove expired items from the queue
333    fn remove_expired_items(
334        &mut self,
335        discard_handler: &Option<Arc<dyn DiscardHandler<TKey, TMsg>>>,
336    ) -> usize {
337        let mut num_removed = 0;
338
339        // scan backlog for expired jobs and pop, discard, and drop them
340        for i in 0..NUM_PRIORITIES {
341            self.queues[i].retain_mut(|queued_item| {
342                if queued_item.is_expired() {
343                    if let Some(handler) = discard_handler {
344                        handler.discard(DiscardReason::TtlExpired, queued_item);
345                    }
346                    num_removed += 1;
347                    false
348                } else {
349                    true
350                }
351            });
352        }
353        num_removed
354    }
355
356    fn is_job_discardable(&self, key: &TKey) -> bool {
357        self.priority_manager.is_discardable(key)
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::super::*;
364    use super::*;
365    use crate::concurrency::Duration;
366
367    #[derive(Default, Debug)]
368    enum BasicPriority {
369        #[default]
370        Low,
371        High,
372    }
373
374    impl Priority for BasicPriority {
375        fn get_index(&self) -> usize {
376            match self {
377                BasicPriority::Low => 1,
378                BasicPriority::High => 0,
379            }
380        }
381    }
382
383    impl From<usize> for BasicPriority {
384        fn from(value: usize) -> Self {
385            match value {
386                0 => BasicPriority::High,
387                _ => BasicPriority::Low,
388            }
389        }
390    }
391
392    struct BasicPriorityManager;
393
394    impl PriorityManager<u64, BasicPriority> for BasicPriorityManager {
395        fn get_priority(&self, _key: &u64) -> Option<BasicPriority> {
396            if *_key % 2 == 0 {
397                Some(BasicPriority::High)
398            } else {
399                Some(BasicPriority::Low)
400            }
401        }
402
403        fn is_discardable(&self, _key: &u64) -> bool {
404            false
405        }
406    }
407
408    #[crate::concurrency::test]
409    #[cfg_attr(
410        not(all(target_arch = "wasm32", target_os = "unknown")),
411        tracing_test::traced_test
412    )]
413    async fn test_basic_queueing() {
414        let mut queue = DefaultQueue::<u64, ()>::default();
415        for i in 0..99 {
416            queue.push_back(Job {
417                key: i,
418                accepted: None,
419                msg: (),
420                options: JobOptions::default(),
421            });
422        }
423
424        queue.push_back(Job {
425            key: 99,
426            accepted: None,
427            msg: (),
428            options: JobOptions::new(Some(Duration::from_millis(1))),
429        });
430
431        let oldest = queue.discard_oldest();
432        assert!(matches!(oldest, Some(Job { key: 0, .. })));
433
434        let peeked = queue.peek();
435        assert!(matches!(peeked, Some(Job { key: 1, .. })));
436
437        let popped = queue.pop_front();
438        assert!(matches!(popped, Some(Job { key: 1, .. })));
439
440        let len = queue.len();
441        assert_eq!(len, 98);
442
443        let is_empty = queue.is_empty();
444        assert!(!is_empty);
445
446        crate::concurrency::sleep(Duration::from_millis(2)).await;
447
448        struct MyDiscardHandler;
449
450        impl DiscardHandler<u64, ()> for MyDiscardHandler {
451            fn discard(&self, _reason: DiscardReason, job: &mut Job<u64, ()>) {
452                tracing::info!("discarding job: {}", job.key);
453                assert_eq!(99, job.key);
454            }
455        }
456
457        // remove expired
458        _ = queue.remove_expired_items(&Some(Arc::new(MyDiscardHandler)));
459        let len = queue.len();
460        assert_eq!(len, 97);
461    }
462
463    #[crate::concurrency::test]
464    #[cfg_attr(
465        not(all(target_arch = "wasm32", target_os = "unknown")),
466        tracing_test::traced_test
467    )]
468    async fn test_priority_queueing() {
469        let mut queue = PriorityQueue::<u64, (), BasicPriority, BasicPriorityManager, 2>::new(
470            BasicPriorityManager,
471        );
472        for i in 0..99 {
473            queue.push_back(Job {
474                key: i,
475                accepted: None,
476                msg: (),
477                options: JobOptions::default(),
478            });
479        }
480
481        queue.push_back(Job {
482            key: 99,
483            accepted: None,
484            msg: (),
485            options: JobOptions::new(Some(Duration::from_millis(1))),
486        });
487
488        // should discard lowest pri first
489        let oldest = queue.discard_oldest();
490        assert!(matches!(oldest, Some(Job { key: 1, .. })));
491
492        // peek from high pri queue
493        let peeked = queue.peek();
494        assert!(matches!(peeked, Some(Job { key: 0, .. })));
495
496        // pop the same item
497        let popped = queue.pop_front();
498        assert!(matches!(popped, Some(Job { key: 0, .. })));
499
500        // we should have 98 items left, as we popped 2
501        let len = queue.len();
502        assert_eq!(len, 98);
503
504        // queue isn't empty
505        let is_empty = queue.is_empty();
506        assert!(!is_empty);
507
508        crate::concurrency::sleep(Duration::from_millis(2)).await;
509
510        struct MyDiscardHandler;
511
512        impl DiscardHandler<u64, ()> for MyDiscardHandler {
513            fn discard(&self, _reason: DiscardReason, job: &mut Job<u64, ()>) {
514                tracing::info!("discarding job: {}", job.key);
515                assert_eq!(99, job.key);
516            }
517        }
518
519        // remove expired
520        _ = queue.remove_expired_items(&Some(Arc::new(MyDiscardHandler)));
521        let len = queue.len();
522        assert_eq!(len, 97);
523    }
524}