use std::str::FromStr;
use std::sync::{Arc, OnceLock};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
const HIGH_LABEL: &str = "priority:high";
const LOW_LABEL: &str = "priority:low";
const PREFIX: &str = "priority:";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Priority {
Low,
#[default]
Normal,
High,
OpenPr,
}
impl Priority {
pub fn from_labels(labels: &[String]) -> Self {
let mut found = Self::Normal;
for label in labels {
match label.as_str() {
HIGH_LABEL => return Self::High,
LOW_LABEL => found = Self::Low,
_ => {}
}
}
found
}
pub fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Normal => "normal",
Self::High => "high",
Self::OpenPr => "open-pr",
}
}
#[must_use]
pub fn with_pr(self, has_pr: bool) -> Self {
if has_pr { Self::OpenPr } else { self }
}
fn label(self) -> Option<&'static str> {
match self {
Self::Low => Some(LOW_LABEL),
Self::Normal | Self::OpenPr => None,
Self::High => Some(HIGH_LABEL),
}
}
pub fn apply_to(self, labels: &[String]) -> Vec<String> {
let mut out: Vec<String> = labels
.iter()
.filter(|l| !is_priority_label(l))
.cloned()
.collect();
if let Some(label) = self.label() {
out.push(label.to_string());
}
out
}
}
impl FromStr for Priority {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"high" => Ok(Self::High),
"normal" => Ok(Self::Normal),
"low" => Ok(Self::Low),
other => anyhow::bail!("unknown priority '{other}', use high, normal or low"),
}
}
}
pub fn is_priority_label(label: &str) -> bool {
label.starts_with(PREFIX)
}
pub fn sort_by_priority<T>(tasks: &mut [T], priority_of: impl Fn(&T) -> Priority) {
tasks.sort_by_key(|t| std::cmp::Reverse(priority_of(t)));
}
fn extra_slot() -> &'static Arc<Semaphore> {
static SLOT: OnceLock<Arc<Semaphore>> = OnceLock::new();
SLOT.get_or_init(|| Arc::new(Semaphore::new(1)))
}
pub fn acquire_dispatch_slot(
semaphore: &Arc<Semaphore>,
priority: Priority,
) -> Option<OwnedSemaphorePermit> {
acquire_with_extra(semaphore, extra_slot(), priority)
}
fn acquire_with_extra(
semaphore: &Arc<Semaphore>,
extra: &Arc<Semaphore>,
priority: Priority,
) -> Option<OwnedSemaphorePermit> {
if let Ok(permit) = Arc::clone(semaphore).try_acquire_owned() {
return Some(permit);
}
if priority >= Priority::High {
return Arc::clone(extra).try_acquire_owned().ok();
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn labels(l: &[&str]) -> Vec<String> {
l.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn reads_priority_from_labels() {
assert_eq!(Priority::from_labels(&labels(&["bug"])), Priority::Normal);
assert_eq!(
Priority::from_labels(&labels(&["priority:low"])),
Priority::Low
);
assert_eq!(
Priority::from_labels(&labels(&["priority:low", "priority:high"])),
Priority::High
);
}
#[test]
fn high_task_filed_later_dispatches_before_normal_task() {
let mut tasks = vec![
("normal-first", labels(&["bug"])),
("low", labels(&["priority:low"])),
("high-later", labels(&["priority:high"])),
("normal-second", labels(&[])),
];
sort_by_priority(&mut tasks, |t| Priority::from_labels(&t.1));
let order: Vec<_> = tasks.iter().map(|t| t.0).collect();
assert_eq!(
order,
["high-later", "normal-first", "normal-second", "low"]
);
}
#[test]
fn open_pr_task_dispatches_before_high_task() {
let mut tasks = vec![
("high", Priority::High.with_pr(false)),
("normal-with-pr", Priority::Normal.with_pr(true)),
("normal", Priority::Normal.with_pr(false)),
("low-with-pr", Priority::Low.with_pr(true)),
];
sort_by_priority(&mut tasks, |t| t.1);
let order: Vec<_> = tasks.iter().map(|t| t.0).collect();
assert_eq!(order, ["normal-with-pr", "low-with-pr", "high", "normal"]);
}
#[test]
fn open_pr_task_may_use_the_extra_slot() {
let semaphore = Arc::new(Semaphore::new(1));
let extra = Arc::new(Semaphore::new(1));
let _busy = acquire_with_extra(&semaphore, &extra, Priority::Normal).unwrap();
assert!(acquire_with_extra(&semaphore, &extra, Priority::OpenPr).is_some());
}
#[test]
fn high_task_takes_the_extra_slot_and_a_second_one_waits() {
let semaphore = Arc::new(Semaphore::new(1));
let extra = Arc::new(Semaphore::new(1));
let _busy = acquire_with_extra(&semaphore, &extra, Priority::Normal).unwrap();
assert!(acquire_with_extra(&semaphore, &extra, Priority::Normal).is_none());
assert!(acquire_with_extra(&semaphore, &extra, Priority::Low).is_none());
let first_high = acquire_with_extra(&semaphore, &extra, Priority::High);
assert!(first_high.is_some());
assert!(acquire_with_extra(&semaphore, &extra, Priority::High).is_none());
drop(first_high);
assert!(acquire_with_extra(&semaphore, &extra, Priority::High).is_some());
}
#[test]
fn high_task_prefers_a_regular_slot() {
let semaphore = Arc::new(Semaphore::new(1));
let extra = Arc::new(Semaphore::new(1));
let _permit = acquire_with_extra(&semaphore, &extra, Priority::High).unwrap();
assert_eq!(extra.available_permits(), 1);
}
#[test]
fn apply_replaces_existing_priority_labels() {
let current = labels(&["bug", "priority:low"]);
assert_eq!(
Priority::High.apply_to(¤t),
labels(&["bug", "priority:high"])
);
assert_eq!(Priority::Normal.apply_to(¤t), labels(&["bug"]));
}
#[test]
fn parses_priority_names() {
assert_eq!("High".parse::<Priority>().unwrap(), Priority::High);
assert!("urgent".parse::<Priority>().is_err());
}
}