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