Skip to main content

gosh_dl/
priority_queue.rs

1//! Priority Queue for Download Scheduling
2//!
3//! Implements a priority-based queue for managing concurrent downloads.
4//! Downloads are scheduled based on priority (Critical > High > Normal > Low),
5//! with FIFO ordering within the same priority level.
6
7use crate::protocol::DownloadId;
8use parking_lot::Mutex;
9use std::collections::{BinaryHeap, HashMap};
10use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
11use std::sync::Arc;
12use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore};
13
14// Re-export DownloadPriority for backward compatibility
15pub use crate::protocol::DownloadPriority;
16
17/// Entry in the priority queue
18#[derive(Debug, Clone, Eq, PartialEq)]
19struct QueueEntry {
20    id: DownloadId,
21    priority: DownloadPriority,
22    /// Sequence number for FIFO ordering within same priority
23    sequence: u64,
24}
25
26impl Ord for QueueEntry {
27    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
28        // Higher priority first, then lower sequence (earlier) first
29        match self.priority.cmp(&other.priority) {
30            std::cmp::Ordering::Equal => other.sequence.cmp(&self.sequence), // Lower sequence = higher priority
31            other => other,
32        }
33    }
34}
35
36impl PartialOrd for QueueEntry {
37    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
38        Some(self.cmp(other))
39    }
40}
41
42/// A permit that allows a download to proceed
43/// When dropped, releases the slot back to the queue
44pub struct PriorityPermit {
45    _permit: OwnedSemaphorePermit,
46    id: DownloadId,
47    queue: Arc<PriorityQueue>,
48}
49
50impl Drop for PriorityPermit {
51    fn drop(&mut self) {
52        // Remove from active set
53        self.queue.inner.lock().active.remove(&self.id);
54        // Notify all waiting downloads so the highest priority can acquire
55        self.queue.notify.notify_waiters();
56    }
57}
58
59/// Internal state of the priority queue
60struct PriorityQueueInner {
61    /// Downloads waiting for a slot
62    waiting: BinaryHeap<QueueEntry>,
63    /// Currently active downloads
64    active: HashMap<DownloadId, DownloadPriority>,
65    /// Priority of each waiting download (for quick lookup)
66    waiting_priorities: HashMap<DownloadId, DownloadPriority>,
67}
68
69/// Priority-based download queue
70///
71/// Manages concurrent download slots with priority ordering.
72/// Higher priority downloads are started before lower priority ones,
73/// with FIFO ordering within the same priority level.
74pub struct PriorityQueue {
75    /// Semaphore for limiting concurrent downloads
76    semaphore: Arc<Semaphore>,
77    /// Current concurrency ceiling for new acquisitions.
78    max_concurrent: AtomicUsize,
79    /// Internal queue state
80    inner: Mutex<PriorityQueueInner>,
81    /// Sequence counter for FIFO ordering
82    sequence: AtomicU64,
83    /// Notification for waiting downloads
84    notify: Notify,
85}
86
87impl PriorityQueue {
88    /// Create a new priority queue with the given concurrency limit
89    pub fn new(max_concurrent: usize) -> Arc<Self> {
90        Arc::new(Self {
91            semaphore: Arc::new(Semaphore::new(max_concurrent)),
92            max_concurrent: AtomicUsize::new(max_concurrent),
93            inner: Mutex::new(PriorityQueueInner {
94                waiting: BinaryHeap::new(),
95                active: HashMap::new(),
96                waiting_priorities: HashMap::new(),
97            }),
98            sequence: AtomicU64::new(0),
99            notify: Notify::new(),
100        })
101    }
102
103    /// Acquire a permit for the download to proceed (blocking).
104    ///
105    /// This method **adds the download to the waiting queue** and blocks until:
106    /// 1. A slot becomes available, AND
107    /// 2. This download is the highest priority in the waiting queue
108    ///
109    /// The download remains in the queue until a permit is granted, ensuring fair
110    /// ordering based on priority and arrival time (FIFO within same priority).
111    ///
112    /// # Difference from `try_acquire`
113    /// - `acquire`: Adds to queue, waits for turn, guarantees eventual permit
114    /// - `try_acquire`: Does NOT add to queue, immediate success or failure
115    ///
116    /// Use `acquire` for downloads that should wait their turn.
117    /// Use `try_acquire` for opportunistic slot acquisition (e.g., resuming paused downloads).
118    pub async fn acquire(
119        self: &Arc<Self>,
120        id: DownloadId,
121        priority: DownloadPriority,
122    ) -> PriorityPermit {
123        // Add to waiting queue
124        let sequence = self.sequence.fetch_add(1, Ordering::Relaxed);
125        {
126            let mut inner = self.inner.lock();
127            inner.waiting.push(QueueEntry {
128                id,
129                priority,
130                sequence,
131            });
132            inner.waiting_priorities.insert(id, priority);
133        }
134
135        loop {
136            // Check if we're next in line
137            {
138                let inner = self.inner.lock();
139                if let Some(next) = inner.waiting.peek() {
140                    if next.id == id
141                        && inner.active.len() < self.max_concurrent.load(Ordering::Relaxed)
142                    {
143                        // We're next, try to acquire semaphore
144                        drop(inner); // Release lock before async operation
145
146                        // Try to acquire permit
147                        if let Ok(permit) = self.semaphore.clone().try_acquire_owned() {
148                            // Got permit, remove from waiting and add to active
149                            let mut inner = self.inner.lock();
150                            inner.waiting.pop();
151                            inner.waiting_priorities.remove(&id);
152                            inner.active.insert(id, priority);
153
154                            return PriorityPermit {
155                                _permit: permit,
156                                id,
157                                queue: Arc::clone(self),
158                            };
159                        }
160                    }
161                }
162            }
163
164            // Wait for notification (either slot freed or priority changed)
165            self.notify.notified().await;
166        }
167    }
168
169    /// Try to acquire a permit immediately without waiting (non-blocking).
170    ///
171    /// This method does **NOT** add the download to the waiting queue. It either
172    /// succeeds immediately or returns `None`.
173    ///
174    /// Returns `None` if:
175    /// - No slot is currently available, OR
176    /// - Higher priority downloads are already waiting in the queue
177    ///
178    /// # Difference from `acquire`
179    /// - `try_acquire`: Does NOT add to queue, immediate success or failure
180    /// - `acquire`: Adds to queue, waits for turn, guarantees eventual permit
181    ///
182    /// # Use Cases
183    /// - Opportunistic slot acquisition (e.g., checking if a paused download can resume)
184    /// - Avoiding queue position for downloads that shouldn't block others
185    /// - Non-async contexts where blocking is not possible
186    ///
187    /// # Warning
188    /// If you call `try_acquire` and it fails, the download is NOT queued.
189    /// You must call `acquire` if you want the download to wait for a slot.
190    pub fn try_acquire(
191        self: &Arc<Self>,
192        id: DownloadId,
193        priority: DownloadPriority,
194    ) -> Option<PriorityPermit> {
195        let mut inner = self.inner.lock();
196        if inner.active.len() >= self.max_concurrent.load(Ordering::Relaxed) {
197            return None;
198        }
199
200        // Check if there are higher priority downloads waiting
201        if let Some(next) = inner.waiting.peek() {
202            if next.priority > priority {
203                return None; // Higher priority download is waiting
204            }
205        }
206
207        // Try to acquire permit
208        match self.semaphore.clone().try_acquire_owned() {
209            Ok(permit) => {
210                inner.active.insert(id, priority);
211                Some(PriorityPermit {
212                    _permit: permit,
213                    id,
214                    queue: Arc::clone(self),
215                })
216            }
217            Err(_) => None,
218        }
219    }
220
221    /// Update the priority of a waiting download
222    ///
223    /// If the download is already active, this has no effect on scheduling.
224    /// Returns true if the priority was updated.
225    pub fn set_priority(&self, id: DownloadId, new_priority: DownloadPriority) -> bool {
226        let mut inner = self.inner.lock();
227
228        // Check if download is waiting
229        if inner.waiting_priorities.contains_key(&id) {
230            // Remove and re-add with new priority
231            let entries: Vec<_> = inner.waiting.drain().collect();
232            for entry in entries {
233                if entry.id == id {
234                    inner.waiting.push(QueueEntry {
235                        id: entry.id,
236                        priority: new_priority,
237                        sequence: entry.sequence,
238                    });
239                } else {
240                    inner.waiting.push(entry);
241                }
242            }
243            inner.waiting_priorities.insert(id, new_priority);
244            drop(inner);
245
246            // Notify waiting downloads to re-check their position
247            self.notify.notify_waiters();
248            return true;
249        }
250
251        // Check if download is active (update tracking but doesn't affect scheduling)
252        if let Some(priority) = inner.active.get_mut(&id) {
253            *priority = new_priority;
254            return true;
255        }
256
257        false
258    }
259
260    /// Remove a download from the waiting queue
261    ///
262    /// Call this if a download is cancelled before acquiring a permit.
263    pub fn remove(&self, id: DownloadId) {
264        let mut inner = self.inner.lock();
265        inner.waiting_priorities.remove(&id);
266        // Rebuild heap without the removed entry
267        let entries: Vec<_> = inner.waiting.drain().filter(|e| e.id != id).collect();
268        for entry in entries {
269            inner.waiting.push(entry);
270        }
271    }
272
273    /// Get the priority of a download (waiting or active)
274    pub fn get_priority(&self, id: DownloadId) -> Option<DownloadPriority> {
275        let inner = self.inner.lock();
276        inner
277            .waiting_priorities
278            .get(&id)
279            .or_else(|| inner.active.get(&id))
280            .copied()
281    }
282
283    /// Update the concurrency ceiling for future acquisitions.
284    pub fn set_max_concurrent(&self, max_concurrent: usize) {
285        let previous = self.max_concurrent.swap(max_concurrent, Ordering::Relaxed);
286        if max_concurrent > previous {
287            self.semaphore.add_permits(max_concurrent - previous);
288        }
289        self.notify.notify_waiters();
290    }
291
292    /// Get the number of active downloads
293    pub fn active_count(&self) -> usize {
294        self.inner.lock().active.len()
295    }
296
297    /// Get the number of waiting downloads
298    pub fn waiting_count(&self) -> usize {
299        self.inner.lock().waiting.len()
300    }
301
302    /// Get the position in queue for a waiting download (1-indexed, None if not waiting)
303    pub fn queue_position(&self, id: DownloadId) -> Option<usize> {
304        let inner = self.inner.lock();
305        if !inner.waiting_priorities.contains_key(&id) {
306            return None;
307        }
308        // Count entries with higher priority or same priority but lower sequence
309        let mut sorted: Vec<_> = inner.waiting.iter().cloned().collect();
310        sorted.sort_by(|a, b| b.cmp(a)); // Reverse to get descending order
311        sorted.iter().position(|e| e.id == id).map(|p| p + 1)
312    }
313
314    /// Get statistics about the queue
315    pub fn stats(&self) -> PriorityQueueStats {
316        let inner = self.inner.lock();
317        let mut by_priority = HashMap::new();
318        for priority in inner.waiting_priorities.values() {
319            *by_priority.entry(*priority).or_insert(0) += 1;
320        }
321        PriorityQueueStats {
322            active: inner.active.len(),
323            waiting: inner.waiting.len(),
324            waiting_by_priority: by_priority,
325        }
326    }
327}
328
329/// Statistics about the priority queue
330#[derive(Debug, Clone)]
331pub struct PriorityQueueStats {
332    /// Number of active downloads
333    pub active: usize,
334    /// Total number of waiting downloads
335    pub waiting: usize,
336    /// Waiting downloads by priority level
337    pub waiting_by_priority: HashMap<DownloadPriority, usize>,
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_priority_ordering() {
346        assert!(DownloadPriority::Critical > DownloadPriority::High);
347        assert!(DownloadPriority::High > DownloadPriority::Normal);
348        assert!(DownloadPriority::Normal > DownloadPriority::Low);
349    }
350
351    #[test]
352    fn test_priority_from_str() {
353        assert_eq!(
354            "low".parse::<DownloadPriority>().unwrap(),
355            DownloadPriority::Low
356        );
357        assert_eq!(
358            "normal".parse::<DownloadPriority>().unwrap(),
359            DownloadPriority::Normal
360        );
361        assert_eq!(
362            "high".parse::<DownloadPriority>().unwrap(),
363            DownloadPriority::High
364        );
365        assert_eq!(
366            "critical".parse::<DownloadPriority>().unwrap(),
367            DownloadPriority::Critical
368        );
369    }
370
371    #[test]
372    fn test_queue_entry_ordering() {
373        let entry1 = QueueEntry {
374            id: DownloadId::new(),
375            priority: DownloadPriority::Normal,
376            sequence: 1,
377        };
378        let entry2 = QueueEntry {
379            id: DownloadId::new(),
380            priority: DownloadPriority::High,
381            sequence: 2,
382        };
383        let entry3 = QueueEntry {
384            id: DownloadId::new(),
385            priority: DownloadPriority::Normal,
386            sequence: 0,
387        };
388
389        // Higher priority should be greater
390        assert!(entry2 > entry1);
391
392        // Same priority, lower sequence should be greater
393        assert!(entry3 > entry1);
394    }
395
396    #[tokio::test]
397    async fn test_priority_queue_basic() {
398        let queue = PriorityQueue::new(2);
399        let id1 = DownloadId::new();
400        let id2 = DownloadId::new();
401
402        // Should be able to acquire 2 permits
403        let permit1 = queue.clone().acquire(id1, DownloadPriority::Normal).await;
404        let permit2 = queue.clone().acquire(id2, DownloadPriority::Normal).await;
405
406        assert_eq!(queue.active_count(), 2);
407
408        // Drop permits
409        drop(permit1);
410        drop(permit2);
411
412        assert_eq!(queue.active_count(), 0);
413    }
414
415    #[tokio::test]
416    async fn test_priority_queue_priority_ordering() {
417        let queue = PriorityQueue::new(1);
418        let id_low = DownloadId::new();
419        let id_high = DownloadId::new();
420
421        // Acquire first slot
422        let permit1 = queue
423            .clone()
424            .acquire(DownloadId::new(), DownloadPriority::Normal)
425            .await;
426
427        // Add low priority to queue first
428        let queue_clone = queue.clone();
429        let low_handle =
430            tokio::spawn(async move { queue_clone.acquire(id_low, DownloadPriority::Low).await });
431
432        // Give it time to enter the queue
433        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
434
435        // Add high priority to queue
436        let queue_clone = queue.clone();
437        let high_handle =
438            tokio::spawn(async move { queue_clone.acquire(id_high, DownloadPriority::High).await });
439
440        // Give it time to enter the queue
441        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
442
443        assert_eq!(queue.waiting_count(), 2);
444
445        // Release first permit - high priority should get the slot
446        drop(permit1);
447
448        // Wait for high priority to acquire
449        let high_permit = tokio::time::timeout(std::time::Duration::from_millis(100), high_handle)
450            .await
451            .expect("timeout")
452            .expect("join error");
453
454        assert_eq!(queue.active_count(), 1);
455        assert_eq!(queue.waiting_count(), 1);
456
457        // Release high priority permit
458        drop(high_permit);
459
460        // Wait for low priority to acquire
461        let _low_permit = tokio::time::timeout(std::time::Duration::from_millis(100), low_handle)
462            .await
463            .expect("timeout")
464            .expect("join error");
465
466        assert_eq!(queue.active_count(), 1);
467        assert_eq!(queue.waiting_count(), 0);
468    }
469
470    #[test]
471    fn test_set_priority() {
472        let queue = PriorityQueue::new(1);
473        let id = DownloadId::new();
474
475        // Add to waiting queue (can't acquire because no async context for test)
476        {
477            let mut inner = queue.inner.lock();
478            inner.waiting.push(QueueEntry {
479                id,
480                priority: DownloadPriority::Low,
481                sequence: 0,
482            });
483            inner.waiting_priorities.insert(id, DownloadPriority::Low);
484        }
485
486        assert_eq!(queue.get_priority(id), Some(DownloadPriority::Low));
487
488        // Update priority
489        assert!(queue.set_priority(id, DownloadPriority::High));
490
491        assert_eq!(queue.get_priority(id), Some(DownloadPriority::High));
492    }
493
494    #[test]
495    fn test_remove() {
496        let queue = PriorityQueue::new(1);
497        let id = DownloadId::new();
498
499        // Add to waiting queue
500        {
501            let mut inner = queue.inner.lock();
502            inner.waiting.push(QueueEntry {
503                id,
504                priority: DownloadPriority::Normal,
505                sequence: 0,
506            });
507            inner
508                .waiting_priorities
509                .insert(id, DownloadPriority::Normal);
510        }
511
512        assert_eq!(queue.waiting_count(), 1);
513
514        // Remove
515        queue.remove(id);
516
517        assert_eq!(queue.waiting_count(), 0);
518        assert_eq!(queue.get_priority(id), None);
519    }
520
521    #[test]
522    fn test_stats() {
523        let queue = PriorityQueue::new(2);
524
525        // Add some waiting entries
526        {
527            let mut inner = queue.inner.lock();
528            for i in 0..3 {
529                let id = DownloadId::new();
530                let priority = match i % 3 {
531                    0 => DownloadPriority::Low,
532                    1 => DownloadPriority::Normal,
533                    _ => DownloadPriority::High,
534                };
535                inner.waiting.push(QueueEntry {
536                    id,
537                    priority,
538                    sequence: i,
539                });
540                inner.waiting_priorities.insert(id, priority);
541            }
542        }
543
544        let stats = queue.stats();
545        assert_eq!(stats.waiting, 3);
546        assert_eq!(stats.active, 0);
547    }
548}