use std::collections::{HashMap, VecDeque};
use std::time::Instant;
use tokio_util::sync::CancellationToken;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TaskId(pub u64);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TaskQueueId(pub String);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TaskKey {
pub queue: TaskQueueId,
pub target: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TaskStatus {
Pending,
Running,
Success,
Warning,
Error,
Cancelled,
}
#[derive(Clone, Debug)]
pub enum TaskTrigger {
Manual,
OnSave,
OnFileChange,
Extension(String),
}
pub struct Task {
pub id: TaskId,
pub key: TaskKey,
pub status: TaskStatus,
pub command: String,
pub created_at: Instant,
pub started_at: Option<Instant>,
pub finished_at: Option<Instant>,
pub cancellation_token: CancellationToken,
pub trigger: TaskTrigger,
}
pub struct TaskRegistry {
tasks: HashMap<TaskId, Task>,
running: HashMap<TaskQueueId, TaskId>,
queues: HashMap<TaskQueueId, VecDeque<TaskId>>,
next_id: u64,
recent_finished: VecDeque<TaskId>,
}
impl TaskRegistry {
pub fn new() -> Self {
Self {
tasks: HashMap::new(),
running: HashMap::new(),
queues: HashMap::new(),
next_id: 1,
recent_finished: VecDeque::with_capacity(5),
}
}
pub fn schedule_task(&mut self, key: TaskKey, trigger: TaskTrigger, command: String) -> TaskId {
self.cancel_by_key(&key);
let id = self.alloc_id();
let task = Task {
id,
key: key.clone(),
status: TaskStatus::Pending,
command,
created_at: Instant::now(),
started_at: None,
finished_at: None,
cancellation_token: CancellationToken::new(),
trigger,
};
self.tasks.insert(id, task);
let queue_id = &key.queue;
if self.running.contains_key(queue_id) {
self.queues.entry(queue_id.clone()).or_default().push_back(id);
} else {
self.mark_running(id);
}
id
}
pub fn cancel(&mut self, task_id: TaskId) -> Option<TaskId> {
let (status, queue_id) = match self.tasks.get(&task_id) {
Some(t) => (t.status.clone(), t.key.queue.clone()),
None => return None,
};
match status {
TaskStatus::Running => {
if let Some(t) = self.tasks.get_mut(&task_id) {
t.cancellation_token.cancel();
t.status = TaskStatus::Cancelled;
t.finished_at = Some(Instant::now());
}
self.running.remove(&queue_id);
self.start_next(&queue_id)
}
TaskStatus::Pending => {
if let Some(queue) = self.queues.get_mut(&queue_id) {
queue.retain(|&id| id != task_id);
}
if let Some(t) = self.tasks.get_mut(&task_id) {
t.status = TaskStatus::Cancelled;
}
None
}
_ => None,
}
}
pub fn cancel_by_key(&mut self, key: &TaskKey) -> Option<TaskId> {
let ids_to_cancel: Vec<TaskId> = self
.tasks
.values()
.filter(|t| {
&t.key == key
&& matches!(t.status, TaskStatus::Pending | TaskStatus::Running)
})
.map(|t| t.id)
.collect();
let mut started = None;
for id in ids_to_cancel {
if let Some(next) = self.cancel(id) {
started = Some(next);
}
}
started
}
pub fn mark_running(&mut self, task_id: TaskId) {
let queue_id = match self.tasks.get_mut(&task_id) {
Some(t) => {
debug_assert_eq!(
t.status,
TaskStatus::Pending,
"mark_running called on task {:?} with status {:?}",
task_id,
t.status
);
t.status = TaskStatus::Running;
t.started_at = Some(Instant::now());
t.key.queue.clone()
}
None => return,
};
self.running.insert(queue_id, task_id);
}
pub fn mark_finished(&mut self, task_id: TaskId, status: TaskStatus) -> Option<TaskId> {
let queue_id = match self.tasks.get_mut(&task_id) {
Some(t) => {
if t.status != TaskStatus::Cancelled {
t.status = status;
t.finished_at = Some(Instant::now());
}
t.key.queue.clone()
}
None => return None,
};
if self.recent_finished.len() >= 5 {
self.recent_finished.pop_front();
}
self.recent_finished.push_back(task_id);
if self.running.get(&queue_id) == Some(&task_id) {
self.running.remove(&queue_id);
self.start_next(&queue_id)
} else {
None
}
}
pub fn all_tasks(&self) -> impl Iterator<Item = &Task> {
self.tasks.values()
}
pub fn task_count(&self) -> usize {
self.tasks.len()
}
pub fn get(&self, id: TaskId) -> Option<&Task> {
self.tasks.get(&id)
}
pub fn running_task(&self, queue: &TaskQueueId) -> Option<TaskId> {
self.running.get(queue).copied()
}
pub fn running_tasks(&self) -> impl Iterator<Item = (&TaskQueueId, &TaskId)> {
self.running.iter()
}
pub fn recently_finished_tasks(&self) -> impl Iterator<Item = TaskId> + '_ {
self.recent_finished.iter().rev().copied()
}
pub fn pending_tasks(&self, queue: &TaskQueueId) -> &[TaskId] {
self.queues
.get(queue)
.map(|q| q.as_slices().0)
.unwrap_or(&[])
}
fn alloc_id(&mut self) -> TaskId {
let id = TaskId(self.next_id);
self.next_id += 1;
id
}
fn start_next(&mut self, queue: &TaskQueueId) -> Option<TaskId> {
let next_id = self.queues.get_mut(queue)?.pop_front()?;
self.mark_running(next_id);
Some(next_id)
}
}
impl Default for TaskRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(queue: &str, target: &str) -> TaskKey {
TaskKey {
queue: TaskQueueId(queue.into()),
target: target.into(),
}
}
fn sched(reg: &mut TaskRegistry, queue: &str, target: &str) -> TaskId {
reg.schedule_task(key(queue, target), TaskTrigger::Manual, format!("echo {target}"))
}
#[test]
fn same_key_cancels_previous() {
let mut reg = TaskRegistry::new();
let id1 = sched(&mut reg, "build", "crate:a");
let id2 = sched(&mut reg, "build", "crate:a");
assert_eq!(reg.get(id1).unwrap().status, TaskStatus::Cancelled);
assert!(reg.get(id1).unwrap().cancellation_token.is_cancelled());
assert_eq!(reg.get(id2).unwrap().status, TaskStatus::Running);
assert_eq!(reg.running_task(&TaskQueueId("build".into())), Some(id2));
}
#[test]
fn different_targets_are_queued() {
let mut reg = TaskRegistry::new();
let id_a = sched(&mut reg, "build", "crate:a");
let id_b = sched(&mut reg, "build", "crate:b");
assert_eq!(reg.get(id_a).unwrap().status, TaskStatus::Running);
assert_eq!(reg.get(id_b).unwrap().status, TaskStatus::Pending);
assert_eq!(reg.running_task(&TaskQueueId("build".into())), Some(id_a));
assert_eq!(reg.pending_tasks(&TaskQueueId("build".into())), &[id_b]);
}
#[test]
fn queue_compaction_deduplicates_key() {
let mut reg = TaskRegistry::new();
let id_a1 = sched(&mut reg, "build", "crate:a");
let id_b = sched(&mut reg, "build", "crate:b");
let id_a2 = sched(&mut reg, "build", "crate:a");
assert_eq!(reg.get(id_a1).unwrap().status, TaskStatus::Cancelled);
assert_eq!(reg.get(id_b).unwrap().status, TaskStatus::Running);
assert_eq!(reg.get(id_a2).unwrap().status, TaskStatus::Pending);
let pending = reg.pending_tasks(&TaskQueueId("build".into()));
assert_eq!(pending, &[id_a2]);
}
#[test]
fn cancel_queued_task_removes_from_queue() {
let mut reg = TaskRegistry::new();
let _id_a = sched(&mut reg, "build", "crate:a");
let id_b = sched(&mut reg, "build", "crate:b");
reg.cancel(id_b);
assert_eq!(reg.get(id_b).unwrap().status, TaskStatus::Cancelled);
assert!(reg.pending_tasks(&TaskQueueId("build".into())).is_empty());
}
#[test]
fn cancel_running_task_triggers_token() {
let mut reg = TaskRegistry::new();
let id = sched(&mut reg, "build", "crate:a");
let token = reg.get(id).unwrap().cancellation_token.clone();
reg.cancel(id);
assert_eq!(reg.get(id).unwrap().status, TaskStatus::Cancelled);
assert!(token.is_cancelled());
assert_eq!(reg.running_task(&TaskQueueId("build".into())), None);
}
#[test]
fn finish_starts_next_queued_task() {
let mut reg = TaskRegistry::new();
let id_a = sched(&mut reg, "build", "crate:a");
let id_b = sched(&mut reg, "build", "crate:b");
let next = reg.mark_finished(id_a, TaskStatus::Success);
assert_eq!(next, Some(id_b));
assert_eq!(reg.get(id_a).unwrap().status, TaskStatus::Success);
assert_eq!(reg.get(id_b).unwrap().status, TaskStatus::Running);
assert_eq!(reg.running_task(&TaskQueueId("build".into())), Some(id_b));
}
#[test]
fn independent_queues_do_not_interfere() {
let mut reg = TaskRegistry::new();
let build_id = sched(&mut reg, "build", "crate:a");
let lint_id = sched(&mut reg, "lint", "crate:a");
assert_eq!(reg.get(build_id).unwrap().status, TaskStatus::Running);
assert_eq!(reg.get(lint_id).unwrap().status, TaskStatus::Running);
reg.mark_finished(build_id, TaskStatus::Success);
assert_eq!(reg.get(lint_id).unwrap().status, TaskStatus::Running);
}
#[test]
fn cancelled_task_stays_cancelled_on_finish() {
let mut reg = TaskRegistry::new();
let id = sched(&mut reg, "build", "crate:a");
reg.cancel(id);
reg.mark_finished(id, TaskStatus::Success);
assert_eq!(reg.get(id).unwrap().status, TaskStatus::Cancelled);
}
#[test]
fn finish_on_empty_queue_is_noop() {
let mut reg = TaskRegistry::new();
let id = sched(&mut reg, "build", "crate:a");
let next = reg.mark_finished(id, TaskStatus::Success);
assert_eq!(next, None);
assert_eq!(reg.running_task(&TaskQueueId("build".into())), None);
assert!(reg.pending_tasks(&TaskQueueId("build".into())).is_empty());
}
#[test]
fn rapid_reschedule_no_duplicate_key_in_queue() {
let mut reg = TaskRegistry::new();
let _anchor = sched(&mut reg, "build", "anchor");
let id1 = sched(&mut reg, "build", "crate:a");
let id2 = sched(&mut reg, "build", "crate:a");
let id3 = sched(&mut reg, "build", "crate:a");
assert_eq!(reg.get(id1).unwrap().status, TaskStatus::Cancelled);
assert_eq!(reg.get(id2).unwrap().status, TaskStatus::Cancelled);
assert_eq!(reg.get(id3).unwrap().status, TaskStatus::Pending);
let pending = reg.pending_tasks(&TaskQueueId("build".into()));
assert_eq!(pending.len(), 1);
assert_eq!(pending[0], id3);
}
#[test]
fn cancel_running_returns_next_started() {
let mut reg = TaskRegistry::new();
let id_a = sched(&mut reg, "build", "crate:a");
let id_b = sched(&mut reg, "build", "crate:b");
let next = reg.cancel(id_a);
assert_eq!(next, Some(id_b));
assert_eq!(reg.get(id_b).unwrap().status, TaskStatus::Running);
}
}