1#![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
19pub struct JobScheduler {
21 priority_queue: BinaryHeap<ScheduledJob>,
23
24 fifo_queue: VecDeque<ScheduledJob>,
26
27 deadline_queue: BinaryHeap<DeadlineJob>,
29
30 resource_tracker: ResourceTracker,
32
33 policy: SchedulingPolicy,
35
36 stats: SchedulerStats,
38}
39
40#[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 self.priority.cmp(&other.priority)
68 }
69}
70
71#[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 other.deadline.cmp(&self.deadline)
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub enum SchedulingPolicy {
102 #[default]
104 Priority,
105 FIFO,
107 EDF,
109 FairShare,
111 SJF,
113}
114
115struct ResourceTracker {
117 cpu_allocated: u32,
119 memory_allocated: u64,
121 gpu_allocated: HashMap<String, bool>,
123 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 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 return false;
146 }
147
148 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#[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, gpu_id: None,
188 }
189 }
190}
191
192#[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 #[must_use]
204 pub fn new() -> Self {
205 Self::with_policy(SchedulingPolicy::default())
206 }
207
208 #[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 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 self.priority_queue.push(job);
241 }
242 }
243 SchedulingPolicy::FairShare => {
244 self.fifo_queue.push_back(job);
246 }
247 SchedulingPolicy::SJF => {
248 self.priority_queue.push(job);
251 }
252 }
253
254 self.stats.total_jobs_scheduled += 1;
255 }
256
257 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 if let Some(deadline_job) = self.deadline_queue.pop() {
265 if deadline_job.deadline > SystemTime::now() {
267 return Some(deadline_job.job);
268 }
269 warn!("Job {} missed deadline", deadline_job.job.job_id);
271 return self.next_job();
272 }
273 self.priority_queue.pop()
275 }
276 SchedulingPolicy::FairShare => self.fair_share_next(),
277 SchedulingPolicy::SJF => self.shortest_job_first(),
278 }
279 }
280
281 fn fair_share_next(&mut self) -> Option<ScheduledJob> {
283 self.fifo_queue.pop_front()
286 }
287
288 fn shortest_job_first(&mut self) -> Option<ScheduledJob> {
290 self.priority_queue.pop()
293 }
294
295 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 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 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 #[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 #[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 #[must_use]
346 pub fn statistics(&self) -> &SchedulerStats {
347 &self.stats
348 }
349
350 pub fn clear(&mut self) {
352 self.priority_queue.clear();
353 self.fifo_queue.clear();
354 self.deadline_queue.clear();
355 }
356
357 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 fn migrate_queues(&mut self) {
368 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 for job in all_jobs {
385 self.enqueue(job);
386 }
387 }
388
389 pub fn optimize(&mut self) {
391 match self.policy {
393 SchedulingPolicy::Priority => {
394 }
396 SchedulingPolicy::EDF => {
397 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
426pub struct SchedulingBuilder {
428 policy: SchedulingPolicy,
429 max_cpu: u32,
430 max_memory: u64,
431 enable_preemption: bool,
432}
433
434impl SchedulingBuilder {
435 #[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 #[must_use]
448 pub fn policy(mut self, policy: SchedulingPolicy) -> Self {
449 self.policy = policy;
450 self
451 }
452
453 #[must_use]
455 pub fn max_cpu(mut self, max_cpu: u32) -> Self {
456 self.max_cpu = max_cpu;
457 self
458 }
459
460 #[must_use]
462 pub fn max_memory(mut self, max_memory: u64) -> Self {
463 self.max_memory = max_memory;
464 self
465 }
466
467 #[must_use]
469 pub fn enable_preemption(mut self, enable: bool) -> Self {
470 self.enable_preemption = enable;
471 self
472 }
473
474 #[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#[derive(Debug, Clone)]
489pub struct TaskAffinity {
490 pub preferred_workers: Vec<String>,
492 pub required_capabilities: Vec<String>,
494 pub requires_gpu: bool,
496}
497
498impl TaskAffinity {
499 #[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 #[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 #[must_use]
518 pub fn require_capability(mut self, capability: String) -> Self {
519 self.required_capabilities.push(capability);
520 self
521 }
522
523 #[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
537pub struct BackfillingScheduler {
539 main_queue: JobScheduler,
540 backfill_queue: VecDeque<ScheduledJob>,
541}
542
543impl BackfillingScheduler {
544 #[must_use]
546 pub fn new() -> Self {
547 Self {
548 main_queue: JobScheduler::new(),
549 backfill_queue: VecDeque::new(),
550 }
551 }
552
553 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 pub fn next_job(&mut self) -> Option<ScheduledJob> {
564 if let Some(job) = self.main_queue.next_job() {
566 return Some(job);
567 }
568
569 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 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}