extensions/net.sf.basedb.opengrid/trunk/src/net/sf/basedb/opengrid/engine/SlurmEngine.java

Code
Comments
Other
Rev Date Author Line
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 34   Cluster engine implementation for Slurm clusters.
5981 07 Jul 20 nicklas 35   @author nicklas
5981 07 Jul 20 nicklas 36   @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 45   // All sbatch options that are automatically set by 
5990 19 Aug 20 nicklas 46   // 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 75     Generates a script that can be submitted with 'sbatch' to slurm. This 
5982 07 Jul 20 nicklas 76     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 115     // Note! the initial '.' in the above command is important since it causes the 'run.sh' script
6831 02 Sep 22 nicklas 116     // to be executed in the same process as 'batch.sh', and this affects the behaviour when 
6831 02 Sep 22 nicklas 117     // manually aborting a job with 'scancel'. The 'trap' commands in 'run.sh' will only work 
6831 02 Sep 22 nicklas 118     // 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 123     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 135     Get the path to where the status information for a job is saved.
6829 01 Sep 22 nicklas 136     @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 144     Check the flag if 'sacct' is disabled on the cluster.
6829 01 Sep 22 nicklas 145     @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 187     Implements the 'squeue' command for getting information about a waiting 
5984 10 Jul 20 nicklas 188     or running job.
5984 10 Jul 20 nicklas 189     
5984 10 Jul 20 nicklas 190     We need to use '-o %all' to get output with '|' as separator. It is not 
5984 10 Jul 20 nicklas 191     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 223         // Thrown when the 'squeue' command completes but has no data for requested job
5984 10 Jul 20 nicklas 224         // Typically happens just after the job has completed -- information should be
5984 10 Jul 20 nicklas 225         // 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 237     Implements the 'sacct' command for getting information about a completed job.
5984 10 Jul 20 nicklas 238     
5984 10 Jul 20 nicklas 239     The -P option generates output separated by '|' and we can specify the columns
5984 10 Jul 20 nicklas 240     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 247       Data columns we want to return from 'sacct'. Note that order is important
5984 10 Jul 20 nicklas 248       in some cases. For example we need 'ExitCode' before 'State' since 'State' will
5984 10 Jul 20 nicklas 249       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 286     Implementation for getting information about a running or
6829 01 Sep 22 nicklas 287     finished job. We simply 'cat' the STATUS_FILE for the job
6829 01 Sep 22 nicklas 288     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 328     Job status information for Slurm jobs. We need the subclass to
5984 10 Jul 20 nicklas 329     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 336       The output from 'squeue' and 'sacct' have *several* ways to indicate 
5984 10 Jul 20 nicklas 337       no information is available. We list all of them (that we know about) 
5984 10 Jul 20 nicklas 338       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 344       Slurm always (I hope) return dates in "yyyy-MM-ddTHH:mm:ss" format.
5984 10 Jul 20 nicklas 345       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 358       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 369       // Get the header line and the data line corresponding to the main job-id
5984 10 Jul 20 nicklas 370       // 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 393       // 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 456           // 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 460           // 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 470       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 }