| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package NBI::Manifest; |
|
2
|
|
|
|
|
|
|
#ABSTRACT: Provenance record for a single nbilaunch job run |
|
3
|
|
|
|
|
|
|
# |
|
4
|
|
|
|
|
|
|
# NBI::Manifest - Reads and writes JSON provenance records for nbilaunch. |
|
5
|
|
|
|
|
|
|
# |
|
6
|
|
|
|
|
|
|
# DESCRIPTION: |
|
7
|
|
|
|
|
|
|
# Serialises every input, parameter, output, and Slurm resource used by a |
|
8
|
|
|
|
|
|
|
# single launcher invocation to a JSON file that lives alongside the results. |
|
9
|
|
|
|
|
|
|
# Written in two phases: |
|
10
|
|
|
|
|
|
|
# 1. At submission (by nbilaunch): status "submitted", no job ID yet. |
|
11
|
|
|
|
|
|
|
# 2. At job end (by injected shell): status "success" or "failure", |
|
12
|
|
|
|
|
|
|
# exit code, completion time, and checksums are patched in. |
|
13
|
|
|
|
|
|
|
# |
|
14
|
|
|
|
|
|
|
# The on-disk format uses JSON::PP with canonical key ordering so that |
|
15
|
|
|
|
|
|
|
# diffs between runs are clean. |
|
16
|
|
|
|
|
|
|
# |
|
17
|
|
|
|
|
|
|
# RELATIONSHIPS: |
|
18
|
|
|
|
|
|
|
# - Created by NBI::Launcher->build() and written by bin/nbilaunch. |
|
19
|
|
|
|
|
|
|
# - The injected shell function _nbi_manifest_update patches the file |
|
20
|
|
|
|
|
|
|
# in-place using a perl one-liner (no jq dependency). |
|
21
|
|
|
|
|
|
|
# - Complex launchers can chain jobs via NBI::Manifest->load() to read |
|
22
|
|
|
|
|
|
|
# a previous run's output paths. |
|
23
|
|
|
|
|
|
|
# |
|
24
|
|
|
|
|
|
|
|
|
25
|
2
|
|
|
2
|
|
237117
|
use 5.012; |
|
|
2
|
|
|
|
|
6
|
|
|
26
|
2
|
|
|
2
|
|
8
|
use strict; |
|
|
2
|
|
|
|
|
2
|
|
|
|
2
|
|
|
|
|
40
|
|
|
27
|
2
|
|
|
2
|
|
7
|
use warnings; |
|
|
2
|
|
|
|
|
2
|
|
|
|
2
|
|
|
|
|
103
|
|
|
28
|
2
|
|
|
2
|
|
7
|
use Carp qw(confess); |
|
|
2
|
|
|
|
|
2
|
|
|
|
2
|
|
|
|
|
101
|
|
|
29
|
2
|
|
|
2
|
|
9
|
use JSON::PP; |
|
|
2
|
|
|
|
|
2
|
|
|
|
2
|
|
|
|
|
106
|
|
|
30
|
2
|
|
|
2
|
|
443
|
use POSIX qw(strftime); |
|
|
2
|
|
|
|
|
5197
|
|
|
|
2
|
|
|
|
|
17
|
|
|
31
|
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
$NBI::Manifest::VERSION = $NBI::Slurm::VERSION; |
|
33
|
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
# Fields serialised to JSON (in this order, for readability). |
|
35
|
|
|
|
|
|
|
# The internal _path field is excluded. |
|
36
|
|
|
|
|
|
|
my @JSON_FIELDS = qw( |
|
37
|
|
|
|
|
|
|
tool tool_version launcher_version nbi_slurm_version |
|
38
|
|
|
|
|
|
|
submitted_at completed_at |
|
39
|
|
|
|
|
|
|
slurm_job_id slurm_queue slurm_cpus slurm_mem_gb |
|
40
|
|
|
|
|
|
|
host user status exit_code sample |
|
41
|
|
|
|
|
|
|
inputs params outputs |
|
42
|
|
|
|
|
|
|
outdir scratch checksums script |
|
43
|
|
|
|
|
|
|
); |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
sub new { |
|
46
|
5
|
|
|
5
|
1
|
3342
|
my ($class, %args) = @_; |
|
47
|
|
|
|
|
|
|
|
|
48
|
5
|
|
|
|
|
11
|
my $self = bless {}, $class; |
|
49
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
# Required fields |
|
51
|
5
|
|
|
|
|
9
|
for my $f (qw(tool sample outdir)) { |
|
52
|
|
|
|
|
|
|
confess "ERROR NBI::Manifest: missing required field '$f'\n" |
|
53
|
14
|
100
|
|
|
|
288
|
unless defined $args{$f}; |
|
54
|
|
|
|
|
|
|
} |
|
55
|
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
# Populate all known fields with defaults where sensible |
|
57
|
3
|
|
|
|
|
17
|
$self->{tool} = $args{tool}; |
|
58
|
3
|
|
100
|
|
|
13
|
$self->{tool_version} = $args{tool_version} // 'unknown'; |
|
59
|
3
|
|
50
|
|
|
17
|
$self->{launcher_version} = $args{launcher_version} // '0.1.0'; |
|
60
|
3
|
|
50
|
|
|
12
|
$self->{nbi_slurm_version} = $args{nbi_slurm_version} // ($NBI::Slurm::VERSION // 'unknown'); |
|
|
|
|
33
|
|
|
|
|
|
61
|
|
|
|
|
|
|
$self->{submitted_at} = $args{submitted_at} |
|
62
|
3
|
|
33
|
|
|
95
|
// strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()); |
|
63
|
3
|
|
|
|
|
8
|
$self->{completed_at} = $args{completed_at}; # undef until job ends |
|
64
|
3
|
|
|
|
|
4
|
$self->{slurm_job_id} = $args{slurm_job_id}; # undef until submitted |
|
65
|
3
|
|
100
|
|
|
8
|
$self->{slurm_queue} = $args{slurm_queue} // 'unknown'; |
|
66
|
3
|
|
100
|
|
|
9
|
$self->{slurm_cpus} = $args{slurm_cpus} // 1; |
|
67
|
3
|
|
100
|
|
|
7
|
$self->{slurm_mem_gb} = $args{slurm_mem_gb} // 0; |
|
68
|
|
|
|
|
|
|
$self->{host} = $args{host} |
|
69
|
|
|
|
|
|
|
// $ENV{HOSTNAME} |
|
70
|
3
|
|
33
|
|
|
26
|
// do { chomp(my $h = `hostname 2>/dev/null`); $h } // 'unknown'; |
|
|
0
|
|
33
|
|
|
0
|
|
|
|
0
|
|
0
|
|
|
0
|
|
|
71
|
3
|
|
33
|
|
|
23
|
$self->{user} = $args{user} // $ENV{USER} // 'unknown'; |
|
|
|
|
50
|
|
|
|
|
|
72
|
3
|
|
100
|
|
|
13
|
$self->{status} = $args{status} // 'submitted'; |
|
73
|
3
|
|
|
|
|
7
|
$self->{exit_code} = $args{exit_code}; # undef until job ends |
|
74
|
3
|
|
|
|
|
8
|
$self->{sample} = $args{sample}; |
|
75
|
3
|
|
100
|
|
|
7
|
$self->{inputs} = $args{inputs} // {}; |
|
76
|
3
|
|
100
|
|
|
11
|
$self->{params} = $args{params} // {}; |
|
77
|
3
|
|
100
|
|
|
6
|
$self->{outputs} = $args{outputs} // {}; |
|
78
|
3
|
|
|
|
|
3
|
$self->{outdir} = $args{outdir}; |
|
79
|
3
|
|
50
|
|
|
8
|
$self->{scratch} = $args{scratch} // ''; |
|
80
|
3
|
|
50
|
|
|
9
|
$self->{checksums} = $args{checksums} // {}; |
|
81
|
3
|
|
100
|
|
|
9
|
$self->{script} = $args{script} // ''; |
|
82
|
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
# Internal: path where this manifest lives on disk (not serialised) |
|
84
|
3
|
|
|
|
|
16
|
$self->{_path} = $args{_path}; |
|
85
|
|
|
|
|
|
|
|
|
86
|
3
|
|
|
|
|
12
|
return $self; |
|
87
|
|
|
|
|
|
|
} |
|
88
|
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
# ── write($path) ────────────────────────────────────────────────────────────── |
|
90
|
|
|
|
|
|
|
# Serialise to JSON and write to $path. Records _path for future update() calls. |
|
91
|
|
|
|
|
|
|
sub write { |
|
92
|
3
|
|
|
3
|
1
|
2933
|
my ($self, $path) = @_; |
|
93
|
3
|
50
|
|
|
|
7
|
confess "ERROR NBI::Manifest::write: path required\n" unless defined $path; |
|
94
|
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
# Build the hash to serialise (JSON_FIELDS order, excluding _path) |
|
96
|
3
|
|
|
|
|
4
|
my %data; |
|
97
|
3
|
|
|
|
|
4
|
for my $f (@JSON_FIELDS) { |
|
98
|
66
|
|
|
|
|
92
|
$data{$f} = $self->{$f}; |
|
99
|
|
|
|
|
|
|
} |
|
100
|
|
|
|
|
|
|
|
|
101
|
3
|
|
|
|
|
13
|
my $json = JSON::PP->new->utf8->pretty->canonical->encode(\%data); |
|
102
|
|
|
|
|
|
|
|
|
103
|
3
|
50
|
|
|
|
2847
|
open(my $fh, '>', $path) |
|
104
|
|
|
|
|
|
|
or confess "ERROR NBI::Manifest::write: cannot open '$path': $!\n"; |
|
105
|
3
|
|
|
|
|
41
|
print $fh $json; |
|
106
|
3
|
|
|
|
|
258
|
close $fh; |
|
107
|
|
|
|
|
|
|
|
|
108
|
3
|
|
|
|
|
10
|
$self->{_path} = $path; |
|
109
|
3
|
|
|
|
|
25
|
return $self; |
|
110
|
|
|
|
|
|
|
} |
|
111
|
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
# ── load($path) ─────────────────────────────────────────────────────────────── |
|
113
|
|
|
|
|
|
|
# Parse an existing manifest JSON file and return a blessed object. |
|
114
|
|
|
|
|
|
|
sub load { |
|
115
|
3
|
|
|
3
|
1
|
6290
|
my ($class, $path) = @_; |
|
116
|
3
|
50
|
|
|
|
7
|
confess "ERROR NBI::Manifest::load: path required\n" unless defined $path; |
|
117
|
3
|
50
|
|
|
|
89
|
confess "ERROR NBI::Manifest::load: '$path' not found\n" unless -f $path; |
|
118
|
|
|
|
|
|
|
|
|
119
|
3
|
50
|
|
|
|
75
|
open(my $fh, '<', $path) |
|
120
|
|
|
|
|
|
|
or confess "ERROR NBI::Manifest::load: cannot open '$path': $!\n"; |
|
121
|
3
|
|
|
|
|
5
|
my $raw = do { local $/; <$fh> }; |
|
|
3
|
|
|
|
|
10
|
|
|
|
3
|
|
|
|
|
107
|
|
|
122
|
3
|
|
|
|
|
22
|
close $fh; |
|
123
|
|
|
|
|
|
|
|
|
124
|
3
|
|
|
|
|
17
|
my $data = JSON::PP->new->utf8->decode($raw); |
|
125
|
3
|
|
|
|
|
13488
|
my $self = bless $data, $class; |
|
126
|
3
|
|
|
|
|
10
|
$self->{_path} = $path; |
|
127
|
3
|
|
|
|
|
14
|
return $self; |
|
128
|
|
|
|
|
|
|
} |
|
129
|
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
# ── output($name) ───────────────────────────────────────────────────────────── |
|
131
|
|
|
|
|
|
|
# Returns the absolute path of a named output file. |
|
132
|
|
|
|
|
|
|
# Useful for chaining launchers: read a previous manifest to get output paths. |
|
133
|
|
|
|
|
|
|
sub output { |
|
134
|
2
|
|
|
2
|
1
|
2923
|
my ($self, $name) = @_; |
|
135
|
2
|
50
|
|
|
|
5
|
confess "ERROR NBI::Manifest::output: name required\n" unless defined $name; |
|
136
|
2
|
100
|
|
|
|
196
|
my $filename = $self->{outputs}{$name} |
|
137
|
|
|
|
|
|
|
or confess "ERROR NBI::Manifest::output: no output named '$name'\n"; |
|
138
|
1
|
|
|
|
|
3
|
return "$self->{outdir}/$filename"; |
|
139
|
|
|
|
|
|
|
} |
|
140
|
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
# ── update(%changes) ────────────────────────────────────────────────────────── |
|
142
|
|
|
|
|
|
|
# Merge %changes into the object and rewrite to the stored _path. |
|
143
|
|
|
|
|
|
|
# Used by nbilaunch after submission to record the Slurm job ID. |
|
144
|
|
|
|
|
|
|
sub update { |
|
145
|
3
|
|
|
3
|
1
|
1364
|
my ($self, %changes) = @_; |
|
146
|
|
|
|
|
|
|
confess "ERROR NBI::Manifest::update: no path known - call write() first\n" |
|
147
|
3
|
100
|
|
|
|
170
|
unless defined $self->{_path}; |
|
148
|
|
|
|
|
|
|
|
|
149
|
2
|
|
|
|
|
5
|
for my $k (keys %changes) { |
|
150
|
5
|
|
|
|
|
8
|
$self->{$k} = $changes{$k}; |
|
151
|
|
|
|
|
|
|
} |
|
152
|
2
|
|
|
|
|
5
|
$self->write($self->{_path}); |
|
153
|
2
|
|
|
|
|
5
|
return $self; |
|
154
|
|
|
|
|
|
|
} |
|
155
|
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
1; |
|
157
|
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
__END__ |