Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add show jobs #66

Merged
merged 7 commits into from
Dec 5, 2024
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
5 changes: 1 addition & 4 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions doc/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 16 additions & 8 deletions doc/src/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 row)`
* Fish: `source (COMPLETE=fish row | psub)`
* Zsh: `source <(COMPLETE=zsh row)`

`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_
Expand All @@ -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:*

Expand Down
59 changes: 59 additions & 0 deletions doc/src/row/show/jobs.md
Original file line number Diff line number Diff line change
@@ -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 <pattern>` to show only jobs that mach the given pattern by name.
By default, **row** shows jobs executing any action. `<pattern>` 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
```
2 changes: 1 addition & 1 deletion doc/src/row/submit.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ the entire workspace.

(also: `-a`)

Set `--action <pattern>` to choose which actions to display by name. By default, **row**
Set `--action <pattern>` to choose which actions to submit by name. By default, **row**
submits the eligible jobs of all actions. `<pattern>` is a wildcard pattern.

### `--dry-run`
Expand Down
30 changes: 30 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -190,6 +191,35 @@ pub enum ShowCommands {
///
/// row show launchers --all --short
Launchers(launchers::Arguments),

/** Show submitted jobs.

`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),
}

#[derive(Subcommand, Debug)]
Expand Down
147 changes: 147 additions & 0 deletions src/cli/jobs.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>,

/// 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<PathBuf>,
action: &str,
project: &Project,
) -> Result<BTreeMap<u32, JobDetails>, Box<dyn Error>> {
debug!("Finding matching jobs.");
let mut result: BTreeMap<u32, JobDetails> = BTreeMap::new();

let action_matcher = WildMatch::new(action);

let query_directories: HashSet<PathBuf> =
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<W: Write>(
options: &GlobalOptions,
args: Arguments,
multi_progress: &mut MultiProgressContainer,
output: &mut W,
) -> Result<(), Box<dyn Error>> {
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(())
}
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ fn main_detail() -> Result<(), Box<dyn Error>> {
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)?;
Expand Down
5 changes: 5 additions & 0 deletions src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -894,3 +894,25 @@ fn init() -> Result<(), Box<dyn std::error::Error>> {

Ok(())
}

#[test]
#[parallel]
fn show_jobs() -> Result<(), Box<dyn std::error::Error>> {
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(())
}
Loading