Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions dashboard/src/components/task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ const Task = (props: TaskProps) => {
}
}

const BADGE_LOCK_COLOR = {
"WRITE": "red",
"READ": "green",
"CLEAN": "red",
}

const BADGE_LOCK_CONTENT = {
"WRITE": "W",
"READ": "R",
"CLEAN": "C",
}

return (
<Panel shaded borderLeft={"1px solid var(--rs-gray-700)"} className={s.Panel}>
<HStack alignItems={"flex-start"} justifyContent="space-between" className={s.TaskHeader}>
Expand All @@ -75,8 +87,8 @@ const Task = (props: TaskProps) => {
return (
<Badge
key={`${task.id}-${lock.name}`}
color={lock.type === "WRITE" ? "red" : "green"}
content={lock.type === "WRITE" ? "W" : "R"}
color={BADGE_LOCK_COLOR[lock.type]}
content={BADGE_LOCK_CONTENT[lock.type]}
offset={[0, 0]}
>
<Tag size="md" className={s.LockTag} color={lock.poisoned ? "red" : null}>{lock.name}</Tag>
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/services/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type ITask = {
id: string,
display_name: string,
locks: {
type: "WRITE" | "READ"
type: "WRITE" | "READ" | "CLEAN"
name: string,
poisoned: string,
}[]
Expand Down
17 changes: 17 additions & 0 deletions vicky/migrations/2026-02-16-100033-0000_lock_clean/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- This file should undo anything in `up.sql`

-- can't drop enum values from an enum.
CREATE TYPE "LockKind_Type_New" AS ENUM (
'READ',
'WRITE',
);

DELETE FROM locks WHERE type = 'CLEAN';

ALTER TABLE tasks
ALTER COLUMN status TYPE "LockKind_Type_New"
USING (status::text::"LockKind_Type_New");

DROP TYPE "LockKind_Type";

ALTER TYPE "LockKind_Type_New" RENAME TO "LockKind_Type";
4 changes: 4 additions & 0 deletions vicky/migrations/2026-02-16-100033-0000_lock_clean/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Your SQL goes here

ALTER TYPE "LockKind_Type" ADD VALUE 'CLEAN';

9 changes: 9 additions & 0 deletions vicky/src/lib/database/entities/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ use crate::database::entities::task::db_impl::DbTask;
pub enum LockKind {
Read,
Write,
Clean
}

impl LockKind {
pub fn is_write(&self) -> bool {
matches!(self, LockKind::Write)
}
pub fn is_cleanup(&self) -> bool {
matches!(self, LockKind::Clean)
}
}

impl TryFrom<&str> for LockKind {
Expand All @@ -41,6 +45,7 @@ impl TryFrom<&str> for LockKind {
match value {
"READ" => Ok(Self::Read),
"WRITE" => Ok(Self::Write),
"CLEAN" => Ok(Self::Clean),
_ => Err("Unexpected lock type received."),
}
}
Expand All @@ -64,6 +69,10 @@ impl Lock {
Self::new(name, LockKind::Write)
}

pub fn clean<S: Into<String>>(name: S) -> Self {
Self::new(name, LockKind::Clean)
}

pub fn is_conflicting(&self, other: &Lock) -> bool {
if self.name() != other.name() {
return false;
Expand Down
16 changes: 16 additions & 0 deletions vicky/src/lib/database/entities/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ impl<T: task_builder::State> TaskBuilder<T> {
self
}

pub fn clean_lock<S: Into<String>>(mut self, name: S) -> Self {
self.locks.push(Lock::clean(name));
self
}

pub fn locks(mut self, locks: Vec<Lock>) -> Self {
self.locks = locks;
self
Expand Down Expand Up @@ -221,6 +226,17 @@ impl TaskStatus {
}
}
}

pub fn is_finished(&self) -> bool {
match self {
TaskStatus::NeedsUserValidation
| TaskStatus::New
| TaskStatus::Running => false,
TaskStatus::Finished(_) => {
true
}
}
}
}

// this was on purpose because these macro-generated entity types
Expand Down
75 changes: 70 additions & 5 deletions vicky/src/lib/vicky/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ impl<'a> ConstraintMgmt<'a> for Constraints<'a> {
if !self.contains_key(lock.name()) {
return true; // lock wasn't used yet
}
let Some(lock) = self.get(lock.name()) else {
return false; // block execution if missing lock entry
if let Some(existing_lock) = self.get(lock.name()) {
return !lock.is_conflicting(existing_lock)
};

!lock.is_conflicting(lock)
false
}

fn from_tasks(tasks: &'a [Task]) -> Result<Self::Type, SchedulerError> {
Expand Down Expand Up @@ -81,6 +81,13 @@ impl<'a> Scheduler<'a> {
Ok(s)
}

fn is_cleanup_and_conflicts(&self, lock: &Lock) -> bool {
// If there is any other lock, we conflict with them and we do not want to be added to the queue.
// We cannot use constraints here, because constraints only contains locks with the status running.
let other_task_with_same_lock_exists = self.tasks.iter().any(|task| !task.status.is_finished() && task.locks.iter().any(|lock| !lock.kind.is_cleanup() && lock.name() == lock.name()));
lock.kind.is_cleanup() && other_task_with_same_lock_exists
}

fn is_poisoned(&self, lock: &Lock) -> bool {
self.poisoned_locks
.iter()
Expand All @@ -90,7 +97,7 @@ impl<'a> Scheduler<'a> {
fn is_unconstrained(&self, task: &Task) -> bool {
task.locks
.iter()
.all(|lock| self.constraints.can_get_lock(lock) && !self.is_poisoned(lock))
.all(|lock| self.constraints.can_get_lock(lock) && !self.is_poisoned(lock) && !self.is_cleanup_and_conflicts(lock))
}

fn supports_all_features(&self, task: &Task) -> bool {
Expand All @@ -117,7 +124,7 @@ impl<'a> Scheduler<'a> {
mod tests {
use uuid::Uuid;

use crate::database::entities::task::TaskStatus;
use crate::database::entities::task::{TaskResult, TaskStatus};
use crate::database::entities::{Lock, Task};

use super::Scheduler;
Expand Down Expand Up @@ -314,6 +321,64 @@ mod tests {
assert_eq!(res.get_next_task(), None)
}

#[test]
fn scheduler_new_task_cleanup_single() {
let tasks = vec![
Task::builder()
.display_name("Test 1")
.status(TaskStatus::New)
.clean_lock("foo1")
.build_expect(),
];

let res = Scheduler::new(&tasks, &[], &[]).unwrap();
// Test 1 is currently running and has the write lock
assert_eq!(res.get_next_task().unwrap().display_name, "Test 1")
}

#[test]
fn scheduler_new_task_cleanup_with_finished() {
let tasks = vec![
Task::builder()
.display_name("Test 5")
.status(TaskStatus::Finished(TaskResult::Success))
.write_lock("foo1")
.build_expect(),
Task::builder()
.display_name("Test 1")
.status(TaskStatus::New)
.clean_lock("foo1")
.build_expect(),
];

let res = Scheduler::new(&tasks, &[], &[]).unwrap();
// Test 1 is currently running and has the write lock
assert_eq!(res.get_next_task().unwrap().display_name, "Test 1")
}


#[test]
fn scheduler_new_task_cleanup() {
let tasks = vec![
Task::builder()
.display_name("Test 1")
.status(TaskStatus::New)
.clean_lock("foo1")
.build_expect(),
Task::builder()
.display_name("Test 2")
.status(TaskStatus::New)
.read_lock("foo1")
.build_expect(),
];

let res = Scheduler::new(&tasks, &[], &[]).unwrap();
// Test 1 is currently running and has the write lock
assert_eq!(res.get_next_task().unwrap().display_name, "Test 2")
}



#[test]
fn schedule_with_poisoned_lock() {
let tasks = vec![
Expand Down
Loading