5981 |
07 Jul 20 |
nicklas |
1 |
package net.sf.basedb.opengrid.engine; |
5981 |
07 Jul 20 |
nicklas |
2 |
|
5984 |
10 Jul 20 |
nicklas |
3 |
import java.text.SimpleDateFormat; |
5982 |
07 Jul 20 |
nicklas |
4 |
import java.util.Arrays; |
5984 |
10 Jul 20 |
nicklas |
5 |
import java.util.HashSet; |
5984 |
10 Jul 20 |
nicklas |
6 |
import java.util.Locale; |
5984 |
10 Jul 20 |
nicklas |
7 |
import java.util.Set; |
6829 |
01 Sep 22 |
nicklas |
8 |
import java.util.regex.Pattern; |
5982 |
07 Jul 20 |
nicklas |
9 |
|
5984 |
10 Jul 20 |
nicklas |
10 |
import org.slf4j.LoggerFactory; |
5984 |
10 Jul 20 |
nicklas |
11 |
|
5984 |
10 Jul 20 |
nicklas |
12 |
import net.sf.basedb.core.ItemNotFoundException; |
5984 |
10 Jul 20 |
nicklas |
13 |
import net.sf.basedb.core.Job; |
5984 |
10 Jul 20 |
nicklas |
14 |
import net.sf.basedb.opengrid.CmdResult; |
5982 |
07 Jul 20 |
nicklas |
15 |
import net.sf.basedb.opengrid.JobDefinition; |
5984 |
10 Jul 20 |
nicklas |
16 |
import net.sf.basedb.opengrid.JobIdentifier; |
5984 |
10 Jul 20 |
nicklas |
17 |
import net.sf.basedb.opengrid.JobStatus; |
6629 |
07 Mar 22 |
nicklas |
18 |
import net.sf.basedb.opengrid.OpenGrid; |
5984 |
10 Jul 20 |
nicklas |
19 |
import net.sf.basedb.opengrid.OpenGridSession; |
6629 |
07 Mar 22 |
nicklas |
20 |
import net.sf.basedb.opengrid.ScriptBuilder; |
6672 |
11 Apr 22 |
nicklas |
21 |
import net.sf.basedb.opengrid.config.BatchConfig; |
5981 |
07 Jul 20 |
nicklas |
22 |
import net.sf.basedb.opengrid.config.ClusterConfig; |
5982 |
07 Jul 20 |
nicklas |
23 |
import net.sf.basedb.opengrid.config.JobConfig; |
6629 |
07 Mar 22 |
nicklas |
24 |
import net.sf.basedb.opengrid.filetransfer.InputStreamUploadSource; |
5982 |
07 Jul 20 |
nicklas |
25 |
import net.sf.basedb.opengrid.filetransfer.StringUploadSource; |
5982 |
07 Jul 20 |
nicklas |
26 |
import net.sf.basedb.opengrid.filetransfer.UploadSource; |
7075 |
27 Mar 23 |
nicklas |
27 |
import net.sf.basedb.opengrid.service.OpenGridService; |
5984 |
10 Jul 20 |
nicklas |
28 |
import net.sf.basedb.util.Values; |
7075 |
27 Mar 23 |
nicklas |
29 |
import net.sf.basedb.util.extensions.logging.ExtensionsLog; |
7075 |
27 Mar 23 |
nicklas |
30 |
import net.sf.basedb.util.extensions.logging.ExtensionsLogger; |
5984 |
10 Jul 20 |
nicklas |
31 |
import net.sf.basedb.util.formatter.DateFormatter; |
5981 |
07 Jul 20 |
nicklas |
32 |
|
5981 |
07 Jul 20 |
nicklas |
33 |
/** |
5981 |
07 Jul 20 |
nicklas |
Cluster engine implementation for Slurm clusters. |
5981 |
07 Jul 20 |
nicklas |
@author nicklas |
5981 |
07 Jul 20 |
nicklas |
@since 1.4 |
5981 |
07 Jul 20 |
nicklas |
37 |
*/ |
5981 |
07 Jul 20 |
nicklas |
38 |
public class SlurmEngine |
5981 |
07 Jul 20 |
nicklas |
39 |
implements ClusterEngine |
5981 |
07 Jul 20 |
nicklas |
40 |
{ |
5984 |
10 Jul 20 |
nicklas |
41 |
|
7075 |
27 Mar 23 |
nicklas |
42 |
private static final ExtensionsLogger logger = |
7075 |
27 Mar 23 |
nicklas |
43 |
ExtensionsLog.getLogger(OpenGridService.ID, true).wrap(LoggerFactory.getLogger(SlurmEngine.class)); |
5981 |
07 Jul 20 |
nicklas |
44 |
|
5990 |
19 Aug 20 |
nicklas |
// All sbatch options that are automatically set by |
5990 |
19 Aug 20 |
nicklas |
// our script (including short versions) |
5989 |
19 Aug 20 |
nicklas |
47 |
private static final Set<String> ignoredSbatchOptions = |
5990 |
19 Aug 20 |
nicklas |
48 |
new HashSet<>(Arrays.asList( |
5990 |
19 Aug 20 |
nicklas |
49 |
"parsable", |
5990 |
19 Aug 20 |
nicklas |
50 |
"job-name", "J", |
5990 |
19 Aug 20 |
nicklas |
51 |
"chdir", "D", |
5990 |
19 Aug 20 |
nicklas |
52 |
"output", "o", |
5990 |
19 Aug 20 |
nicklas |
53 |
"error", "e")); |
5989 |
19 Aug 20 |
nicklas |
54 |
|
5981 |
07 Jul 20 |
nicklas |
55 |
public SlurmEngine() |
5981 |
07 Jul 20 |
nicklas |
56 |
{} |
5981 |
07 Jul 20 |
nicklas |
57 |
|
5981 |
07 Jul 20 |
nicklas |
58 |
@Override |
5981 |
07 Jul 20 |
nicklas |
59 |
public void setDefaultConfig(ClusterConfig config) |
5981 |
07 Jul 20 |
nicklas |
60 |
{ |
5981 |
07 Jul 20 |
nicklas |
61 |
config.setOpenGridInfoCommand("sinfo -V"); |
5981 |
07 Jul 20 |
nicklas |
62 |
config.setTmpFolder("${TMPDIR}", false); // TODO - TMPDIR need to be handled by custom prolog/epilog script in Slurm |
5981 |
07 Jul 20 |
nicklas |
63 |
} |
5981 |
07 Jul 20 |
nicklas |
64 |
|
5982 |
07 Jul 20 |
nicklas |
65 |
@Override |
6614 |
28 Feb 22 |
nicklas |
66 |
public JobSubmission createJobSubmission(OpenGridSession session, JobDefinition job, String workFolder, String tmpFolder) |
5982 |
07 Jul 20 |
nicklas |
67 |
{ |
6829 |
01 Sep 22 |
nicklas |
68 |
UploadSource batch = new StringUploadSource("batch.sh", createSbatchScript(session, job, workFolder, tmpFolder)); |
6629 |
07 Mar 22 |
nicklas |
69 |
UploadSource run = getScript("run.sh"); |
6629 |
07 Mar 22 |
nicklas |
70 |
UploadSource jobScript = new StringUploadSource("job.sh", createJobScript(job, workFolder, tmpFolder)); |
6629 |
07 Mar 22 |
nicklas |
71 |
return new JobSubmission("sbatch " + workFolder + "/batch.sh", Arrays.asList(batch, run, jobScript)); |
5982 |
07 Jul 20 |
nicklas |
72 |
} |
5982 |
07 Jul 20 |
nicklas |
73 |
|
5982 |
07 Jul 20 |
nicklas |
74 |
/** |
5982 |
07 Jul 20 |
nicklas |
Generates a script that can be submitted with 'sbatch' to slurm. This |
5982 |
07 Jul 20 |
nicklas |
script will in turn call the actual job script with 'srun'. |
5982 |
07 Jul 20 |
nicklas |
77 |
*/ |
6829 |
01 Sep 22 |
nicklas |
78 |
public String createSbatchScript(OpenGridSession session, JobDefinition job, String workFolder, String tmpFolder) |
5982 |
07 Jul 20 |
nicklas |
79 |
{ |
5987 |
18 Aug 20 |
nicklas |
80 |
JobConfig config = job.getConfig(); |
6672 |
11 Apr 22 |
nicklas |
81 |
BatchConfig batchConfig = job.getBatchConfig(); |
5987 |
18 Aug 20 |
nicklas |
82 |
|
6629 |
07 Mar 22 |
nicklas |
83 |
ScriptBuilder script = new ScriptBuilder(); |
6629 |
07 Mar 22 |
nicklas |
84 |
script.cmd("#!/bin/bash"); |
6629 |
07 Mar 22 |
nicklas |
85 |
script.comment("--- start of sbatch options ---"); |
6629 |
07 Mar 22 |
nicklas |
86 |
script.cmd("#SBATCH --parsable"); // sbatch will report <job-id> only |
6629 |
07 Mar 22 |
nicklas |
87 |
script.cmd("#SBATCH --job-name=" + job.getName()); // Name of the job |
6629 |
07 Mar 22 |
nicklas |
88 |
script.cmd("#SBATCH --chdir=" + workFolder); // Working directory (must already exist) |
6629 |
07 Mar 22 |
nicklas |
89 |
script.cmd("#SBATCH --output=stdout"); // Stdout is saved to this file |
6629 |
07 Mar 22 |
nicklas |
90 |
script.cmd("#SBATCH --error=stderr"); // Stderr is saved to this file |
5982 |
07 Jul 20 |
nicklas |
91 |
|
5987 |
18 Aug 20 |
nicklas |
92 |
if (config.getSlurmNice() != null) |
5987 |
18 Aug 20 |
nicklas |
93 |
{ |
6629 |
07 Mar 22 |
nicklas |
94 |
script.cmd("#SBATCH --nice=" + config.getSlurmNice()); |
5987 |
18 Aug 20 |
nicklas |
95 |
} |
6672 |
11 Apr 22 |
nicklas |
96 |
if (batchConfig != null) |
6672 |
11 Apr 22 |
nicklas |
97 |
{ |
6672 |
11 Apr 22 |
nicklas |
98 |
int delay = batchConfig.getDelayForNextJob(); |
6672 |
11 Apr 22 |
nicklas |
99 |
if (delay > 0) |
6672 |
11 Apr 22 |
nicklas |
100 |
{ |
6672 |
11 Apr 22 |
nicklas |
101 |
script.cmd("#SBATCH --begin=now+"+delay); |
6672 |
11 Apr 22 |
nicklas |
102 |
} |
6672 |
11 Apr 22 |
nicklas |
103 |
} |
5989 |
19 Aug 20 |
nicklas |
104 |
config.appendSbatchOptionsToScript(script, ignoredSbatchOptions); |
6629 |
07 Mar 22 |
nicklas |
105 |
script.comment("--- end sbatch options ---"); |
6629 |
07 Mar 22 |
nicklas |
106 |
script.export("NSLOTS", "${SLURM_JOB_CPUS_PER_NODE}"); // For compatibility with OpenGrid |
6629 |
07 Mar 22 |
nicklas |
107 |
script.export("WD", workFolder); |
6829 |
01 Sep 22 |
nicklas |
108 |
if (isSacctDisabled(session)) |
6829 |
01 Sep 22 |
nicklas |
109 |
{ |
6829 |
01 Sep 22 |
nicklas |
110 |
script.export("STATUS_FILE", getJobStatusPath(session, "${SLURM_JOB_ID}")); |
6829 |
01 Sep 22 |
nicklas |
111 |
} |
6826 |
31 Aug 22 |
nicklas |
112 |
script.export("_tmpdir", tmpFolder); |
6629 |
07 Mar 22 |
nicklas |
113 |
if (job.getDebug()) script.export("JOB_DEBUG", "1"); |
6831 |
02 Sep 22 |
nicklas |
114 |
script.cmd(". ./run.sh"); |
6831 |
02 Sep 22 |
nicklas |
// Note! the initial '.' in the above command is important since it causes the 'run.sh' script |
6831 |
02 Sep 22 |
nicklas |
// to be executed in the same process as 'batch.sh', and this affects the behaviour when |
6831 |
02 Sep 22 |
nicklas |
// manually aborting a job with 'scancel'. The 'trap' commands in 'run.sh' will only work |
6831 |
02 Sep 22 |
nicklas |
// as expected if they are in the original batch process. |
5982 |
07 Jul 20 |
nicklas |
119 |
return script.toString(); |
5982 |
07 Jul 20 |
nicklas |
120 |
} |
5982 |
07 Jul 20 |
nicklas |
121 |
|
5982 |
07 Jul 20 |
nicklas |
122 |
/** |
6629 |
07 Mar 22 |
nicklas |
Generates a script that executes the job script. |
5982 |
07 Jul 20 |
nicklas |
124 |
*/ |
6629 |
07 Mar 22 |
nicklas |
125 |
public String createJobScript(JobDefinition job, String workFolder, String tmpFolder) |
5982 |
07 Jul 20 |
nicklas |
126 |
{ |
6629 |
07 Mar 22 |
nicklas |
127 |
ScriptBuilder script = new ScriptBuilder(); |
6629 |
07 Mar 22 |
nicklas |
128 |
script.cmd("#!/bin/bash"); |
6826 |
31 Aug 22 |
nicklas |
129 |
script.export("TMPDIR", "${_tmpdir}"); |
6629 |
07 Mar 22 |
nicklas |
130 |
script.cmd(job.getCmd()); |
5982 |
07 Jul 20 |
nicklas |
131 |
return script.toString(); |
5982 |
07 Jul 20 |
nicklas |
132 |
} |
6629 |
07 Mar 22 |
nicklas |
133 |
|
6829 |
01 Sep 22 |
nicklas |
134 |
/** |
6829 |
01 Sep 22 |
nicklas |
Get the path to where the status information for a job is saved. |
6829 |
01 Sep 22 |
nicklas |
@since 1.7 |
6829 |
01 Sep 22 |
nicklas |
137 |
*/ |
6829 |
01 Sep 22 |
nicklas |
138 |
private String getJobStatusPath(OpenGridSession session, String jobId) |
6829 |
01 Sep 22 |
nicklas |
139 |
{ |
6829 |
01 Sep 22 |
nicklas |
140 |
return session.getHost().getConfig().getJobFolder()+"/status-"+jobId; |
6829 |
01 Sep 22 |
nicklas |
141 |
} |
6829 |
01 Sep 22 |
nicklas |
142 |
|
6829 |
01 Sep 22 |
nicklas |
143 |
/** |
6829 |
01 Sep 22 |
nicklas |
Check the flag if 'sacct' is disabled on the cluster. |
6829 |
01 Sep 22 |
nicklas |
@since 1.7 |
6829 |
01 Sep 22 |
nicklas |
146 |
*/ |
6829 |
01 Sep 22 |
nicklas |
147 |
private boolean isSacctDisabled(OpenGridSession session) |
6829 |
01 Sep 22 |
nicklas |
148 |
{ |
6829 |
01 Sep 22 |
nicklas |
149 |
return Values.getBoolean(session.getHost().getConfig().getCustomOption("slurm-accounting-disabled")); |
6829 |
01 Sep 22 |
nicklas |
150 |
} |
6829 |
01 Sep 22 |
nicklas |
151 |
|
6629 |
07 Mar 22 |
nicklas |
152 |
private UploadSource getScript(String name) |
6629 |
07 Mar 22 |
nicklas |
153 |
{ |
6629 |
07 Mar 22 |
nicklas |
154 |
return new InputStreamUploadSource(name, OpenGrid.class.getResourceAsStream("/net/sf/basedb/opengrid/engine/slurm/"+name)); |
6629 |
07 Mar 22 |
nicklas |
155 |
} |
6629 |
07 Mar 22 |
nicklas |
156 |
|
5984 |
10 Jul 20 |
nicklas |
157 |
|
5984 |
10 Jul 20 |
nicklas |
158 |
@Override |
5984 |
10 Jul 20 |
nicklas |
159 |
public CmdResult<JobStatus> getStatusInQueue(OpenGridSession session, JobIdentifier jobId, int timeAdjustment) |
5984 |
10 Jul 20 |
nicklas |
160 |
{ |
5984 |
10 Jul 20 |
nicklas |
161 |
return session.execute(new SqueueCmd(jobId, timeAdjustment), 5); |
5984 |
10 Jul 20 |
nicklas |
162 |
} |
5984 |
10 Jul 20 |
nicklas |
163 |
|
5984 |
10 Jul 20 |
nicklas |
164 |
|
5984 |
10 Jul 20 |
nicklas |
165 |
@Override |
5984 |
10 Jul 20 |
nicklas |
166 |
public CmdResult<JobStatus> getStatusIfFinished(OpenGridSession session, JobIdentifier jobId, int timeAdjustment) |
5984 |
10 Jul 20 |
nicklas |
167 |
{ |
6829 |
01 Sep 22 |
nicklas |
168 |
CmdResult<JobStatus> cmd = null; |
6829 |
01 Sep 22 |
nicklas |
169 |
if (isSacctDisabled(session)) |
6829 |
01 Sep 22 |
nicklas |
170 |
{ |
6829 |
01 Sep 22 |
nicklas |
171 |
cmd = new StatusCmd(getJobStatusPath(session, jobId.getClusterJobId()), jobId, timeAdjustment); |
6829 |
01 Sep 22 |
nicklas |
172 |
} |
6829 |
01 Sep 22 |
nicklas |
173 |
else |
6829 |
01 Sep 22 |
nicklas |
174 |
{ |
6829 |
01 Sep 22 |
nicklas |
175 |
cmd = new SacctCmd(jobId, timeAdjustment); |
6829 |
01 Sep 22 |
nicklas |
176 |
} |
6829 |
01 Sep 22 |
nicklas |
177 |
return session.execute(cmd, 5); |
5984 |
10 Jul 20 |
nicklas |
178 |
} |
5984 |
10 Jul 20 |
nicklas |
179 |
|
5984 |
10 Jul 20 |
nicklas |
180 |
@Override |
5984 |
10 Jul 20 |
nicklas |
181 |
public CmdResult<String> cancelJob(OpenGridSession session, JobIdentifier jobId) |
5984 |
10 Jul 20 |
nicklas |
182 |
{ |
5984 |
10 Jul 20 |
nicklas |
183 |
return session.executeCmd("scancel " + jobId.getClusterJobId(), 5); |
5984 |
10 Jul 20 |
nicklas |
184 |
} |
5984 |
10 Jul 20 |
nicklas |
185 |
|
5984 |
10 Jul 20 |
nicklas |
186 |
/** |
5984 |
10 Jul 20 |
nicklas |
Implements the 'squeue' command for getting information about a waiting |
5984 |
10 Jul 20 |
nicklas |
or running job. |
5984 |
10 Jul 20 |
nicklas |
189 |
|
5984 |
10 Jul 20 |
nicklas |
We need to use '-o %all' to get output with '|' as separator. It is not |
5984 |
10 Jul 20 |
nicklas |
possible to specify only the columns that we need. |
5984 |
10 Jul 20 |
nicklas |
192 |
*/ |
5984 |
10 Jul 20 |
nicklas |
193 |
public static class SqueueCmd |
5984 |
10 Jul 20 |
nicklas |
194 |
extends CmdResult<JobStatus> |
5984 |
10 Jul 20 |
nicklas |
195 |
{ |
5984 |
10 Jul 20 |
nicklas |
196 |
private final JobIdentifier jobId; |
5984 |
10 Jul 20 |
nicklas |
197 |
private final int timeAdjustment; |
5984 |
10 Jul 20 |
nicklas |
198 |
|
5984 |
10 Jul 20 |
nicklas |
199 |
public SqueueCmd(JobIdentifier jobId, int timeAdjustment) |
5984 |
10 Jul 20 |
nicklas |
200 |
{ |
5984 |
10 Jul 20 |
nicklas |
201 |
super("squeue -o %all -j" + jobId.getClusterJobId()); // NOTE! No space after '-j' |
5984 |
10 Jul 20 |
nicklas |
202 |
this.jobId = jobId; |
5984 |
10 Jul 20 |
nicklas |
203 |
this.timeAdjustment = timeAdjustment; |
5984 |
10 Jul 20 |
nicklas |
204 |
} |
5984 |
10 Jul 20 |
nicklas |
205 |
|
5984 |
10 Jul 20 |
nicklas |
206 |
@Override |
5984 |
10 Jul 20 |
nicklas |
207 |
protected void parseResult() |
5984 |
10 Jul 20 |
nicklas |
208 |
{ |
5984 |
10 Jul 20 |
nicklas |
209 |
if (getExitStatus() != 0) return; |
5984 |
10 Jul 20 |
nicklas |
210 |
|
5984 |
10 Jul 20 |
nicklas |
211 |
if (logger.isDebugEnabled()) |
5984 |
10 Jul 20 |
nicklas |
212 |
{ |
5984 |
10 Jul 20 |
nicklas |
213 |
logger.debug("Got 'squeue' information for job: " + jobId); |
5984 |
10 Jul 20 |
nicklas |
214 |
} |
5984 |
10 Jul 20 |
nicklas |
215 |
try |
5984 |
10 Jul 20 |
nicklas |
216 |
{ |
5984 |
10 Jul 20 |
nicklas |
217 |
SlurmJobStatus status = new SlurmJobStatus(jobId); |
5984 |
10 Jul 20 |
nicklas |
218 |
status.readFromSqueueAndSacct(getStdout(), timeAdjustment); |
5984 |
10 Jul 20 |
nicklas |
219 |
setResult(status); |
5984 |
10 Jul 20 |
nicklas |
220 |
} |
5984 |
10 Jul 20 |
nicklas |
221 |
catch (ItemNotFoundException ex) |
5984 |
10 Jul 20 |
nicklas |
222 |
{ |
5984 |
10 Jul 20 |
nicklas |
// Thrown when the 'squeue' command completes but has no data for requested job |
5984 |
10 Jul 20 |
nicklas |
// Typically happens just after the job has completed -- information should be |
5984 |
10 Jul 20 |
nicklas |
// available by 'sacct'. |
5984 |
10 Jul 20 |
nicklas |
226 |
setExitStatus(1); |
5984 |
10 Jul 20 |
nicklas |
227 |
} |
5984 |
10 Jul 20 |
nicklas |
228 |
catch (Exception ex) |
5984 |
10 Jul 20 |
nicklas |
229 |
{ |
5984 |
10 Jul 20 |
nicklas |
230 |
setException(new RuntimeException("Could not parse 'squeue' output for job: " + jobId, ex)); |
5984 |
10 Jul 20 |
nicklas |
231 |
logger.error("Could not parse 'squeue' output for job: " + jobId, ex); |
5984 |
10 Jul 20 |
nicklas |
232 |
} |
5984 |
10 Jul 20 |
nicklas |
233 |
} |
5984 |
10 Jul 20 |
nicklas |
234 |
} |
5984 |
10 Jul 20 |
nicklas |
235 |
|
5984 |
10 Jul 20 |
nicklas |
236 |
/** |
5984 |
10 Jul 20 |
nicklas |
Implements the 'sacct' command for getting information about a completed job. |
5984 |
10 Jul 20 |
nicklas |
238 |
|
5984 |
10 Jul 20 |
nicklas |
The -P option generates output separated by '|' and we can specify the columns |
5984 |
10 Jul 20 |
nicklas |
that we need. |
5984 |
10 Jul 20 |
nicklas |
241 |
*/ |
5984 |
10 Jul 20 |
nicklas |
242 |
public static class SacctCmd |
5984 |
10 Jul 20 |
nicklas |
243 |
extends CmdResult<JobStatus> |
5984 |
10 Jul 20 |
nicklas |
244 |
{ |
5984 |
10 Jul 20 |
nicklas |
245 |
|
5984 |
10 Jul 20 |
nicklas |
246 |
/** |
5984 |
10 Jul 20 |
nicklas |
Data columns we want to return from 'sacct'. Note that order is important |
5984 |
10 Jul 20 |
nicklas |
in some cases. For example we need 'ExitCode' before 'State' since 'State' will |
5984 |
10 Jul 20 |
nicklas |
override in some cases. |
5984 |
10 Jul 20 |
nicklas |
250 |
*/ |
5984 |
10 Jul 20 |
nicklas |
251 |
public static final String SACCT_COLS = "JobID,JobName,ExitCode,State,Submit,Start,End,Partition,NodeList"; |
5984 |
10 Jul 20 |
nicklas |
252 |
|
5984 |
10 Jul 20 |
nicklas |
253 |
private final JobIdentifier jobId; |
5984 |
10 Jul 20 |
nicklas |
254 |
private final int timeAdjustment; |
5984 |
10 Jul 20 |
nicklas |
255 |
public SacctCmd(JobIdentifier jobId, int timeAdjustment) |
5984 |
10 Jul 20 |
nicklas |
256 |
{ |
5984 |
10 Jul 20 |
nicklas |
257 |
super("sacct -P -o " + SACCT_COLS + " -j " + jobId.getClusterJobId()); |
5984 |
10 Jul 20 |
nicklas |
258 |
this.jobId = jobId; |
5984 |
10 Jul 20 |
nicklas |
259 |
this.timeAdjustment = timeAdjustment; |
5984 |
10 Jul 20 |
nicklas |
260 |
} |
5984 |
10 Jul 20 |
nicklas |
261 |
|
5984 |
10 Jul 20 |
nicklas |
262 |
@Override |
5984 |
10 Jul 20 |
nicklas |
263 |
protected void parseResult() |
5984 |
10 Jul 20 |
nicklas |
264 |
{ |
5984 |
10 Jul 20 |
nicklas |
265 |
if (getExitStatus() != 0) return; |
5984 |
10 Jul 20 |
nicklas |
266 |
|
5984 |
10 Jul 20 |
nicklas |
267 |
if (logger.isDebugEnabled()) |
5984 |
10 Jul 20 |
nicklas |
268 |
{ |
5984 |
10 Jul 20 |
nicklas |
269 |
logger.debug("Got 'sacct' information for job: " + jobId); |
5984 |
10 Jul 20 |
nicklas |
270 |
} |
5984 |
10 Jul 20 |
nicklas |
271 |
try |
5984 |
10 Jul 20 |
nicklas |
272 |
{ |
5984 |
10 Jul 20 |
nicklas |
273 |
SlurmJobStatus status = new SlurmJobStatus(jobId); |
5984 |
10 Jul 20 |
nicklas |
274 |
status.readFromSqueueAndSacct(getStdout(), timeAdjustment); |
5984 |
10 Jul 20 |
nicklas |
275 |
setResult(status); |
5984 |
10 Jul 20 |
nicklas |
276 |
} |
5984 |
10 Jul 20 |
nicklas |
277 |
catch (Exception ex) |
5984 |
10 Jul 20 |
nicklas |
278 |
{ |
5984 |
10 Jul 20 |
nicklas |
279 |
setException(new RuntimeException("Could not parse 'sacct' output for job: " + jobId, ex)); |
5984 |
10 Jul 20 |
nicklas |
280 |
logger.error("Could not parse 'sacct' output for job: " + jobId, ex); |
5984 |
10 Jul 20 |
nicklas |
281 |
} |
5984 |
10 Jul 20 |
nicklas |
282 |
} |
5984 |
10 Jul 20 |
nicklas |
283 |
} |
5984 |
10 Jul 20 |
nicklas |
284 |
|
5984 |
10 Jul 20 |
nicklas |
285 |
/** |
6829 |
01 Sep 22 |
nicklas |
Implementation for getting information about a running or |
6829 |
01 Sep 22 |
nicklas |
finished job. We simply 'cat' the STATUS_FILE for the job |
6829 |
01 Sep 22 |
nicklas |
and expect useful information to be in there. |
6829 |
01 Sep 22 |
nicklas |
289 |
*/ |
6829 |
01 Sep 22 |
nicklas |
290 |
public static class StatusCmd |
6829 |
01 Sep 22 |
nicklas |
291 |
extends CmdResult<JobStatus> |
6829 |
01 Sep 22 |
nicklas |
292 |
{ |
6829 |
01 Sep 22 |
nicklas |
293 |
private final JobIdentifier jobId; |
6829 |
01 Sep 22 |
nicklas |
294 |
private final int timeAdjustment; |
6829 |
01 Sep 22 |
nicklas |
295 |
|
6829 |
01 Sep 22 |
nicklas |
296 |
public StatusCmd(String statusFile, JobIdentifier jobId, int timeAdjustment) |
6829 |
01 Sep 22 |
nicklas |
297 |
{ |
6829 |
01 Sep 22 |
nicklas |
298 |
super("cat "+statusFile); |
6829 |
01 Sep 22 |
nicklas |
299 |
this.jobId = jobId; |
6829 |
01 Sep 22 |
nicklas |
300 |
this.timeAdjustment = timeAdjustment; |
6829 |
01 Sep 22 |
nicklas |
301 |
} |
6829 |
01 Sep 22 |
nicklas |
302 |
|
6829 |
01 Sep 22 |
nicklas |
303 |
@Override |
6829 |
01 Sep 22 |
nicklas |
304 |
protected void parseResult() |
6829 |
01 Sep 22 |
nicklas |
305 |
{ |
6829 |
01 Sep 22 |
nicklas |
306 |
if (getExitStatus() != 0) return; |
6829 |
01 Sep 22 |
nicklas |
307 |
|
6829 |
01 Sep 22 |
nicklas |
308 |
if (logger.isDebugEnabled()) |
6829 |
01 Sep 22 |
nicklas |
309 |
{ |
6829 |
01 Sep 22 |
nicklas |
310 |
logger.debug("Got status information for job: " + jobId); |
6829 |
01 Sep 22 |
nicklas |
311 |
} |
6829 |
01 Sep 22 |
nicklas |
312 |
try |
6829 |
01 Sep 22 |
nicklas |
313 |
{ |
6829 |
01 Sep 22 |
nicklas |
314 |
SlurmJobStatus status = new SlurmJobStatus(jobId); |
6829 |
01 Sep 22 |
nicklas |
315 |
status.readFromStatusFile(getStdout(), timeAdjustment); |
6829 |
01 Sep 22 |
nicklas |
316 |
setResult(status); |
6829 |
01 Sep 22 |
nicklas |
317 |
} |
6829 |
01 Sep 22 |
nicklas |
318 |
catch (Exception ex) |
6829 |
01 Sep 22 |
nicklas |
319 |
{ |
6829 |
01 Sep 22 |
nicklas |
320 |
setException(new RuntimeException("Could not parse status output for job: " + jobId, ex)); |
6829 |
01 Sep 22 |
nicklas |
321 |
logger.error("Could not parse status output for job: " + jobId, ex); |
6829 |
01 Sep 22 |
nicklas |
322 |
} |
6829 |
01 Sep 22 |
nicklas |
323 |
} |
6829 |
01 Sep 22 |
nicklas |
324 |
} |
6829 |
01 Sep 22 |
nicklas |
325 |
|
6829 |
01 Sep 22 |
nicklas |
326 |
|
6829 |
01 Sep 22 |
nicklas |
327 |
/** |
5984 |
10 Jul 20 |
nicklas |
Job status information for Slurm jobs. We need the subclass to |
5984 |
10 Jul 20 |
nicklas |
be able to parse and update the status via protected setter methods. |
5984 |
10 Jul 20 |
nicklas |
330 |
*/ |
5984 |
10 Jul 20 |
nicklas |
331 |
public static class SlurmJobStatus |
5984 |
10 Jul 20 |
nicklas |
332 |
extends JobStatus |
5984 |
10 Jul 20 |
nicklas |
333 |
{ |
5984 |
10 Jul 20 |
nicklas |
334 |
|
5984 |
10 Jul 20 |
nicklas |
335 |
/** |
5984 |
10 Jul 20 |
nicklas |
The output from 'squeue' and 'sacct' have *several* ways to indicate |
5984 |
10 Jul 20 |
nicklas |
no information is available. We list all of them (that we know about) |
5984 |
10 Jul 20 |
nicklas |
and convert the actual value to 'null' when parsing. |
5984 |
10 Jul 20 |
nicklas |
339 |
*/ |
5984 |
10 Jul 20 |
nicklas |
340 |
private static final Set<String> nullVariants = |
5984 |
10 Jul 20 |
nicklas |
341 |
new HashSet<>(Arrays.asList("(null)", "N/A", "NONE", "None", "n/a", "", "Unknown")); |
5984 |
10 Jul 20 |
nicklas |
342 |
|
5984 |
10 Jul 20 |
nicklas |
343 |
/** |
5984 |
10 Jul 20 |
nicklas |
Slurm always (I hope) return dates in "yyyy-MM-ddTHH:mm:ss" format. |
5984 |
10 Jul 20 |
nicklas |
Example: 2014-03-21T08:59:09 |
5984 |
10 Jul 20 |
nicklas |
346 |
*/ |
5984 |
10 Jul 20 |
nicklas |
347 |
public static final DateFormatter SLURM_DATE = |
5984 |
10 Jul 20 |
nicklas |
348 |
new DateFormatter(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)); |
5984 |
10 Jul 20 |
nicklas |
349 |
|
5984 |
10 Jul 20 |
nicklas |
350 |
|
5984 |
10 Jul 20 |
nicklas |
351 |
public SlurmJobStatus(JobIdentifier jobId) |
5984 |
10 Jul 20 |
nicklas |
352 |
{ |
5984 |
10 Jul 20 |
nicklas |
353 |
super(jobId); |
5984 |
10 Jul 20 |
nicklas |
354 |
} |
5984 |
10 Jul 20 |
nicklas |
355 |
|
5984 |
10 Jul 20 |
nicklas |
356 |
|
5984 |
10 Jul 20 |
nicklas |
357 |
/** |
5984 |
10 Jul 20 |
nicklas |
Parse the output from 'squeue -o %all -j{id}'. |
5984 |
10 Jul 20 |
nicklas |
359 |
*/ |
5984 |
10 Jul 20 |
nicklas |
360 |
void readFromSqueueAndSacct(String text, int timeAdjustment) |
5984 |
10 Jul 20 |
nicklas |
361 |
{ |
5984 |
10 Jul 20 |
nicklas |
362 |
String jobId = getJobIdentifier().getClusterJobId(); |
5984 |
10 Jul 20 |
nicklas |
363 |
|
5984 |
10 Jul 20 |
nicklas |
364 |
String[] lines = text.split("\n"); |
5984 |
10 Jul 20 |
nicklas |
365 |
String[] header = null; |
5984 |
10 Jul 20 |
nicklas |
366 |
int idCol = 0; |
5984 |
10 Jul 20 |
nicklas |
367 |
String[] data = null; |
5984 |
10 Jul 20 |
nicklas |
368 |
|
5984 |
10 Jul 20 |
nicklas |
// Get the header line and the data line corresponding to the main job-id |
5984 |
10 Jul 20 |
nicklas |
// Note that the output contain additional lines for sub-steps (eg. from 'srun') |
5984 |
10 Jul 20 |
nicklas |
371 |
for (String line : lines) |
5984 |
10 Jul 20 |
nicklas |
372 |
{ |
5984 |
10 Jul 20 |
nicklas |
373 |
String[] cols = line.split("\\|", -1); |
5984 |
10 Jul 20 |
nicklas |
374 |
if (header == null) |
5984 |
10 Jul 20 |
nicklas |
375 |
{ |
5984 |
10 Jul 20 |
nicklas |
376 |
header = cols; |
5984 |
10 Jul 20 |
nicklas |
377 |
for (int colNo = 0; colNo < header.length; colNo++) |
5984 |
10 Jul 20 |
nicklas |
378 |
{ |
5984 |
10 Jul 20 |
nicklas |
379 |
if (header[colNo].equals("JOBID") || header[colNo].equals("JobId")) |
5984 |
10 Jul 20 |
nicklas |
380 |
{ |
5984 |
10 Jul 20 |
nicklas |
381 |
idCol = colNo; |
5984 |
10 Jul 20 |
nicklas |
382 |
break; |
5984 |
10 Jul 20 |
nicklas |
383 |
} |
5984 |
10 Jul 20 |
nicklas |
384 |
} |
5984 |
10 Jul 20 |
nicklas |
385 |
} |
5984 |
10 Jul 20 |
nicklas |
386 |
else if (jobId.equals(cols[idCol])) |
5984 |
10 Jul 20 |
nicklas |
387 |
{ |
5984 |
10 Jul 20 |
nicklas |
388 |
data = cols; |
5984 |
10 Jul 20 |
nicklas |
389 |
break; |
5984 |
10 Jul 20 |
nicklas |
390 |
} |
5984 |
10 Jul 20 |
nicklas |
391 |
} |
5984 |
10 Jul 20 |
nicklas |
392 |
|
5984 |
10 Jul 20 |
nicklas |
// If we don't find any data we throw an exception which is converted to exit status 1 in SqueueCmd.parseResult |
5984 |
10 Jul 20 |
nicklas |
394 |
if (data == null || header == null) throw new ItemNotFoundException("Job["+jobId+"]"); |
5984 |
10 Jul 20 |
nicklas |
395 |
|
5984 |
10 Jul 20 |
nicklas |
396 |
String partitionName = null; |
5984 |
10 Jul 20 |
nicklas |
397 |
String nodeName = null; |
5984 |
10 Jul 20 |
nicklas |
398 |
|
5984 |
10 Jul 20 |
nicklas |
399 |
for (int colNo = 0; colNo < Math.min(header.length, data.length); colNo++) |
5984 |
10 Jul 20 |
nicklas |
400 |
{ |
5984 |
10 Jul 20 |
nicklas |
401 |
String key = header[colNo]; |
5984 |
10 Jul 20 |
nicklas |
402 |
String value = data[colNo].trim(); |
5984 |
10 Jul 20 |
nicklas |
403 |
|
5984 |
10 Jul 20 |
nicklas |
404 |
if (nullVariants.contains(value)) continue; // Skip columns with null-like value |
5984 |
10 Jul 20 |
nicklas |
405 |
|
5984 |
10 Jul 20 |
nicklas |
406 |
if ("NAME".equals(key) || "JobName".equals(key)) |
5984 |
10 Jul 20 |
nicklas |
407 |
{ |
5984 |
10 Jul 20 |
nicklas |
408 |
setName(value); |
5984 |
10 Jul 20 |
nicklas |
409 |
} |
5984 |
10 Jul 20 |
nicklas |
410 |
else if ("SUBMIT_TIME".equals(key) || "Submit".equals(key)) |
5984 |
10 Jul 20 |
nicklas |
411 |
{ |
5984 |
10 Jul 20 |
nicklas |
412 |
setSubmissionTime(SLURM_DATE.parseString(value).getTime() + timeAdjustment * 1000); |
5984 |
10 Jul 20 |
nicklas |
413 |
} |
5984 |
10 Jul 20 |
nicklas |
414 |
else if ("START_TIME".equals(key) || "Start".equals(key)) |
5984 |
10 Jul 20 |
nicklas |
415 |
{ |
5984 |
10 Jul 20 |
nicklas |
416 |
setStartTime(SLURM_DATE.parseString(value).getTime() + timeAdjustment * 1000); |
5984 |
10 Jul 20 |
nicklas |
417 |
} |
5984 |
10 Jul 20 |
nicklas |
418 |
else if ("END_TIME".equals(key) || "End".equals(key)) |
5984 |
10 Jul 20 |
nicklas |
419 |
{ |
5984 |
10 Jul 20 |
nicklas |
420 |
setEndTime(SLURM_DATE.parseString(value).getTime() + timeAdjustment * 1000); |
5984 |
10 Jul 20 |
nicklas |
421 |
} |
5984 |
10 Jul 20 |
nicklas |
422 |
else if ("PARTITION".equals(key) || "Partition".equals(key)) |
5984 |
10 Jul 20 |
nicklas |
423 |
{ |
5984 |
10 Jul 20 |
nicklas |
424 |
partitionName = value; |
5984 |
10 Jul 20 |
nicklas |
425 |
setNodeName(partitionName + "@" + nodeName); |
5984 |
10 Jul 20 |
nicklas |
426 |
} |
5984 |
10 Jul 20 |
nicklas |
427 |
else if ("NODELIST".equals(key) || "NodeList".equals(key)) |
5984 |
10 Jul 20 |
nicklas |
428 |
{ |
5984 |
10 Jul 20 |
nicklas |
429 |
nodeName = value; |
5984 |
10 Jul 20 |
nicklas |
430 |
setNodeName(partitionName + "@" + nodeName); |
5984 |
10 Jul 20 |
nicklas |
431 |
} |
5984 |
10 Jul 20 |
nicklas |
432 |
else if ("STATE".equals(key) || "State".equals(key)) |
5984 |
10 Jul 20 |
nicklas |
433 |
{ |
5984 |
10 Jul 20 |
nicklas |
434 |
if ("PENDING".equals(value) || "CONFIGURING".equals(value)) |
5984 |
10 Jul 20 |
nicklas |
435 |
{ |
5984 |
10 Jul 20 |
nicklas |
436 |
setStatus(Job.Status.WAITING); |
5984 |
10 Jul 20 |
nicklas |
437 |
} |
5984 |
10 Jul 20 |
nicklas |
438 |
else if ("RUNNING".equals(value) || "COMPLETING".equals(value)) |
5984 |
10 Jul 20 |
nicklas |
439 |
{ |
5984 |
10 Jul 20 |
nicklas |
440 |
setStatus(Job.Status.EXECUTING); |
5984 |
10 Jul 20 |
nicklas |
441 |
} |
5984 |
10 Jul 20 |
nicklas |
442 |
else if ("FAILED".equals(value)) |
5984 |
10 Jul 20 |
nicklas |
443 |
{ |
5984 |
10 Jul 20 |
nicklas |
444 |
setStatus(Job.Status.ERROR); |
5984 |
10 Jul 20 |
nicklas |
445 |
} |
7378 |
12 Oct 23 |
nicklas |
446 |
else if (value.startsWith("CANCELLED")) |
5984 |
10 Jul 20 |
nicklas |
447 |
{ |
5984 |
10 Jul 20 |
nicklas |
448 |
setExitCode(137); // Aborted by user! |
5984 |
10 Jul 20 |
nicklas |
449 |
setStatus(Job.Status.ERROR); |
5984 |
10 Jul 20 |
nicklas |
450 |
setMessage("Aborted by user."); |
5984 |
10 Jul 20 |
nicklas |
451 |
} |
5984 |
10 Jul 20 |
nicklas |
452 |
else if ("COMPLETED".equals(value)) |
5984 |
10 Jul 20 |
nicklas |
453 |
{ |
5984 |
10 Jul 20 |
nicklas |
454 |
setStatus(Job.Status.DONE); |
5984 |
10 Jul 20 |
nicklas |
455 |
} |
5984 |
10 Jul 20 |
nicklas |
// TODO -- there are more possible values, but until we see them in real... |
5984 |
10 Jul 20 |
nicklas |
457 |
} |
5984 |
10 Jul 20 |
nicklas |
458 |
else if ("ExitCode".equals(key)) |
5984 |
10 Jul 20 |
nicklas |
459 |
{ |
5984 |
10 Jul 20 |
nicklas |
// Exit code is typically two values with ':' between (0:0) -- we use the highest one |
5984 |
10 Jul 20 |
nicklas |
461 |
String[] tmp = value.split("\\:"); |
5984 |
10 Jul 20 |
nicklas |
462 |
int exitCode = Math.max(Values.getInt(tmp[0]), Values.getInt(tmp[1])); |
5984 |
10 Jul 20 |
nicklas |
463 |
setExitCode(exitCode); |
5984 |
10 Jul 20 |
nicklas |
464 |
setStatus(exitCode == 0 ? Job.Status.DONE : Job.Status.ERROR); |
5984 |
10 Jul 20 |
nicklas |
465 |
} |
5984 |
10 Jul 20 |
nicklas |
466 |
} |
5984 |
10 Jul 20 |
nicklas |
467 |
} |
6829 |
01 Sep 22 |
nicklas |
468 |
|
6829 |
01 Sep 22 |
nicklas |
469 |
/** |
6829 |
01 Sep 22 |
nicklas |
Parse information from the STATUS_FILE. |
6829 |
01 Sep 22 |
nicklas |
471 |
*/ |
6829 |
01 Sep 22 |
nicklas |
472 |
void readFromStatusFile(String text, int timeAdjustment) |
6829 |
01 Sep 22 |
nicklas |
473 |
{ |
6829 |
01 Sep 22 |
nicklas |
474 |
String[] lines = text.split("\n"); |
6829 |
01 Sep 22 |
nicklas |
475 |
Pattern p = Pattern.compile(":"); |
6829 |
01 Sep 22 |
nicklas |
476 |
|
6829 |
01 Sep 22 |
nicklas |
477 |
boolean started = false; |
6829 |
01 Sep 22 |
nicklas |
478 |
boolean ended = false; |
6829 |
01 Sep 22 |
nicklas |
479 |
boolean aborted = false; |
6829 |
01 Sep 22 |
nicklas |
480 |
|
6829 |
01 Sep 22 |
nicklas |
481 |
String partitionName = null; |
6829 |
01 Sep 22 |
nicklas |
482 |
String nodeName = null; |
6829 |
01 Sep 22 |
nicklas |
483 |
for (String line : lines) |
6829 |
01 Sep 22 |
nicklas |
484 |
{ |
6829 |
01 Sep 22 |
nicklas |
485 |
String[] kv = p.split(line, 2); |
6829 |
01 Sep 22 |
nicklas |
486 |
if (kv.length == 2) |
6829 |
01 Sep 22 |
nicklas |
487 |
{ |
6829 |
01 Sep 22 |
nicklas |
488 |
String key = kv[0].trim(); |
6829 |
01 Sep 22 |
nicklas |
489 |
String value = kv[1].trim(); |
6829 |
01 Sep 22 |
nicklas |
490 |
|
6829 |
01 Sep 22 |
nicklas |
491 |
if ("Name".equals(key)) |
6829 |
01 Sep 22 |
nicklas |
492 |
{ |
6829 |
01 Sep 22 |
nicklas |
493 |
setName(value); |
6829 |
01 Sep 22 |
nicklas |
494 |
} |
6829 |
01 Sep 22 |
nicklas |
495 |
else if ("Partition".equals(key)) |
6829 |
01 Sep 22 |
nicklas |
496 |
{ |
6829 |
01 Sep 22 |
nicklas |
497 |
partitionName = value; |
6829 |
01 Sep 22 |
nicklas |
498 |
setNodeName(partitionName + "@" + nodeName); |
6829 |
01 Sep 22 |
nicklas |
499 |
} |
6829 |
01 Sep 22 |
nicklas |
500 |
else if ("NodeList".equals(key)) |
6829 |
01 Sep 22 |
nicklas |
501 |
{ |
6829 |
01 Sep 22 |
nicklas |
502 |
nodeName = value; |
6829 |
01 Sep 22 |
nicklas |
503 |
setNodeName(partitionName + "@" + nodeName); |
6829 |
01 Sep 22 |
nicklas |
504 |
} |
6829 |
01 Sep 22 |
nicklas |
505 |
else if ("Started".equals(key)) |
6829 |
01 Sep 22 |
nicklas |
506 |
{ |
6829 |
01 Sep 22 |
nicklas |
507 |
setStartTime(ClusterConfig.DATE_CMD.parseString(value).getTime() + timeAdjustment * 1000); |
6829 |
01 Sep 22 |
nicklas |
508 |
started = true; |
6829 |
01 Sep 22 |
nicklas |
509 |
} |
6829 |
01 Sep 22 |
nicklas |
510 |
else if ("Ended".equals(key)) |
6829 |
01 Sep 22 |
nicklas |
511 |
{ |
6829 |
01 Sep 22 |
nicklas |
512 |
setEndTime(ClusterConfig.DATE_CMD.parseString(value).getTime() + timeAdjustment * 1000); |
6829 |
01 Sep 22 |
nicklas |
513 |
ended = true; |
6829 |
01 Sep 22 |
nicklas |
514 |
} |
6829 |
01 Sep 22 |
nicklas |
515 |
else if ("ExitCode".equals(key)) |
6829 |
01 Sep 22 |
nicklas |
516 |
{ |
6829 |
01 Sep 22 |
nicklas |
517 |
if (!aborted) setExitCode(Values.getInt(value)); |
6829 |
01 Sep 22 |
nicklas |
518 |
} |
6829 |
01 Sep 22 |
nicklas |
519 |
else if ("Aborted".equals(key)) |
6829 |
01 Sep 22 |
nicklas |
520 |
{ |
6829 |
01 Sep 22 |
nicklas |
521 |
setExitCode(137); // Aborted by user! |
6829 |
01 Sep 22 |
nicklas |
522 |
setStatus(Job.Status.ERROR); |
6829 |
01 Sep 22 |
nicklas |
523 |
setMessage("Aborted by user."); |
6829 |
01 Sep 22 |
nicklas |
524 |
aborted = true; |
6829 |
01 Sep 22 |
nicklas |
525 |
if (!ended) |
6829 |
01 Sep 22 |
nicklas |
526 |
{ |
6829 |
01 Sep 22 |
nicklas |
527 |
ended = true; |
6829 |
01 Sep 22 |
nicklas |
528 |
setEndTime(System.currentTimeMillis()); |
6829 |
01 Sep 22 |
nicklas |
529 |
} |
6829 |
01 Sep 22 |
nicklas |
530 |
} |
6829 |
01 Sep 22 |
nicklas |
531 |
} |
6829 |
01 Sep 22 |
nicklas |
532 |
} |
6829 |
01 Sep 22 |
nicklas |
533 |
|
6829 |
01 Sep 22 |
nicklas |
534 |
if (ended) |
6829 |
01 Sep 22 |
nicklas |
535 |
{ |
6829 |
01 Sep 22 |
nicklas |
536 |
setStatus(getExitCode() == 0 ? Job.Status.DONE : Job.Status.ERROR); |
6829 |
01 Sep 22 |
nicklas |
537 |
} |
6829 |
01 Sep 22 |
nicklas |
538 |
else if (started) |
6829 |
01 Sep 22 |
nicklas |
539 |
{ |
6829 |
01 Sep 22 |
nicklas |
540 |
setStatus(Job.Status.EXECUTING); |
6829 |
01 Sep 22 |
nicklas |
541 |
} |
6829 |
01 Sep 22 |
nicklas |
542 |
else |
6829 |
01 Sep 22 |
nicklas |
543 |
{ |
6829 |
01 Sep 22 |
nicklas |
544 |
setStatus(Job.Status.WAITING); |
6829 |
01 Sep 22 |
nicklas |
545 |
} |
6829 |
01 Sep 22 |
nicklas |
546 |
} |
5984 |
10 Jul 20 |
nicklas |
547 |
} |
5984 |
10 Jul 20 |
nicklas |
548 |
|
5981 |
07 Jul 20 |
nicklas |
549 |
} |