@@ -32,6 +32,7 @@ import dev.failsafe.function.CheckedSupplier
3232import groovy.transform.CompileStatic
3333import groovy.transform.Memoized
3434import groovy.util.logging.Slf4j
35+ import nextflow.Global
3536import nextflow.exception.ProcessException
3637import nextflow.exception.ProcessFailedException
3738import nextflow.exception.ProcessNonZeroExitStatusException
@@ -83,7 +84,9 @@ class GridTaskHandler extends TaskHandler implements FusionAwareTask {
8384 BatchCleanup batch
8485
8586 @TestOnly
86- protected GridTaskHandler () {}
87+ protected GridTaskHandler () {
88+ this . exitAwaiter = new ExitStatusAwaiter (Duration . of(' 270sec' ))
89+ }
8790
8891 GridTaskHandler ( TaskRun task , AbstractGridExecutor executor ) {
8992 super (task)
@@ -96,6 +99,7 @@ class GridTaskHandler extends TaskHandler implements FusionAwareTask {
9699 this . wrapperFile = task. workDir. resolve(TaskRun . CMD_RUN )
97100 final duration = executor. config. getExitReadTimeout(executor. name)
98101 this . exitStatusReadTimeoutMillis = duration. toMillis()
102+ this . exitAwaiter = new ExitStatusAwaiter (duration)
99103 this . queue = task. config?. queue
100104 this . sanityCheckInterval = duration
101105 }
@@ -300,9 +304,7 @@ class GridTaskHandler extends TaskHandler implements FusionAwareTask {
300304
301305 private long exitTimestampMillis0 = System . currentTimeMillis()
302306
303- private long exitTimestampMillis1
304-
305- private long exitTimestampMillis2
307+ private ExitStatusAwaiter exitAwaiter
306308
307309 /**
308310 * When a process terminated save its exit status into the file defined by #exitFile
@@ -312,17 +314,6 @@ class GridTaskHandler extends TaskHandler implements FusionAwareTask {
312314 */
313315 protected Integer readExitStatus () {
314316
315- String workDirList = null
316- if ( exitTimestampMillis1 && FileHelper . workDirIsSharedFS ) {
317- /*
318- * When the file is in a NFS folder in order to avoid false negative
319- * list the content of the parent path to force refresh of NFS metadata
320- * http://stackoverflow.com/questions/3833127/alternative-to-file-exists-in-java
321- * http://superuser.com/questions/422061/how-to-determine-whether-a-directory-is-on-an-nfs-mounted-drive
322- */
323- workDirList = FileHelper . listDirectory(task. workDir)
324- }
325-
326317 /*
327318 * when the file does not exist return null, to force the monitor to continue to wait
328319 */
@@ -334,86 +325,52 @@ class GridTaskHandler extends TaskHandler implements FusionAwareTask {
334325 else
335326 log. trace " JobId `$jobId ` exit file: ${ exitFile.toUriString()} - lastModified: ${ exitAttrs?.lastModifiedTime()} - size: ${ exitAttrs?.size()} "
336327 }
337- // -- fetch the job status before return a result
338- final active = executor. checkActiveStatus(jobId, queue)
339328
340- // --
329+ // -- grace period: don't query the scheduler until enough time has elapsed since job submission
341330 def elapsed = System . currentTimeMillis() - startedMillis
342- if ( elapsed < executor. queueInterval. toMillis() * 2.5 ) {
331+ if ( executor. queueInterval && elapsed < executor. queueInterval. toMillis() * 2.5 ) {
332+ exitAwaiter. reset()
343333 return null
344334 }
345335
346- // -- if the job is active, this means that it is still running and thus the exit file cannot exist
347- // returns null to continue to wait
348- if ( active ) {
349- // make sure to reset exit time if the task is active -- see #927
350- exitTimestampMillis1 = 0
336+ // -- fetch the job status before returning a result
337+ // -- if the job is active, it is still running and the exit file absence is expected
338+ if ( executor . checkActiveStatus(jobId, queue) ) {
339+ // make sure to reset the awaiter if the task is still active -- see #927
340+ exitAwaiter . reset()
351341 return null
352342 }
353343
354- // -- if the job is not active, something is going wrong
355- // * before returning an error code make (due to NFS latency) the file status could be in a incoherent state
356- if ( ! exitTimestampMillis1 ) {
357- log. trace " Exit file does not exist for and the job is not running for task: $this -- Try to wait before kill it"
358- exitTimestampMillis1 = System . currentTimeMillis()
344+ // -- job is not active and exit file is missing: force NFS metadata refresh and delegate
345+ // timeout tracking to the shared awaiter (mirrors ExitStatusAwaiter two-phase logic)
346+ String workDirList = null
347+ if ( Global . session && FileHelper . workDirIsSharedFS ) {
348+ /*
349+ * When the file is in a NFS folder, list the parent path to force refresh of NFS metadata
350+ * before the awaiter re-reads the attributes, avoiding false-negative cache hits.
351+ * http://stackoverflow.com/questions/3833127/alternative-to-file-exists-in-java
352+ * http://superuser.com/questions/422061/how-to-determine-whether-a-directory-is-on-an-nfs-mounted-drive
353+ */
354+ workDirList = FileHelper . listDirectory(task. workDir)
359355 }
360356
361- def delta = System . currentTimeMillis() - exitTimestampMillis1
362- if ( delta < exitStatusReadTimeoutMillis ) {
363- return null
357+ final result = exitAwaiter. read(exitFile)
358+ if ( result == Integer . MAX_VALUE ) {
359+ def errMessage = []
360+ errMessage << " Failed to get exit status for process ${ this} -- exitStatusReadTimeoutMillis: $exitStatusReadTimeoutMillis "
361+ errMessage << " Current queue status:"
362+ errMessage << executor. dumpQueueStatus()?. indent(' > ' )
363+ errMessage << " Content of workDir: ${ task.workDir} "
364+ errMessage << workDirList?. indent(' > ' )
365+ log. debug errMessage. join(' \n ' )
364366 }
365-
366- def errMessage = []
367- errMessage << " Failed to get exit status for process ${ this} -- exitStatusReadTimeoutMillis: $exitStatusReadTimeoutMillis ; delta: $delta "
368- // -- dump current queue stats
369- errMessage << " Current queue status:"
370- errMessage << executor. dumpQueueStatus()?. indent(' > ' )
371- // -- dump directory listing
372- errMessage << " Content of workDir: ${ task.workDir} "
373- errMessage << workDirList?. indent(' > ' )
374- log. debug errMessage. join(' \n ' )
375-
376- return Integer . MAX_VALUE
367+ return result
377368 }
378369
379370 /*
380- * read the exit file, it should contain the executed process exit status
371+ * file is present: delegate content parsing and empty-file retry to the shared awaiter
381372 */
382- def status = exitFile. text?. trim()
383- if ( status ) {
384- try {
385- return status. toInteger()
386- }
387- catch ( Exception e ) {
388- log. warn " Unable to parse process exit file: ${ exitFile.toUriString()} -- bad value: '$status '"
389- return Integer . MAX_VALUE
390- }
391- }
392-
393- else {
394- /*
395- * Since working with NFS it may happen that the file exists BUT it is empty due to network latencies,
396- * before returning an invalid exit code, wait some seconds.
397- *
398- * More in detail:
399- * 1) the very first time that arrive here initialize the 'exitTimestampMillis' to the current timestamp
400- * 2) when the file is empty but less than 5 seconds are spent from the first check, return null
401- * this will force the monitor to continue to wait for job termination
402- * 3) if more than 5 seconds are spent, and the file is empty return MAX_INT as an invalid exit status
403- *
404- */
405- if ( ! exitTimestampMillis2 ) {
406- log. debug " File is returning empty content: $this -- Try to wait a while... and pray."
407- exitTimestampMillis2 = System . currentTimeMillis()
408- }
409-
410- def delta = System . currentTimeMillis() - exitTimestampMillis2
411- if ( delta < exitStatusReadTimeoutMillis ) {
412- return null
413- }
414- log. warn " Unable to read command status from: ${ exitFile.toUriString()} after $delta ms"
415- return -1
416- }
373+ return exitAwaiter. read(exitFile)
417374 }
418375
419376 @Override
0 commit comments