Skip to main content

oximedia_distributed/
task_queue.rs

1//! Distributed task queue with priority ordering.
2//!
3//! This module implements a priority-based task queue for distributing
4//! encoding tasks across worker nodes. Tasks are dequeued in priority
5//! order, with FIFO ordering within the same priority level.
6
7#![allow(dead_code)]
8
9use std::cmp::Ordering;
10use std::collections::BinaryHeap;
11use uuid::Uuid;
12
13/// Priority levels for distributed tasks.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15pub enum TaskPriority {
16    /// Background tasks, lowest priority
17    Background = 0,
18    /// Normal priority
19    Normal = 1,
20    /// High priority
21    High = 2,
22    /// Urgent tasks, highest priority
23    Urgent = 3,
24    /// Critical infrastructure tasks
25    Critical = 4,
26}
27
28impl TaskPriority {
29    /// Returns the numeric value of this priority.
30    #[must_use]
31    pub fn value(&self) -> u8 {
32        *self as u8
33    }
34
35    /// Returns true if this priority is higher than the other.
36    #[must_use]
37    pub fn is_higher_than(&self, other: &Self) -> bool {
38        self.value() > other.value()
39    }
40}
41
42impl PartialOrd for TaskPriority {
43    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
44        Some(self.cmp(other))
45    }
46}
47
48impl Ord for TaskPriority {
49    fn cmp(&self, other: &Self) -> Ordering {
50        self.value().cmp(&other.value())
51    }
52}
53
54/// Status of a distributed task.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
56pub enum TaskStatus {
57    /// Task is waiting in the queue
58    Pending,
59    /// Task is assigned to a worker
60    Assigned,
61    /// Task is currently being executed
62    Running,
63    /// Task completed successfully
64    Completed,
65    /// Task failed
66    Failed,
67    /// Task was cancelled
68    Cancelled,
69}
70
71/// A distributed task that can be queued and assigned to workers.
72#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
73pub struct DistributedTask {
74    /// Unique task identifier
75    pub id: Uuid,
76    /// Task name/description
77    pub name: String,
78    /// Task priority
79    pub priority: TaskPriority,
80    /// Current status
81    pub status: TaskStatus,
82    /// Payload data (serialized task parameters)
83    pub payload: String,
84    /// Unix timestamp when the task was enqueued
85    pub enqueued_at: i64,
86    /// Maximum number of retry attempts
87    pub max_retries: u32,
88    /// Current retry count
89    pub retry_count: u32,
90    /// Optional deadline (unix timestamp)
91    pub deadline: Option<i64>,
92    /// Sequence number for FIFO within same priority
93    sequence: u64,
94}
95
96impl DistributedTask {
97    /// Creates a new distributed task.
98    #[must_use]
99    pub fn new(name: &str, priority: TaskPriority, payload: &str) -> Self {
100        Self {
101            id: Uuid::new_v4(),
102            name: name.to_string(),
103            priority,
104            status: TaskStatus::Pending,
105            payload: payload.to_string(),
106            enqueued_at: chrono::Utc::now().timestamp(),
107            max_retries: 3,
108            retry_count: 0,
109            deadline: None,
110            sequence: 0,
111        }
112    }
113
114    /// Sets the maximum retry count.
115    #[must_use]
116    pub fn with_max_retries(mut self, retries: u32) -> Self {
117        self.max_retries = retries;
118        self
119    }
120
121    /// Sets a deadline for the task.
122    #[must_use]
123    pub fn with_deadline(mut self, deadline: i64) -> Self {
124        self.deadline = Some(deadline);
125        self
126    }
127
128    /// Returns true if the task can be retried.
129    #[must_use]
130    pub fn can_retry(&self) -> bool {
131        self.retry_count < self.max_retries
132    }
133
134    /// Returns true if the task has passed its deadline.
135    #[must_use]
136    pub fn is_past_deadline(&self, now: i64) -> bool {
137        self.deadline.is_some_and(|d| now > d)
138    }
139
140    /// Increments the retry count and resets status to Pending.
141    pub fn retry(&mut self) {
142        self.retry_count += 1;
143        self.status = TaskStatus::Pending;
144    }
145
146    /// Marks the task as running.
147    pub fn mark_running(&mut self) {
148        self.status = TaskStatus::Running;
149    }
150
151    /// Marks the task as completed.
152    pub fn mark_completed(&mut self) {
153        self.status = TaskStatus::Completed;
154    }
155
156    /// Marks the task as failed.
157    pub fn mark_failed(&mut self) {
158        self.status = TaskStatus::Failed;
159    }
160}
161
162impl PartialEq for DistributedTask {
163    fn eq(&self, other: &Self) -> bool {
164        self.id == other.id
165    }
166}
167
168impl Eq for DistributedTask {}
169
170impl PartialOrd for DistributedTask {
171    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
172        Some(self.cmp(other))
173    }
174}
175
176impl Ord for DistributedTask {
177    fn cmp(&self, other: &Self) -> Ordering {
178        // Higher priority first, then lower sequence (FIFO)
179        match self.priority.cmp(&other.priority) {
180            Ordering::Equal => other.sequence.cmp(&self.sequence), // lower seq = earlier
181            other_ord => other_ord,
182        }
183    }
184}
185
186/// A priority-based task queue for distributed task scheduling.
187///
188/// Tasks are dequeued in priority order; within the same priority,
189/// tasks follow FIFO ordering based on their enqueue sequence.
190#[derive(Debug)]
191pub struct TaskQueue {
192    /// The priority heap
193    heap: BinaryHeap<DistributedTask>,
194    /// Monotonically increasing sequence counter
195    next_sequence: u64,
196    /// Maximum queue capacity (0 = unlimited)
197    max_capacity: usize,
198    /// Total tasks ever enqueued
199    total_enqueued: u64,
200    /// Total tasks ever dequeued
201    total_dequeued: u64,
202}
203
204impl TaskQueue {
205    /// Creates a new empty task queue.
206    #[must_use]
207    pub fn new() -> Self {
208        Self {
209            heap: BinaryHeap::new(),
210            next_sequence: 0,
211            max_capacity: 0,
212            total_enqueued: 0,
213            total_dequeued: 0,
214        }
215    }
216
217    /// Creates a new task queue with a capacity limit.
218    #[must_use]
219    pub fn with_capacity(max_capacity: usize) -> Self {
220        Self {
221            heap: BinaryHeap::new(),
222            next_sequence: 0,
223            max_capacity,
224            total_enqueued: 0,
225            total_dequeued: 0,
226        }
227    }
228
229    /// Enqueues a task. Returns false if the queue is at capacity.
230    pub fn enqueue(&mut self, mut task: DistributedTask) -> bool {
231        if self.max_capacity > 0 && self.heap.len() >= self.max_capacity {
232            return false;
233        }
234        task.sequence = self.next_sequence;
235        self.next_sequence += 1;
236        self.total_enqueued += 1;
237        self.heap.push(task);
238        true
239    }
240
241    /// Dequeues the highest-priority task.
242    ///
243    /// Returns `None` if the queue is empty.
244    pub fn dequeue(&mut self) -> Option<DistributedTask> {
245        let task = self.heap.pop()?;
246        self.total_dequeued += 1;
247        Some(task)
248    }
249
250    /// Peeks at the highest-priority task without removing it.
251    #[must_use]
252    pub fn peek(&self) -> Option<&DistributedTask> {
253        self.heap.peek()
254    }
255
256    /// Returns the number of tasks in the queue.
257    #[must_use]
258    pub fn len(&self) -> usize {
259        self.heap.len()
260    }
261
262    /// Returns true if the queue is empty.
263    #[must_use]
264    pub fn is_empty(&self) -> bool {
265        self.heap.is_empty()
266    }
267
268    /// Returns total tasks ever enqueued.
269    #[must_use]
270    pub fn total_enqueued(&self) -> u64 {
271        self.total_enqueued
272    }
273
274    /// Returns total tasks ever dequeued.
275    #[must_use]
276    pub fn total_dequeued(&self) -> u64 {
277        self.total_dequeued
278    }
279
280    /// Drains all tasks from the queue in priority order.
281    pub fn drain(&mut self) -> Vec<DistributedTask> {
282        let mut result = Vec::with_capacity(self.heap.len());
283        while let Some(task) = self.heap.pop() {
284            result.push(task);
285        }
286        self.total_dequeued += result.len() as u64;
287        result
288    }
289
290    /// Removes tasks that have passed their deadline.
291    pub fn remove_expired(&mut self, now: i64) -> Vec<DistributedTask> {
292        let mut remaining = Vec::new();
293        let mut expired = Vec::new();
294        while let Some(task) = self.heap.pop() {
295            if task.is_past_deadline(now) {
296                expired.push(task);
297            } else {
298                remaining.push(task);
299            }
300        }
301        for task in remaining {
302            self.heap.push(task);
303        }
304        expired
305    }
306}
307
308impl Default for TaskQueue {
309    fn default() -> Self {
310        Self::new()
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_task_priority_ordering() {
320        assert!(TaskPriority::Critical > TaskPriority::Urgent);
321        assert!(TaskPriority::Urgent > TaskPriority::High);
322        assert!(TaskPriority::High > TaskPriority::Normal);
323        assert!(TaskPriority::Normal > TaskPriority::Background);
324    }
325
326    #[test]
327    fn test_task_priority_is_higher_than() {
328        assert!(TaskPriority::Critical.is_higher_than(&TaskPriority::High));
329        assert!(!TaskPriority::Normal.is_higher_than(&TaskPriority::High));
330    }
331
332    #[test]
333    fn test_task_creation() {
334        let task =
335            DistributedTask::new("encode_video", TaskPriority::Normal, "{\"file\":\"a.mp4\"}");
336        assert_eq!(task.name, "encode_video");
337        assert_eq!(task.priority, TaskPriority::Normal);
338        assert_eq!(task.status, TaskStatus::Pending);
339        assert_eq!(task.retry_count, 0);
340    }
341
342    #[test]
343    fn test_task_with_deadline() {
344        let task = DistributedTask::new("t1", TaskPriority::High, "{}").with_deadline(9999);
345        assert_eq!(task.deadline, Some(9999));
346        assert!(!task.is_past_deadline(9998));
347        assert!(task.is_past_deadline(10000));
348    }
349
350    #[test]
351    fn test_task_retry() {
352        let mut task = DistributedTask::new("t1", TaskPriority::Normal, "{}").with_max_retries(2);
353        assert!(task.can_retry());
354        task.retry();
355        assert_eq!(task.retry_count, 1);
356        task.retry();
357        assert!(!task.can_retry());
358    }
359
360    #[test]
361    fn test_task_lifecycle() {
362        let mut task = DistributedTask::new("t1", TaskPriority::Normal, "{}");
363        assert_eq!(task.status, TaskStatus::Pending);
364        task.mark_running();
365        assert_eq!(task.status, TaskStatus::Running);
366        task.mark_completed();
367        assert_eq!(task.status, TaskStatus::Completed);
368    }
369
370    #[test]
371    fn test_queue_enqueue_dequeue() {
372        let mut q = TaskQueue::new();
373        q.enqueue(DistributedTask::new("t1", TaskPriority::Normal, "{}"));
374        assert_eq!(q.len(), 1);
375        let t = q.dequeue().expect("dequeue should return a task");
376        assert_eq!(t.name, "t1");
377        assert!(q.is_empty());
378    }
379
380    #[test]
381    fn test_queue_priority_order() {
382        let mut q = TaskQueue::new();
383        q.enqueue(DistributedTask::new("low", TaskPriority::Background, "{}"));
384        q.enqueue(DistributedTask::new("high", TaskPriority::High, "{}"));
385        q.enqueue(DistributedTask::new("normal", TaskPriority::Normal, "{}"));
386        let first = q.dequeue().expect("dequeue should return a task");
387        assert_eq!(first.name, "high");
388        let second = q.dequeue().expect("dequeue should return a task");
389        assert_eq!(second.name, "normal");
390        let third = q.dequeue().expect("dequeue should return a task");
391        assert_eq!(third.name, "low");
392    }
393
394    #[test]
395    fn test_queue_fifo_within_same_priority() {
396        let mut q = TaskQueue::new();
397        q.enqueue(DistributedTask::new("first", TaskPriority::Normal, "{}"));
398        q.enqueue(DistributedTask::new("second", TaskPriority::Normal, "{}"));
399        q.enqueue(DistributedTask::new("third", TaskPriority::Normal, "{}"));
400        assert_eq!(
401            q.dequeue().expect("dequeue should return a task").name,
402            "first"
403        );
404        assert_eq!(
405            q.dequeue().expect("dequeue should return a task").name,
406            "second"
407        );
408        assert_eq!(
409            q.dequeue().expect("dequeue should return a task").name,
410            "third"
411        );
412    }
413
414    #[test]
415    fn test_queue_capacity_limit() {
416        let mut q = TaskQueue::with_capacity(2);
417        assert!(q.enqueue(DistributedTask::new("t1", TaskPriority::Normal, "{}")));
418        assert!(q.enqueue(DistributedTask::new("t2", TaskPriority::Normal, "{}")));
419        assert!(!q.enqueue(DistributedTask::new("t3", TaskPriority::Normal, "{}")));
420        assert_eq!(q.len(), 2);
421    }
422
423    #[test]
424    fn test_queue_peek() {
425        let mut q = TaskQueue::new();
426        assert!(q.peek().is_none());
427        q.enqueue(DistributedTask::new("t1", TaskPriority::High, "{}"));
428        assert_eq!(q.peek().expect("peek should return a value").name, "t1");
429        assert_eq!(q.len(), 1); // peek doesn't remove
430    }
431
432    #[test]
433    fn test_queue_drain() {
434        let mut q = TaskQueue::new();
435        q.enqueue(DistributedTask::new("t1", TaskPriority::Normal, "{}"));
436        q.enqueue(DistributedTask::new("t2", TaskPriority::High, "{}"));
437        let drained = q.drain();
438        assert_eq!(drained.len(), 2);
439        assert!(q.is_empty());
440        assert_eq!(drained[0].name, "t2"); // high priority first
441    }
442
443    #[test]
444    fn test_queue_remove_expired() {
445        let mut q = TaskQueue::new();
446        q.enqueue(DistributedTask::new("expired", TaskPriority::Normal, "{}").with_deadline(100));
447        q.enqueue(DistributedTask::new("alive", TaskPriority::Normal, "{}").with_deadline(9999));
448        q.enqueue(DistributedTask::new(
449            "no_deadline",
450            TaskPriority::Normal,
451            "{}",
452        ));
453        let expired = q.remove_expired(200);
454        assert_eq!(expired.len(), 1);
455        assert_eq!(expired[0].name, "expired");
456        assert_eq!(q.len(), 2);
457    }
458
459    #[test]
460    fn test_queue_counters() {
461        let mut q = TaskQueue::new();
462        q.enqueue(DistributedTask::new("t1", TaskPriority::Normal, "{}"));
463        q.enqueue(DistributedTask::new("t2", TaskPriority::Normal, "{}"));
464        let _ = q.dequeue();
465        assert_eq!(q.total_enqueued(), 2);
466        assert_eq!(q.total_dequeued(), 1);
467    }
468
469    #[test]
470    fn test_task_mark_failed() {
471        let mut task = DistributedTask::new("t1", TaskPriority::Normal, "{}");
472        task.mark_failed();
473        assert_eq!(task.status, TaskStatus::Failed);
474    }
475}