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}
18
19impl Priority {
20 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 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
80pub 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
85fn 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
91pub 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(¤t),
182 labels(&["bug", "priority:high"])
183 );
184 assert_eq!(Priority::Normal.apply_to(¤t), 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}