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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
12pub enum Priority {
13 Low,
14 #[default]
15 Normal,
16 High,
17 OpenPr,
19}
20
21impl Priority {
22 pub fn from_labels(labels: &[String]) -> Self {
24 let mut found = Self::Normal;
25 for label in labels {
26 match label.as_str() {
27 HIGH_LABEL => return Self::High,
28 LOW_LABEL => found = Self::Low,
29 _ => {}
30 }
31 }
32 found
33 }
34
35 pub fn as_str(self) -> &'static str {
36 match self {
37 Self::Low => "low",
38 Self::Normal => "normal",
39 Self::High => "high",
40 Self::OpenPr => "open-pr",
41 }
42 }
43
44 #[must_use]
46 pub fn with_pr(self, has_pr: bool) -> Self {
47 if has_pr { Self::OpenPr } else { self }
48 }
49
50 fn label(self) -> Option<&'static str> {
51 match self {
52 Self::Low => Some(LOW_LABEL),
53 Self::Normal | Self::OpenPr => None,
54 Self::High => Some(HIGH_LABEL),
55 }
56 }
57
58 pub fn apply_to(self, labels: &[String]) -> Vec<String> {
60 let mut out: Vec<String> = labels
61 .iter()
62 .filter(|l| !is_priority_label(l))
63 .cloned()
64 .collect();
65 if let Some(label) = self.label() {
66 out.push(label.to_string());
67 }
68 out
69 }
70}
71
72impl FromStr for Priority {
73 type Err = anyhow::Error;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 match s.trim().to_ascii_lowercase().as_str() {
77 "high" => Ok(Self::High),
78 "normal" => Ok(Self::Normal),
79 "low" => Ok(Self::Low),
80 other => anyhow::bail!("unknown priority '{other}', use high, normal or low"),
81 }
82 }
83}
84
85pub fn is_priority_label(label: &str) -> bool {
86 label.starts_with(PREFIX)
87}
88
89pub fn sort_by_priority<T>(tasks: &mut [T], priority_of: impl Fn(&T) -> Priority) {
91 tasks.sort_by_key(|t| std::cmp::Reverse(priority_of(t)));
92}
93
94fn extra_slot() -> &'static Arc<Semaphore> {
96 static SLOT: OnceLock<Arc<Semaphore>> = OnceLock::new();
97 SLOT.get_or_init(|| Arc::new(Semaphore::new(1)))
98}
99
100pub fn acquire_dispatch_slot(
102 semaphore: &Arc<Semaphore>,
103 priority: Priority,
104) -> Option<OwnedSemaphorePermit> {
105 acquire_with_extra(semaphore, extra_slot(), priority)
106}
107
108fn acquire_with_extra(
109 semaphore: &Arc<Semaphore>,
110 extra: &Arc<Semaphore>,
111 priority: Priority,
112) -> Option<OwnedSemaphorePermit> {
113 if let Ok(permit) = Arc::clone(semaphore).try_acquire_owned() {
114 return Some(permit);
115 }
116
117 if priority >= Priority::High {
118 return Arc::clone(extra).try_acquire_owned().ok();
119 }
120
121 None
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 fn labels(l: &[&str]) -> Vec<String> {
129 l.iter().map(|s| (*s).to_string()).collect()
130 }
131
132 #[test]
133 fn reads_priority_from_labels() {
134 assert_eq!(Priority::from_labels(&labels(&["bug"])), Priority::Normal);
135 assert_eq!(
136 Priority::from_labels(&labels(&["priority:low"])),
137 Priority::Low
138 );
139 assert_eq!(
140 Priority::from_labels(&labels(&["priority:low", "priority:high"])),
141 Priority::High
142 );
143 }
144
145 #[test]
146 fn high_task_filed_later_dispatches_before_normal_task() {
147 let mut tasks = vec![
148 ("normal-first", labels(&["bug"])),
149 ("low", labels(&["priority:low"])),
150 ("high-later", labels(&["priority:high"])),
151 ("normal-second", labels(&[])),
152 ];
153 sort_by_priority(&mut tasks, |t| Priority::from_labels(&t.1));
154 let order: Vec<_> = tasks.iter().map(|t| t.0).collect();
155 assert_eq!(
156 order,
157 ["high-later", "normal-first", "normal-second", "low"]
158 );
159 }
160
161 #[test]
162 fn open_pr_task_dispatches_before_high_task() {
163 let mut tasks = vec![
164 ("high", Priority::High.with_pr(false)),
165 ("normal-with-pr", Priority::Normal.with_pr(true)),
166 ("normal", Priority::Normal.with_pr(false)),
167 ("low-with-pr", Priority::Low.with_pr(true)),
168 ];
169 sort_by_priority(&mut tasks, |t| t.1);
170 let order: Vec<_> = tasks.iter().map(|t| t.0).collect();
171 assert_eq!(order, ["normal-with-pr", "low-with-pr", "high", "normal"]);
172 }
173
174 #[test]
175 fn open_pr_task_may_use_the_extra_slot() {
176 let semaphore = Arc::new(Semaphore::new(1));
177 let extra = Arc::new(Semaphore::new(1));
178 let _busy = acquire_with_extra(&semaphore, &extra, Priority::Normal).unwrap();
179 assert!(acquire_with_extra(&semaphore, &extra, Priority::OpenPr).is_some());
180 }
181
182 #[test]
183 fn high_task_takes_the_extra_slot_and_a_second_one_waits() {
184 let semaphore = Arc::new(Semaphore::new(1));
185 let extra = Arc::new(Semaphore::new(1));
186 let _busy = acquire_with_extra(&semaphore, &extra, Priority::Normal).unwrap();
187
188 assert!(acquire_with_extra(&semaphore, &extra, Priority::Normal).is_none());
189 assert!(acquire_with_extra(&semaphore, &extra, Priority::Low).is_none());
190
191 let first_high = acquire_with_extra(&semaphore, &extra, Priority::High);
192 assert!(first_high.is_some());
193 assert!(acquire_with_extra(&semaphore, &extra, Priority::High).is_none());
194
195 drop(first_high);
196 assert!(acquire_with_extra(&semaphore, &extra, Priority::High).is_some());
197 }
198
199 #[test]
200 fn high_task_prefers_a_regular_slot() {
201 let semaphore = Arc::new(Semaphore::new(1));
202 let extra = Arc::new(Semaphore::new(1));
203 let _permit = acquire_with_extra(&semaphore, &extra, Priority::High).unwrap();
204 assert_eq!(extra.available_permits(), 1);
205 }
206
207 #[test]
208 fn apply_replaces_existing_priority_labels() {
209 let current = labels(&["bug", "priority:low"]);
210 assert_eq!(
211 Priority::High.apply_to(¤t),
212 labels(&["bug", "priority:high"])
213 );
214 assert_eq!(Priority::Normal.apply_to(¤t), labels(&["bug"]));
215 }
216
217 #[test]
218 fn parses_priority_names() {
219 assert_eq!("High".parse::<Priority>().unwrap(), Priority::High);
220 assert!("urgent".parse::<Priority>().is_err());
221 }
222}