Skip to main content

orch/engine/
priority.rs

1use std::str::FromStr;
2use std::sync::{Arc, OnceLock};
3
4use tokio::sync::{OwnedSemaphorePermit, Semaphore};
5
6const HIGH_LABEL: &str = "priority:high";
7const LOW_LABEL: &str = "priority:low";
8const PREFIX: &str = "priority:";
9
10/// Dispatch priority read from the `priority:high` and `priority:low` labels
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
12pub enum Priority {
13    Low,
14    #[default]
15    Normal,
16    High,
17}
18
19impl Priority {
20    /// The highest label wins when a task carries both
21    pub fn from_labels(labels: &[String]) -> Self {
22        let mut found = Self::Normal;
23        for label in labels {
24            match label.as_str() {
25                HIGH_LABEL => return Self::High,
26                LOW_LABEL => found = Self::Low,
27                _ => {}
28            }
29        }
30        found
31    }
32
33    pub fn as_str(self) -> &'static str {
34        match self {
35            Self::Low => "low",
36            Self::Normal => "normal",
37            Self::High => "high",
38        }
39    }
40
41    fn label(self) -> Option<&'static str> {
42        match self {
43            Self::Low => Some(LOW_LABEL),
44            Self::Normal => None,
45            Self::High => Some(HIGH_LABEL),
46        }
47    }
48
49    /// `labels` with every priority label replaced by the one for `self`
50    pub fn apply_to(self, labels: &[String]) -> Vec<String> {
51        let mut out: Vec<String> = labels
52            .iter()
53            .filter(|l| !is_priority_label(l))
54            .cloned()
55            .collect();
56        if let Some(label) = self.label() {
57            out.push(label.to_string());
58        }
59        out
60    }
61}
62
63impl FromStr for Priority {
64    type Err = anyhow::Error;
65
66    fn from_str(s: &str) -> Result<Self, Self::Err> {
67        match s.trim().to_ascii_lowercase().as_str() {
68            "high" => Ok(Self::High),
69            "normal" => Ok(Self::Normal),
70            "low" => Ok(Self::Low),
71            other => anyhow::bail!("unknown priority '{other}', use high, normal or low"),
72        }
73    }
74}
75
76pub fn is_priority_label(label: &str) -> bool {
77    label.starts_with(PREFIX)
78}
79
80/// Highest priority first, arrival order kept inside a priority
81pub fn sort_by_priority<T>(tasks: &mut [T], labels_of: impl Fn(&T) -> &[String]) {
82    tasks.sort_by_key(|t| std::cmp::Reverse(Priority::from_labels(labels_of(t))));
83}
84
85/// The one slot a `priority:high` task may take beyond `max_parallel`
86fn extra_slot() -> &'static Arc<Semaphore> {
87    static SLOT: OnceLock<Arc<Semaphore>> = OnceLock::new();
88    SLOT.get_or_init(|| Arc::new(Semaphore::new(1)))
89}
90
91/// A regular slot, or for a high priority task the extra slot when every regular slot is busy
92pub fn acquire_dispatch_slot(
93    semaphore: &Arc<Semaphore>,
94    priority: Priority,
95) -> Option<OwnedSemaphorePermit> {
96    acquire_with_extra(semaphore, extra_slot(), priority)
97}
98
99fn acquire_with_extra(
100    semaphore: &Arc<Semaphore>,
101    extra: &Arc<Semaphore>,
102    priority: Priority,
103) -> Option<OwnedSemaphorePermit> {
104    if let Ok(permit) = Arc::clone(semaphore).try_acquire_owned() {
105        return Some(permit);
106    }
107
108    if priority == Priority::High {
109        return Arc::clone(extra).try_acquire_owned().ok();
110    }
111
112    None
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn labels(l: &[&str]) -> Vec<String> {
120        l.iter().map(|s| (*s).to_string()).collect()
121    }
122
123    #[test]
124    fn reads_priority_from_labels() {
125        assert_eq!(Priority::from_labels(&labels(&["bug"])), Priority::Normal);
126        assert_eq!(
127            Priority::from_labels(&labels(&["priority:low"])),
128            Priority::Low
129        );
130        assert_eq!(
131            Priority::from_labels(&labels(&["priority:low", "priority:high"])),
132            Priority::High
133        );
134    }
135
136    #[test]
137    fn high_task_filed_later_dispatches_before_normal_task() {
138        let mut tasks = vec![
139            ("normal-first", labels(&["bug"])),
140            ("low", labels(&["priority:low"])),
141            ("high-later", labels(&["priority:high"])),
142            ("normal-second", labels(&[])),
143        ];
144        sort_by_priority(&mut tasks, |t| &t.1);
145        let order: Vec<_> = tasks.iter().map(|t| t.0).collect();
146        assert_eq!(
147            order,
148            ["high-later", "normal-first", "normal-second", "low"]
149        );
150    }
151
152    #[test]
153    fn high_task_takes_the_extra_slot_and_a_second_one_waits() {
154        let semaphore = Arc::new(Semaphore::new(1));
155        let extra = Arc::new(Semaphore::new(1));
156        let _busy = acquire_with_extra(&semaphore, &extra, Priority::Normal).unwrap();
157
158        assert!(acquire_with_extra(&semaphore, &extra, Priority::Normal).is_none());
159        assert!(acquire_with_extra(&semaphore, &extra, Priority::Low).is_none());
160
161        let first_high = acquire_with_extra(&semaphore, &extra, Priority::High);
162        assert!(first_high.is_some());
163        assert!(acquire_with_extra(&semaphore, &extra, Priority::High).is_none());
164
165        drop(first_high);
166        assert!(acquire_with_extra(&semaphore, &extra, Priority::High).is_some());
167    }
168
169    #[test]
170    fn high_task_prefers_a_regular_slot() {
171        let semaphore = Arc::new(Semaphore::new(1));
172        let extra = Arc::new(Semaphore::new(1));
173        let _permit = acquire_with_extra(&semaphore, &extra, Priority::High).unwrap();
174        assert_eq!(extra.available_permits(), 1);
175    }
176
177    #[test]
178    fn apply_replaces_existing_priority_labels() {
179        let current = labels(&["bug", "priority:low"]);
180        assert_eq!(
181            Priority::High.apply_to(&current),
182            labels(&["bug", "priority:high"])
183        );
184        assert_eq!(Priority::Normal.apply_to(&current), labels(&["bug"]));
185    }
186
187    #[test]
188    fn parses_priority_names() {
189        assert_eq!("High".parse::<Priority>().unwrap(), Priority::High);
190        assert!("urgent".parse::<Priority>().is_err());
191    }
192}