🐛 fix: clean cancelled jobs immediately
Cancelled jobs are now cleaned without requiring `--all` flag or waiting for the 24-hour age threshold. Since the user explicitly cancelled these jobs, they should be cleaned immediately.
This commit is contained in:
+83
-8
@@ -1,6 +1,7 @@
|
|||||||
//! Job management commands (pause, resume, kill, clean).
|
//! Job management commands (pause, resume, kill, clean).
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use inquire::Select;
|
||||||
use paracas_daemon::{DaemonSpawner, DownloadJob, JobStatus, StateManager};
|
use paracas_daemon::{DaemonSpawner, DownloadJob, JobStatus, StateManager};
|
||||||
|
|
||||||
/// Pause a running job by sending SIGSTOP to its process.
|
/// Pause a running job by sending SIGSTOP to its process.
|
||||||
@@ -166,9 +167,11 @@ pub(crate) fn clean_jobs(state: &StateManager, all: bool) -> Result<()> {
|
|||||||
let should_clean = if all {
|
let should_clean = if all {
|
||||||
job.is_finished()
|
job.is_finished()
|
||||||
} else {
|
} else {
|
||||||
// By default, only clean jobs older than 24 hours that are finished
|
// Cancelled jobs are always cleaned (user explicitly cancelled them)
|
||||||
|
// Completed/Failed jobs are only cleaned if older than 24 hours
|
||||||
|
let is_cancelled = job.status == JobStatus::Cancelled;
|
||||||
let is_old = job.created_at < chrono::Utc::now() - chrono::Duration::hours(24);
|
let is_old = job.created_at < chrono::Utc::now() - chrono::Duration::hours(24);
|
||||||
is_old && job.is_finished()
|
is_cancelled || (is_old && job.is_finished())
|
||||||
};
|
};
|
||||||
|
|
||||||
if should_clean {
|
if should_clean {
|
||||||
@@ -186,6 +189,65 @@ pub(crate) fn clean_jobs(state: &StateManager, all: bool) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prompt user to select a job from available jobs filtered by the given statuses.
|
||||||
|
fn prompt_job_selection(
|
||||||
|
state: &StateManager,
|
||||||
|
action_name: &str,
|
||||||
|
filter_statuses: &[JobStatus],
|
||||||
|
) -> Result<String> {
|
||||||
|
let jobs = state.list_jobs()?;
|
||||||
|
|
||||||
|
let filtered: Vec<_> = jobs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|job| filter_statuses.contains(&job.status))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if filtered.is_empty() {
|
||||||
|
let status_names: Vec<_> = filter_statuses.iter().map(|s| format!("{s:?}")).collect();
|
||||||
|
anyhow::bail!(
|
||||||
|
"No jobs with status {} found to {}.",
|
||||||
|
status_names.join(" or "),
|
||||||
|
action_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let options: Vec<String> = filtered
|
||||||
|
.iter()
|
||||||
|
.map(|job| {
|
||||||
|
let instruments: Vec<_> = job.tasks.iter().map(|t| t.instrument_id.as_str()).collect();
|
||||||
|
let instruments_display = if instruments.len() > 3 {
|
||||||
|
format!(
|
||||||
|
"{}, ... (+{} more)",
|
||||||
|
instruments[..3].join(", "),
|
||||||
|
instruments.len() - 3
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
instruments.join(", ")
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"{} | {:?} | {:.1}% | {}",
|
||||||
|
job.id,
|
||||||
|
job.status,
|
||||||
|
job.progress_percent(),
|
||||||
|
instruments_display
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let selection = Select::new(&format!("Select a job to {action_name}:"), options)
|
||||||
|
.prompt()
|
||||||
|
.context("Job selection cancelled")?;
|
||||||
|
|
||||||
|
// Extract the job ID from the selection (first part before " | ")
|
||||||
|
let job_id = selection
|
||||||
|
.split(" | ")
|
||||||
|
.next()
|
||||||
|
.context("Failed to parse job selection")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(job_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute the job management command.
|
/// Execute the job management command.
|
||||||
pub(crate) fn job_command(action: &str, job_id: Option<&str>, all: bool) -> Result<()> {
|
pub(crate) fn job_command(action: &str, job_id: Option<&str>, all: bool) -> Result<()> {
|
||||||
let state_manager =
|
let state_manager =
|
||||||
@@ -193,16 +255,29 @@ pub(crate) fn job_command(action: &str, job_id: Option<&str>, all: bool) -> Resu
|
|||||||
|
|
||||||
match action {
|
match action {
|
||||||
"pause" => {
|
"pause" => {
|
||||||
let id = job_id.context("Job ID required for pause")?;
|
let id = match job_id {
|
||||||
pause_job(&state_manager, id)
|
Some(id) => id.to_string(),
|
||||||
|
None => prompt_job_selection(&state_manager, "pause", &[JobStatus::Running])?,
|
||||||
|
};
|
||||||
|
pause_job(&state_manager, &id)
|
||||||
}
|
}
|
||||||
"resume" => {
|
"resume" => {
|
||||||
let id = job_id.context("Job ID required for resume")?;
|
let id = match job_id {
|
||||||
resume_job(&state_manager, id)
|
Some(id) => id.to_string(),
|
||||||
|
None => prompt_job_selection(&state_manager, "resume", &[JobStatus::Paused])?,
|
||||||
|
};
|
||||||
|
resume_job(&state_manager, &id)
|
||||||
}
|
}
|
||||||
"kill" => {
|
"kill" => {
|
||||||
let id = job_id.context("Job ID required for kill")?;
|
let id = match job_id {
|
||||||
kill_job(&state_manager, id)
|
Some(id) => id.to_string(),
|
||||||
|
None => prompt_job_selection(
|
||||||
|
&state_manager,
|
||||||
|
"kill",
|
||||||
|
&[JobStatus::Running, JobStatus::Pending, JobStatus::Paused],
|
||||||
|
)?,
|
||||||
|
};
|
||||||
|
kill_job(&state_manager, &id)
|
||||||
}
|
}
|
||||||
"clean" => clean_jobs(&state_manager, all),
|
"clean" => clean_jobs(&state_manager, all),
|
||||||
_ => anyhow::bail!("Unknown action: {}", action),
|
_ => anyhow::bail!("Unknown action: {}", action),
|
||||||
|
|||||||
Reference in New Issue
Block a user