From b2ae8f7322297d98104790b060d94529edce9a5a Mon Sep 17 00:00:00 2001 From: "Joshua A. Anderson" Date: Thu, 5 Dec 2024 09:14:24 -0500 Subject: [PATCH 1/6] Implement `show jobs`. --- src/cli.rs | 7 +++ src/cli/jobs.rs | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 8 +++ src/project.rs | 5 ++ 4 files changed, 167 insertions(+) create mode 100644 src/cli/jobs.rs diff --git a/src/cli.rs b/src/cli.rs index c2ff3a0..e2f7579 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,7 @@ pub mod clean; pub mod cluster; pub mod directories; pub mod init; +pub mod jobs; pub mod launchers; pub mod scan; pub mod status; @@ -190,6 +191,12 @@ pub enum ShowCommands { /// /// row show launchers --all --short Launchers(launchers::Arguments), + + /** Show submitted jobs. + + TODO. + */ + Jobs(jobs::Arguments), } #[derive(Subcommand, Debug)] diff --git a/src/cli/jobs.rs b/src/cli/jobs.rs new file mode 100644 index 0000000..9997177 --- /dev/null +++ b/src/cli/jobs.rs @@ -0,0 +1,147 @@ +// Copyright (c) 2024 The Regents of the University of Michigan. +// Part of row, released under the BSD 3-Clause License. + +use clap::Args; +use clap_complete::ArgValueCandidates; +use console::Style; +use log::{debug, trace}; +use std::collections::{BTreeMap, HashSet}; +use std::error::Error; +use std::io::Write; +use std::path::PathBuf; +use wildmatch::WildMatch; + +use crate::cli::{self, autocomplete, GlobalOptions}; +use crate::ui::{Alignment, Item, Row, Table}; +use row::project::Project; +use row::MultiProgressContainer; + +#[derive(Args, Debug)] +#[allow(clippy::struct_excessive_bools)] +pub struct Arguments { + /// Show jobs running on these directories (defaults to all). Use 'show jobs -' to read from stdin. + #[arg(add=ArgValueCandidates::new(autocomplete::get_directory_candidates))] + directories: Vec, + + /// Show jobs running actions that match a wildcard pattern. + #[arg(short, long, value_name = "pattern", default_value_t=String::from("*"), display_order=0, + add=ArgValueCandidates::new(autocomplete::get_action_candidates))] + action: String, + + /// Hide the table header. + #[arg(long, display_order = 0)] + no_header: bool, + + /// Show only job IDs. + #[arg(long, default_value_t = false, display_order = 0)] + short: bool, +} + +struct JobDetails { + action: String, + n: u64, +} + +/** Find jobs that match the given directories and the action wildcard on the selected cluster. +*/ +fn find( + directories: Vec, + action: &str, + project: &Project, +) -> Result, Box> { + debug!("Finding matching jobs."); + let mut result: BTreeMap = BTreeMap::new(); + + let action_matcher = WildMatch::new(action); + + let query_directories: HashSet = + HashSet::from_iter(cli::parse_directories(directories, || { + Ok(project.state().list_directories()) + })?); + + for (action_name, jobs_by_directory) in project.state().submitted() { + if !action_matcher.matches(action_name) { + trace!( + "Skipping action '{}'. It does not match the pattern '{}'.", + action_name, + action + ); + continue; + } + + for (directory_name, (cluster_name, job_id)) in jobs_by_directory { + if cluster_name != project.cluster_name() { + trace!( + "Skipping cluster '{cluster_name}'. It does not match selected cluster '{}'.", + project.cluster_name() + ); + continue; + } + + if query_directories.contains(directory_name) { + result + .entry(*job_id) + .and_modify(|e| e.n += 1) + .or_insert(JobDetails { + action: action_name.clone(), + n: 1, + }); + } + } + } + + Ok(result) +} + +/** Show jobs running on given directories where the action also matches a wildcard. + +Print a human-readable list of job IDs, the action they are running, and the number of +directories that the job acts on. +*/ +pub fn show( + options: &GlobalOptions, + args: Arguments, + multi_progress: &mut MultiProgressContainer, + output: &mut W, +) -> Result<(), Box> { + debug!("Showing jobs."); + + let mut project = Project::open(options.io_threads, &options.cluster, multi_progress)?; + + let jobs = find(args.directories, &args.action, &project)?; + + let mut table = Table::new().with_hide_header(if args.short { true } else { args.no_header }); + table.header = vec![ + Item::new("ID".to_string(), Style::new().underlined()), + Item::new("Action".to_string(), Style::new().underlined()), + Item::new("Directories".to_string(), Style::new().underlined()), + ]; + + for (job_id, job_details) in jobs { + let mut row = Vec::new(); + + row.push( + Item::new(job_id.to_string(), Style::new().bold()).with_alignment(Alignment::Right), + ); + + // Only show job IDs when user requests short output. + if args.short { + table.rows.push(Row::Items(row)); + continue; + } + + row.push(Item::new(job_details.action, Style::new())); + row.push( + Item::new(job_details.n.to_string(), Style::new()).with_alignment(Alignment::Right), + ); + + table.rows.push(Row::Items(row)); + } + + table.write(output)?; + output.flush()?; + + project.close(multi_progress)?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index fe59ab6..57268d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,6 +99,14 @@ fn main_detail() -> Result<(), Box> { ShowCommands::Launchers(args) => { cli::launchers::launchers(&options.global, &args, &mut output)?; } + ShowCommands::Jobs(args) => { + cli::jobs::show( + &options.global, + args, + &mut multi_progress_container, + &mut output, + )?; + } }, Some(Commands::Scan(args)) => { cli::scan::scan(&options.global, args, &mut multi_progress_container)?; diff --git a/src/project.rs b/src/project.rs index a3dcc41..911aa07 100644 --- a/src/project.rs +++ b/src/project.rs @@ -153,6 +153,11 @@ impl Project { &self.state } + /// Get the currently active cluster name. + pub fn cluster_name(&self) -> &String { + &self.cluster_name + } + /// Find the directories that are included by the action. /// /// # Parameters: From 3af2cd04d702840a199a7eceecf32046e8550f47 Mon Sep 17 00:00:00 2001 From: "Joshua A. Anderson" Date: Thu, 5 Dec 2024 11:40:10 -0500 Subject: [PATCH 2/6] Document implementation of show jobs. --- DESIGN.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 139072a..35638ad 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -48,10 +48,7 @@ Row is yet another workflow engine that automates the process of executing **act * **Waiting** on previous actions. * List directories and show completion status, submitted job ID, and user-defined keys from the value. - -Ideas: -* List scheduler jobs and show useful information. -* Cancel scheduler jobs specific to actions and/or directories. +* List submitted jobs. ## Overview From a66bba690cb3833013cb57b686834871ee7e7dd0 Mon Sep 17 00:00:00 2001 From: "Joshua A. Anderson" Date: Thu, 5 Dec 2024 12:16:17 -0500 Subject: [PATCH 3/6] Document show jobs. --- doc/src/SUMMARY.md | 1 + doc/src/row/show/jobs.md | 59 ++++++++++++++++++++++++++++++++++++++++ doc/src/row/submit.md | 2 +- src/cli.rs | 25 ++++++++++++++++- 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 doc/src/row/show/jobs.md diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 9ae989f..a6c6535 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -36,6 +36,7 @@ - [show](row/show/index.md) - [show status](row/show/status.md) - [show directories](row/show/directories.md) + - [show jobs](row/show/jobs.md) - [show cluster](row/show/cluster.md) - [show launchers](row/show/launchers.md) - [scan](row/scan.md) diff --git a/doc/src/row/show/jobs.md b/doc/src/row/show/jobs.md new file mode 100644 index 0000000..479faa8 --- /dev/null +++ b/doc/src/row/show/jobs.md @@ -0,0 +1,59 @@ +# show jobs + +Usage: +```bash +row show jobs [OPTIONS] [DIRECTORIES] +``` + +`row show jobs` lists submitted jobs that execute a matching action on any of the +provided directories. + +## `[DIRECTORIES]` + +List jobs that execute an action on one or more of the given directories. By default, +**row** shows jobs executing an action on any directory. + +Pass a single `-` to read the directories from stdin (separated by newlines): +```bash +echo "dir1" | row show jobs [OPTIONS] - +``` + +## `[OPTIONS]` + +### `--action` + +(also: `-a`) + +Set `--action ` to show only jobs that mach the given pattern by name. +By default, **row** shows jobs executing any action. `` is a wildcard pattern. + +### `--no-header` + +Hide the header in the output. + +### `--short` + +Show only the job IDs. + +## Examples + +* Show all jobs: + ```bash + row show jobs + ``` +* Show jobs that execute actions on any of the given directories: + ```bash + row show jobs directory1 directory + ``` +* Show jobs that execute the action 'one': + ```bash + row show jobs --action one + ``` +* Show jobs that execute an action starting with 'analyze': + ```bash + row show jobs --action 'analyze*' + ``` +* Cancel SLURM jobs executing action 'two': + ```bash + row show jobs --action two --short | xargs scancel + ``` diff --git a/doc/src/row/submit.md b/doc/src/row/submit.md index b332501..c16b940 100644 --- a/doc/src/row/submit.md +++ b/doc/src/row/submit.md @@ -24,7 +24,7 @@ the entire workspace. (also: `-a`) -Set `--action ` to choose which actions to display by name. By default, **row** +Set `--action ` to choose which actions to submit by name. By default, **row** submits the eligible jobs of all actions. `` is a wildcard pattern. ### `--dry-run` diff --git a/src/cli.rs b/src/cli.rs index e2f7579..d758f5d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -194,7 +194,30 @@ pub enum ShowCommands { /** Show submitted jobs. - TODO. + `row show jobs` lists submitted jobs that execute a matching action on + any of the provided directories. + + EXAMPLES + + * Show all jobs: + + row show jobs + + * Show jobs that execute actions on any of the given directories: + + row show jobs directory1 directory + + * Show jobs that execute the action 'one': + + row show jobs --action one + + * Show jobs that execute an action starting with 'analyze': + + row show jobs --action 'analyze*' + + * Cancel SLURM jobs executing action 'two': + + row show jobs --action two --short | xargs scancel */ Jobs(jobs::Arguments), } From 0a4f050a85f128cc915d43e044efc74620c67e23 Mon Sep 17 00:00:00 2001 From: "Joshua A. Anderson" Date: Thu, 5 Dec 2024 12:28:35 -0500 Subject: [PATCH 4/6] Include show jobs. --- doc/src/release-notes.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/doc/src/release-notes.md b/doc/src/release-notes.md index 79f169e..8a442cd 100644 --- a/doc/src/release-notes.md +++ b/doc/src/release-notes.md @@ -4,18 +4,25 @@ *Highlights:* -**Row** 0.4 expands the `command` templating functionality to improve support for -command line applications as actions. This removes the need for _shim_ scripts that -access the workspace path and/or directory values before invoking a subprocess. -`{workspace_path}` expands to the current project's workspace path and `{/JSON pointer}` -expands to the value of the given JSON pointer for the directory acted on. - -**Row** 0.4 also adds _shell autocompletion_. To enable, execute the appropriate -command in your shell's profile: +**Row** 0.4 expands the `command` templating functionality, adds shell autocompletion, +and the `show jobs` subcommand. + +The expanded _templating_ functionality improves support for command line applications +as actions. This removes the need for _shim_ scripts that access the workspace path and/ +or directory values before invoking a subprocess. `{workspace_path}` expands to the +current project's workspace path and `{/JSON pointer}` expands to the value of the given +JSON pointer for the directory acted on. + +_Shell autocompletion_ allows users to autocomplete all parameter names and +workspace dependent values for `cluster`, `action`, and `directories`. To enable, +execute the appropriate command in your shell's profile: * Bash: `source <(COMPLETE=bash your_program)` * Fish: `source (COMPLETE=fish your_program | psub)` * Zsh: `source <(COMPLETE=zsh your_program)` +`show jobs` prints a table summarizing all currently submitted jobs that match given +action and directory criteria. + *Added:* * In job scripts, set the environment variable `ACTION_WORKSPACE_PATH` to the _relative_ @@ -25,6 +32,7 @@ command in your shell's profile: * `{/JSON pointer}` template parameter in `action.command` - replaced with the portion of the directory's value referenced by the given JSON pointer. * Shell autocomplete. +* `show jobs` subcommand. *Fixed:* From 8a4137dac504f73bdea593f606aaaea4c958b0ce Mon Sep 17 00:00:00 2001 From: "Joshua A. Anderson" Date: Thu, 5 Dec 2024 12:35:38 -0500 Subject: [PATCH 5/6] pre-commit --- doc/src/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/release-notes.md b/doc/src/release-notes.md index a7345c4..87b707c 100644 --- a/doc/src/release-notes.md +++ b/doc/src/release-notes.md @@ -13,7 +13,7 @@ or directory values before invoking a subprocess. `{workspace_path}` expands to current project's workspace path and `{/JSON pointer}` expands to the value of the given JSON pointer for the directory acted on. -_Shell autocompletion_ allows users to autocomplete all parameter names and +_Shell autocompletion_ allows users to autocomplete all parameter names and workspace dependent values for `cluster`, `action`, and `directories`. To enable, execute the appropriate command in your shell's profile: * Bash: `source <(COMPLETE=bash row)` From 2616e20b1a9132a940484ebb3cf81eb93ee8bf75 Mon Sep 17 00:00:00 2001 From: "Joshua A. Anderson" Date: Thu, 5 Dec 2024 12:38:38 -0500 Subject: [PATCH 6/6] Add show jobs test. --- tests/cli.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 1156e46..8faa2b4 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -894,3 +894,25 @@ fn init() -> Result<(), Box> { Ok(()) } + +#[test] +#[parallel] +fn show_jobs() -> Result<(), Box> { + let temp = TempDir::new()?; + let _ = setup_sample_workflow(&temp, 4); + + Command::cargo_bin("row")? + .args(["show", "jobs"]) + .args(["--cluster", "none"]) + .env_remove("ROW_COLOR") + .env_remove("CLICOLOR") + .env("ROW_HOME", "/not/a/path") + .current_dir(temp.path()) + .assert() + .success(); + + // It is not possible to automatically check the output of show jobs. This unit test + // must run on systems that do not have SLURM or where developers have no SLURM account. + + Ok(()) +}