Skip to content
Merged
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
5 changes: 1 addition & 4 deletions fairy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,7 @@ fn log_sink(cfg: Arc<AppConfig>, task_id: Uuid) -> impl Sink<Vec<String>, Error
}

async fn try_run_task(cfg: Arc<AppConfig>, task: &Task) -> Result<()> {
let mut args = vec![
"run".into(),
"--refresh".into()
];
let mut args = vec!["run".into(), "--refresh".into()];

if !&cfg.verbose_nix_logs {
args.push("--quiet".into());
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';

6 changes: 3 additions & 3 deletions vicky/src/bin/vicky/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ use crate::locks::{
};
use crate::startup::Result;
use crate::tasks::{
tasks_add, tasks_claim, tasks_confirm, tasks_count, tasks_download_logs, tasks_finish,
tasks_get, tasks_get_logs, tasks_get_specific, tasks_heartbeat, tasks_put_logs, tasks_cancel
tasks_add, tasks_cancel, tasks_claim, tasks_confirm, tasks_count, tasks_download_logs,
tasks_finish, tasks_get, tasks_get_logs, tasks_get_specific, tasks_heartbeat, tasks_put_logs,
};
use crate::user::get_user;
use crate::webconfig::get_web_config;
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use errors::AppError;
use jwtk::jwk::RemoteJwksVerifier;
use log::{LevelFilter, error, trace, warn, info};
use log::{LevelFilter, error, info, trace, warn};
use rocket::fairing::AdHoc;
use rocket::{Build, Ignite, Rocket, routes};
use snafu::ResultExt;
Expand Down
31 changes: 26 additions & 5 deletions vicky/src/lib/database/entities/lock.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
//! The (presumably) intended locking/scheduling behavior:
//!
//! - Task that needs confirmation: locks can't be acquired.
//! - (Task A with Write Lock A and Read Lock B needs confirmation, then Task A2 in Validation with Read Lock A can't be scheduled, Task B with Read Lock B can be scheduled)
//! - (Task A with Read Lock A, then Task A2 in Validation with Read Lock A can be scheduled)
//! - Task in validation: locks can be freely acquired as long as they're not locked by a running task, or they are poisoned locks (by same name).
//! - Task is running: used locks are blocked from acquiring.
//! - Task failed: locks are poisoned until manually cleared.
//! - Task succeeded: locks are free again.
//! - Tasks are scheduled in queue order where FI is also FO

use diesel::{AsExpression, FromSqlRow};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
Expand Down Expand Up @@ -26,12 +37,17 @@ use crate::database::entities::task::db_impl::DbTask;
pub enum LockKind {
Read,
Write,
Clean,
}

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

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

impl TryFrom<&str> for LockKind {
Expand All @@ -41,6 +57,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 +81,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 All @@ -76,19 +97,19 @@ impl Lock {
self.kind.is_write() || other.kind.is_write()
}

pub fn poison(&mut self, by_task: &Uuid) {
pub const fn poison(&mut self, by_task: &Uuid) {
self.poisoned_by = Some(*by_task);
}

pub fn clear_poison(&mut self) {
pub const fn clear_poison(&mut self) {
self.poisoned_by = None;
}

pub fn name(&self) -> &str {
pub const fn name(&self) -> &str {
self.name.as_str()
}

pub fn is_poisoned(&self) -> bool {
pub const fn is_poisoned(&self) -> bool {
self.poisoned_by.is_some()
}

Expand Down
32 changes: 31 additions & 1 deletion vicky/src/lib/database/entities/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ impl Task {
pub fn clear_poisoned_locks(&mut self) {
self.locks.iter_mut().for_each(|lock| lock.clear_poison());
}

pub fn is_running(&self) -> bool {
self.status == TaskStatus::Running
}

pub fn is_waiting_confirmation(&self) -> bool {
self.status.is_waiting_confirmation()
}

pub fn is_new(&self) -> bool {
self.status == TaskStatus::New
}
}

impl AsRef<Task> for Task {
Expand All @@ -125,6 +137,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 @@ -210,7 +227,8 @@ impl From<(DbTask, Vec<DbLock>)> for Task {
}

impl TaskStatus {
pub fn is_failed(&self) -> bool {
pub fn is_failed(self) -> bool {
// explicitly state all variants so that rust makes us add new ones
match self {
TaskStatus::NeedsUserValidation
| TaskStatus::New
Expand All @@ -221,6 +239,18 @@ impl TaskStatus {
}
}
}

pub fn is_finished(self) -> bool {
matches!(self, TaskStatus::Finished(_))
}

pub fn is_pending(self) -> bool {
!self.is_finished()
}

pub fn is_waiting_confirmation(self) -> bool {
matches!(self, TaskStatus::NeedsUserValidation)
}
}

// this was on purpose because these macro-generated entity types
Expand Down
Loading
Loading