Skip to main content

oximedia_distributed/
scheduler.rs

1//! Job scheduling algorithms and priority management.
2//!
3//! The scheduler handles:
4//! - Priority queue management
5//! - Resource allocation
6//! - Deadline-based scheduling
7//! - Fairness policies
8//! - Preemption support
9
10#![allow(dead_code)]
11
12use crate::pb::EncodingTask;
13use crate::{JobPriority, Result};
14use std::cmp::Ordering as CmpOrdering;
15use std::collections::{BinaryHeap, HashMap, VecDeque};
16use std::time::{Duration, SystemTime};
17use tracing::{debug, info, warn};
18
19/// Job scheduler with multiple scheduling strategies
20pub struct JobScheduler {
21    /// Priority queue for jobs
22    priority_queue: BinaryHeap<ScheduledJob>,
23
24    /// FIFO queue for fair scheduling
25    fifo_queue: VecDeque<ScheduledJob>,
26
27    /// Deadline-based queue
28    deadline_queue: BinaryHeap<DeadlineJob>,
29
30    /// Resource allocation tracker
31    resource_tracker: ResourceTracker,
32
33    /// Scheduling policy
34    policy: SchedulingPolicy,
35
36    /// Statistics
37    stats: SchedulerStats,
38}
39
40/// Scheduled job representation
41#[derive(Debug, Clone)]
42pub struct ScheduledJob {
43    pub job_id: String,
44    pub task_id: String,
45    pub priority: JobPriority,
46    pub deadline: Option<SystemTime>,
47    pub encoding_task: Option<EncodingTask>,
48}
49
50impl PartialEq for ScheduledJob {
51    fn eq(&self, other: &Self) -> bool {
52        self.job_id == other.job_id
53    }
54}
55
56impl Eq for ScheduledJob {}
57
58impl PartialOrd for ScheduledJob {
59    fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
60        Some(self.cmp(other))
61    }
62}
63
64impl Ord for ScheduledJob {
65    fn cmp(&self, other: &Self) -> CmpOrdering {
66        // Higher priority comes first
67        self.priority.cmp(&other.priority)
68    }
69}
70
71/// Deadline-based job wrapper
72#[derive(Debug, Clone)]
73struct DeadlineJob {
74    job: ScheduledJob,
75    deadline: SystemTime,
76}
77
78impl PartialEq for DeadlineJob {
79    fn eq(&self, other: &Self) -> bool {
80        self.job.job_id == other.job.job_id
81    }
82}
83
84impl Eq for DeadlineJob {}
85
86impl PartialOrd for DeadlineJob {
87    fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
88        Some(self.cmp(other))
89    }
90}
91
92impl Ord for DeadlineJob {
93    fn cmp(&self, other: &Self) -> CmpOrdering {
94        // Earlier deadline comes first (reverse ordering for max heap)
95        other.deadline.cmp(&self.deadline)
96    }
97}
98
99/// Scheduling policy
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub enum SchedulingPolicy {
102    /// Priority-based scheduling
103    #[default]
104    Priority,
105    /// First-In-First-Out
106    FIFO,
107    /// Earliest Deadline First
108    EDF,
109    /// Fair share scheduling
110    FairShare,
111    /// Shortest Job First
112    SJF,
113}
114
115/// Resource allocation tracker
116struct ResourceTracker {
117    /// CPU cores allocated
118    cpu_allocated: u32,
119    /// Memory allocated (bytes)
120    memory_allocated: u64,
121    /// GPU devices allocated
122    gpu_allocated: HashMap<String, bool>,
123    /// Job resource usage
124    job_resources: HashMap<String, ResourceAllocation>,
125}
126
127impl ResourceTracker {
128    fn new() -> Self {
129        Self {
130            cpu_allocated: 0,
131            memory_allocated: 0,
132            gpu_allocated: HashMap::new(),
133            job_resources: HashMap::new(),
134        }
135    }
136
137    fn allocate(&mut self, job_id: &str, allocation: ResourceAllocation) -> bool {
138        // Check if resources are available
139        if self.cpu_allocated + allocation.cpu_cores > 1000 {
140            return false;
141        }
142
143        if self.memory_allocated + allocation.memory_bytes > 1_099_511_627_776 {
144            // 1TB limit
145            return false;
146        }
147
148        // Allocate resources
149        self.cpu_allocated += allocation.cpu_cores;
150        self.memory_allocated += allocation.memory_bytes;
151        self.job_resources.insert(job_id.to_string(), allocation);
152
153        true
154    }
155
156    fn release(&mut self, job_id: &str) {
157        if let Some(allocation) = self.job_resources.remove(job_id) {
158            self.cpu_allocated = self.cpu_allocated.saturating_sub(allocation.cpu_cores);
159            self.memory_allocated = self
160                .memory_allocated
161                .saturating_sub(allocation.memory_bytes);
162        }
163    }
164
165    fn available_cpu(&self) -> u32 {
166        1000u32.saturating_sub(self.cpu_allocated)
167    }
168
169    fn available_memory(&self) -> u64 {
170        1_099_511_627_776u64.saturating_sub(self.memory_allocated)
171    }
172}
173
174/// Resource allocation for a job
175#[derive(Debug, Clone, Copy)]
176pub struct ResourceAllocation {
177    pub cpu_cores: u32,
178    pub memory_bytes: u64,
179    pub gpu_id: Option<usize>,
180}
181
182impl Default for ResourceAllocation {
183    fn default() -> Self {
184        Self {
185            cpu_cores: 4,
186            memory_bytes: 4_294_967_296, // 4GB
187            gpu_id: None,
188        }
189    }
190}
191
192/// Scheduler statistics
193#[derive(Debug, Default)]
194pub struct SchedulerStats {
195    pub total_jobs_scheduled: u64,
196    pub total_jobs_completed: u64,
197    pub total_jobs_preempted: u64,
198    pub average_wait_time: Duration,
199}
200
201impl JobScheduler {
202    /// Create a new job scheduler
203    #[must_use]
204    pub fn new() -> Self {
205        Self::with_policy(SchedulingPolicy::default())
206    }
207
208    /// Create a scheduler with a specific policy
209    #[must_use]
210    pub fn with_policy(policy: SchedulingPolicy) -> Self {
211        Self {
212            priority_queue: BinaryHeap::new(),
213            fifo_queue: VecDeque::new(),
214            deadline_queue: BinaryHeap::new(),
215            resource_tracker: ResourceTracker::new(),
216            policy,
217            stats: SchedulerStats::default(),
218        }
219    }
220
221    /// Enqueue a job for scheduling
222    pub fn enqueue(&mut self, job: ScheduledJob) {
223        info!(
224            "Enqueueing job {} with priority {:?}",
225            job.job_id, job.priority
226        );
227
228        match self.policy {
229            SchedulingPolicy::Priority => {
230                self.priority_queue.push(job);
231            }
232            SchedulingPolicy::FIFO => {
233                self.fifo_queue.push_back(job);
234            }
235            SchedulingPolicy::EDF => {
236                if let Some(deadline) = job.deadline {
237                    self.deadline_queue.push(DeadlineJob { job, deadline });
238                } else {
239                    // No deadline, use priority queue
240                    self.priority_queue.push(job);
241                }
242            }
243            SchedulingPolicy::FairShare => {
244                // Fair share uses FIFO with task-based balancing
245                self.fifo_queue.push_back(job);
246            }
247            SchedulingPolicy::SJF => {
248                // Shortest Job First - use priority queue
249                // In practice, would estimate job duration
250                self.priority_queue.push(job);
251            }
252        }
253
254        self.stats.total_jobs_scheduled += 1;
255    }
256
257    /// Get the next job to schedule
258    pub fn next_job(&mut self) -> Option<ScheduledJob> {
259        match self.policy {
260            SchedulingPolicy::Priority => self.priority_queue.pop(),
261            SchedulingPolicy::FIFO => self.fifo_queue.pop_front(),
262            SchedulingPolicy::EDF => {
263                // Check deadline queue first
264                if let Some(deadline_job) = self.deadline_queue.pop() {
265                    // Check if deadline is still valid
266                    if deadline_job.deadline > SystemTime::now() {
267                        return Some(deadline_job.job);
268                    }
269                    // Deadline passed, drop job
270                    warn!("Job {} missed deadline", deadline_job.job.job_id);
271                    return self.next_job();
272                }
273                // Fall back to priority queue
274                self.priority_queue.pop()
275            }
276            SchedulingPolicy::FairShare => self.fair_share_next(),
277            SchedulingPolicy::SJF => self.shortest_job_first(),
278        }
279    }
280
281    /// Fair share scheduling
282    fn fair_share_next(&mut self) -> Option<ScheduledJob> {
283        // Simple fair share: round-robin by task_id
284        // In production, would track task quotas
285        self.fifo_queue.pop_front()
286    }
287
288    /// Shortest Job First scheduling
289    fn shortest_job_first(&mut self) -> Option<ScheduledJob> {
290        // Estimate job duration and return shortest
291        // For now, use priority queue
292        self.priority_queue.pop()
293    }
294
295    /// Allocate resources for a job
296    pub fn allocate_resources(&mut self, job_id: &str) -> Option<ResourceAllocation> {
297        let allocation = ResourceAllocation::default();
298
299        if self.resource_tracker.allocate(job_id, allocation) {
300            debug!("Allocated resources for job {}", job_id);
301            Some(allocation)
302        } else {
303            debug!("Insufficient resources for job {}", job_id);
304            None
305        }
306    }
307
308    /// Release resources for a completed job
309    pub fn release_resources(&mut self, job_id: &str) {
310        debug!("Releasing resources for job {}", job_id);
311        self.resource_tracker.release(job_id);
312        self.stats.total_jobs_completed += 1;
313    }
314
315    /// Preempt a job (remove and re-enqueue)
316    pub fn preempt_job(&mut self, job_id: &str) -> Result<()> {
317        info!("Preempting job {}", job_id);
318        self.resource_tracker.release(job_id);
319        self.stats.total_jobs_preempted += 1;
320        Ok(())
321    }
322
323    /// Get queue length
324    #[must_use]
325    pub fn queue_length(&self) -> usize {
326        match self.policy {
327            SchedulingPolicy::Priority => self.priority_queue.len(),
328            SchedulingPolicy::FIFO => self.fifo_queue.len(),
329            SchedulingPolicy::EDF => self.deadline_queue.len() + self.priority_queue.len(),
330            SchedulingPolicy::FairShare => self.fifo_queue.len(),
331            SchedulingPolicy::SJF => self.priority_queue.len(),
332        }
333    }
334
335    /// Get available resources
336    #[must_use]
337    pub fn available_resources(&self) -> (u32, u64) {
338        (
339            self.resource_tracker.available_cpu(),
340            self.resource_tracker.available_memory(),
341        )
342    }
343
344    /// Get scheduler statistics
345    #[must_use]
346    pub fn statistics(&self) -> &SchedulerStats {
347        &self.stats
348    }
349
350    /// Clear all queues
351    pub fn clear(&mut self) {
352        self.priority_queue.clear();
353        self.fifo_queue.clear();
354        self.deadline_queue.clear();
355    }
356
357    /// Set scheduling policy
358    pub fn set_policy(&mut self, policy: SchedulingPolicy) {
359        if self.policy != policy {
360            info!("Changing scheduling policy to {:?}", policy);
361            self.policy = policy;
362            self.migrate_queues();
363        }
364    }
365
366    /// Migrate jobs between queues when policy changes
367    fn migrate_queues(&mut self) {
368        // Collect all jobs
369        let mut all_jobs = Vec::new();
370
371        while let Some(job) = self.priority_queue.pop() {
372            all_jobs.push(job);
373        }
374
375        while let Some(job) = self.fifo_queue.pop_front() {
376            all_jobs.push(job);
377        }
378
379        while let Some(deadline_job) = self.deadline_queue.pop() {
380            all_jobs.push(deadline_job.job);
381        }
382
383        // Re-enqueue with new policy
384        for job in all_jobs {
385            self.enqueue(job);
386        }
387    }
388
389    /// Optimize queue order
390    pub fn optimize(&mut self) {
391        // Re-prioritize jobs based on current conditions
392        match self.policy {
393            SchedulingPolicy::Priority => {
394                // Already optimized by heap
395            }
396            SchedulingPolicy::EDF => {
397                // Check for deadline violations and re-prioritize
398                let now = SystemTime::now();
399                let urgent_jobs: Vec<_> = self
400                    .deadline_queue
401                    .iter()
402                    .filter(|dj| {
403                        dj.deadline
404                            .duration_since(now)
405                            .map(|d| d < Duration::from_secs(60))
406                            .unwrap_or(true)
407                    })
408                    .cloned()
409                    .collect();
410
411                if !urgent_jobs.is_empty() {
412                    debug!("Found {} urgent jobs", urgent_jobs.len());
413                }
414            }
415            _ => {}
416        }
417    }
418}
419
420impl Default for JobScheduler {
421    fn default() -> Self {
422        Self::new()
423    }
424}
425
426/// Job scheduling builder for complex scheduling scenarios
427pub struct SchedulingBuilder {
428    policy: SchedulingPolicy,
429    max_cpu: u32,
430    max_memory: u64,
431    enable_preemption: bool,
432}
433
434impl SchedulingBuilder {
435    /// Create a new scheduling builder
436    #[must_use]
437    pub fn new() -> Self {
438        Self {
439            policy: SchedulingPolicy::Priority,
440            max_cpu: 1000,
441            max_memory: 1_099_511_627_776,
442            enable_preemption: false,
443        }
444    }
445
446    /// Set scheduling policy
447    #[must_use]
448    pub fn policy(mut self, policy: SchedulingPolicy) -> Self {
449        self.policy = policy;
450        self
451    }
452
453    /// Set maximum CPU allocation
454    #[must_use]
455    pub fn max_cpu(mut self, max_cpu: u32) -> Self {
456        self.max_cpu = max_cpu;
457        self
458    }
459
460    /// Set maximum memory allocation
461    #[must_use]
462    pub fn max_memory(mut self, max_memory: u64) -> Self {
463        self.max_memory = max_memory;
464        self
465    }
466
467    /// Enable job preemption
468    #[must_use]
469    pub fn enable_preemption(mut self, enable: bool) -> Self {
470        self.enable_preemption = enable;
471        self
472    }
473
474    /// Build the scheduler
475    #[must_use]
476    pub fn build(self) -> JobScheduler {
477        JobScheduler::with_policy(self.policy)
478    }
479}
480
481impl Default for SchedulingBuilder {
482    fn default() -> Self {
483        Self::new()
484    }
485}
486
487/// Task affinity for scheduling optimization
488#[derive(Debug, Clone)]
489pub struct TaskAffinity {
490    /// Preferred worker IDs
491    pub preferred_workers: Vec<String>,
492    /// Required capabilities
493    pub required_capabilities: Vec<String>,
494    /// GPU requirement
495    pub requires_gpu: bool,
496}
497
498impl TaskAffinity {
499    /// Create a new task affinity
500    #[must_use]
501    pub fn new() -> Self {
502        Self {
503            preferred_workers: Vec::new(),
504            required_capabilities: Vec::new(),
505            requires_gpu: false,
506        }
507    }
508
509    /// Add preferred worker
510    #[must_use]
511    pub fn prefer_worker(mut self, worker_id: String) -> Self {
512        self.preferred_workers.push(worker_id);
513        self
514    }
515
516    /// Add required capability
517    #[must_use]
518    pub fn require_capability(mut self, capability: String) -> Self {
519        self.required_capabilities.push(capability);
520        self
521    }
522
523    /// Set GPU requirement
524    #[must_use]
525    pub fn require_gpu(mut self, require: bool) -> Self {
526        self.requires_gpu = require;
527        self
528    }
529}
530
531impl Default for TaskAffinity {
532    fn default() -> Self {
533        Self::new()
534    }
535}
536
537/// Backfilling scheduler for improved utilization
538pub struct BackfillingScheduler {
539    main_queue: JobScheduler,
540    backfill_queue: VecDeque<ScheduledJob>,
541}
542
543impl BackfillingScheduler {
544    /// Create a new backfilling scheduler
545    #[must_use]
546    pub fn new() -> Self {
547        Self {
548            main_queue: JobScheduler::new(),
549            backfill_queue: VecDeque::new(),
550        }
551    }
552
553    /// Enqueue a job
554    pub fn enqueue(&mut self, job: ScheduledJob) {
555        if job.priority == JobPriority::Low {
556            self.backfill_queue.push_back(job);
557        } else {
558            self.main_queue.enqueue(job);
559        }
560    }
561
562    /// Get next job with backfilling
563    pub fn next_job(&mut self) -> Option<ScheduledJob> {
564        // Try main queue first
565        if let Some(job) = self.main_queue.next_job() {
566            return Some(job);
567        }
568
569        // Try backfill if resources available
570        if !self.backfill_queue.is_empty() {
571            let (cpu, mem) = self.main_queue.available_resources();
572            if cpu >= 4 && mem >= 4_294_967_296 {
573                return self.backfill_queue.pop_front();
574            }
575        }
576
577        None
578    }
579
580    /// Clear all queues
581    pub fn clear(&mut self) {
582        self.main_queue.clear();
583        self.backfill_queue.clear();
584    }
585}
586
587impl Default for BackfillingScheduler {
588    fn default() -> Self {
589        Self::new()
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use uuid::Uuid;
597
598    #[test]
599    fn test_scheduler_creation() {
600        let scheduler = JobScheduler::new();
601        assert_eq!(scheduler.queue_length(), 0);
602    }
603
604    #[test]
605    fn test_priority_scheduling() {
606        let mut scheduler = JobScheduler::with_policy(SchedulingPolicy::Priority);
607
608        let job1 = ScheduledJob {
609            job_id: Uuid::new_v4().to_string(),
610            task_id: Uuid::new_v4().to_string(),
611            priority: JobPriority::Low,
612            deadline: None,
613            encoding_task: None,
614        };
615
616        let job2 = ScheduledJob {
617            job_id: Uuid::new_v4().to_string(),
618            task_id: Uuid::new_v4().to_string(),
619            priority: JobPriority::Critical,
620            deadline: None,
621            encoding_task: None,
622        };
623
624        scheduler.enqueue(job1);
625        scheduler.enqueue(job2.clone());
626
627        let next = scheduler.next_job();
628        assert!(next.is_some());
629        assert_eq!(next.expect("next job should exist").job_id, job2.job_id);
630    }
631
632    #[test]
633    fn test_fifo_scheduling() {
634        let mut scheduler = JobScheduler::with_policy(SchedulingPolicy::FIFO);
635
636        let job1 = ScheduledJob {
637            job_id: "job1".to_string(),
638            task_id: Uuid::new_v4().to_string(),
639            priority: JobPriority::Critical,
640            deadline: None,
641            encoding_task: None,
642        };
643
644        let job2 = ScheduledJob {
645            job_id: "job2".to_string(),
646            task_id: Uuid::new_v4().to_string(),
647            priority: JobPriority::Low,
648            deadline: None,
649            encoding_task: None,
650        };
651
652        scheduler.enqueue(job1.clone());
653        scheduler.enqueue(job2);
654
655        let next = scheduler.next_job();
656        assert!(next.is_some());
657        assert_eq!(next.expect("next job should exist").job_id, job1.job_id);
658    }
659
660    #[test]
661    fn test_resource_allocation() {
662        let mut scheduler = JobScheduler::new();
663        let job_id = "test_job";
664
665        let allocation = scheduler.allocate_resources(job_id);
666        assert!(allocation.is_some());
667
668        scheduler.release_resources(job_id);
669        assert_eq!(scheduler.stats.total_jobs_completed, 1);
670    }
671
672    #[test]
673    fn test_scheduling_builder() {
674        let scheduler = SchedulingBuilder::new()
675            .policy(SchedulingPolicy::EDF)
676            .max_cpu(100)
677            .max_memory(1_073_741_824)
678            .enable_preemption(true)
679            .build();
680
681        assert_eq!(scheduler.queue_length(), 0);
682    }
683
684    #[test]
685    fn test_backfilling() {
686        let mut scheduler = BackfillingScheduler::new();
687
688        let job1 = ScheduledJob {
689            job_id: "job1".to_string(),
690            task_id: Uuid::new_v4().to_string(),
691            priority: JobPriority::Normal,
692            deadline: None,
693            encoding_task: None,
694        };
695
696        let job2 = ScheduledJob {
697            job_id: "job2".to_string(),
698            task_id: Uuid::new_v4().to_string(),
699            priority: JobPriority::Low,
700            deadline: None,
701            encoding_task: None,
702        };
703
704        scheduler.enqueue(job1.clone());
705        scheduler.enqueue(job2);
706
707        let next = scheduler.next_job();
708        assert!(next.is_some());
709        assert_eq!(next.expect("next job should exist").job_id, job1.job_id);
710    }
711
712    #[test]
713    fn test_policy_migration() {
714        let mut scheduler = JobScheduler::with_policy(SchedulingPolicy::Priority);
715
716        for i in 0..5 {
717            scheduler.enqueue(ScheduledJob {
718                job_id: format!("job{}", i),
719                task_id: Uuid::new_v4().to_string(),
720                priority: JobPriority::Normal,
721                deadline: None,
722                encoding_task: None,
723            });
724        }
725
726        assert_eq!(scheduler.queue_length(), 5);
727
728        scheduler.set_policy(SchedulingPolicy::FIFO);
729        assert_eq!(scheduler.queue_length(), 5);
730    }
731}