Skip to content

Commit a1d8166

Browse files
committed
fix(workers): make hardcoded run-workers overall timeout configurable
The `run-workers` command has had a hardcoded 10-minute overall timeout since #5370 ("fix workers crash"). When the timeout fires, ALL still-running workers are force-terminated and only results from naturally-finished workers make it into the aggregated summary. For suites whose total wall-clock with N workers is in the same ballpark as 10 minutes, this guarantees a portion of tests will be silently dropped from the report — even on a healthy run. There is currently no CLI flag, env var, or config option to override. Make the timeout configurable, with `600000` ms (10 min) preserved as default for backward compatibility: 1. `CODECEPT_WORKERS_TIMEOUT` env var (ms) — highest precedence 2. `workersTimeout` field in codecept.conf (ms) 3. Default `600000` Setting the value to `0` (or any non-positive number) disables the timeout entirely. Adds 6 unit tests covering resolution order, env-over-config precedence, disabled mode, and non-numeric fallback.
1 parent 6f791dc commit a1d8166

3 files changed

Lines changed: 96 additions & 13 deletions

File tree

lib/workers.js

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,26 @@ class Workers extends EventEmitter {
501501
return runHook(this.codecept.config.teardownAll, 'teardownAll')
502502
}
503503

504+
/**
505+
* Resolves the overall `run-workers` timeout in milliseconds.
506+
*
507+
* Resolution order:
508+
* 1. `CODECEPT_WORKERS_TIMEOUT` env var (numeric, ms)
509+
* 2. `workersTimeout` in codecept.conf (ms)
510+
* 3. Default: 600000 (10 minutes)
511+
*
512+
* Set the value to `0` (or any non-positive number) to disable the timeout entirely.
513+
*
514+
* @returns {number} timeout in milliseconds
515+
*/
516+
_getWorkersTimeoutMs() {
517+
const envTimeout = parseInt(process.env.CODECEPT_WORKERS_TIMEOUT, 10)
518+
if (Number.isFinite(envTimeout)) return envTimeout
519+
const configTimeout = this.codecept?.config?.workersTimeout
520+
if (Number.isFinite(configTimeout)) return configTimeout
521+
return 600000
522+
}
523+
504524
async run() {
505525
await this._ensureInitialized()
506526
recorder.startUnlessRunning()
@@ -521,22 +541,27 @@ class Workers extends EventEmitter {
521541
// Workers are already running, this is just a placeholder step
522542
})
523543

524-
// Add overall timeout to prevent infinite hanging
525-
const overallTimeout = setTimeout(() => {
526-
console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...')
527-
workerThreads.forEach(w => {
528-
try {
529-
w.terminate()
530-
} catch (e) {
531-
// ignore
532-
}
533-
})
534-
this._finishRun()
535-
}, 600000) // 10 minutes
544+
// Overall timeout to prevent infinite hanging. See _getWorkersTimeoutMs() for resolution rules.
545+
const overallTimeoutMs = this._getWorkersTimeoutMs()
546+
let overallTimeout
547+
if (overallTimeoutMs > 0) {
548+
overallTimeout = setTimeout(() => {
549+
const minutes = (overallTimeoutMs / 60000).toFixed(1)
550+
console.error(`[Main] Overall timeout reached (${minutes} minutes). Force terminating remaining workers...`)
551+
workerThreads.forEach(w => {
552+
try {
553+
w.terminate()
554+
} catch (e) {
555+
// ignore
556+
}
557+
})
558+
this._finishRun()
559+
}, overallTimeoutMs)
560+
}
536561

537562
return new Promise(resolve => {
538563
this.on('end', () => {
539-
clearTimeout(overallTimeout)
564+
if (overallTimeout) clearTimeout(overallTimeout)
540565
resolve()
541566
})
542567
})

test/unit/worker_test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,4 +382,55 @@ describe('Workers', function () {
382382

383383
workers.run()
384384
})
385+
386+
describe('_getWorkersTimeoutMs', () => {
387+
let savedEnv
388+
let workers
389+
390+
beforeEach(() => {
391+
savedEnv = process.env.CODECEPT_WORKERS_TIMEOUT
392+
delete process.env.CODECEPT_WORKERS_TIMEOUT
393+
workers = new Workers(1, { by: 'test', testConfig: './test/data/sandbox/codecept.workers.conf.js' })
394+
// Stub the codecept config so we don't need to await initialization for this pure-logic test
395+
workers.codecept = { config: {} }
396+
})
397+
398+
afterEach(() => {
399+
if (savedEnv === undefined) {
400+
delete process.env.CODECEPT_WORKERS_TIMEOUT
401+
} else {
402+
process.env.CODECEPT_WORKERS_TIMEOUT = savedEnv
403+
}
404+
})
405+
406+
it('returns default 10 minutes when nothing is configured', () => {
407+
expect(workers._getWorkersTimeoutMs()).to.equal(600000)
408+
})
409+
410+
it('uses CODECEPT_WORKERS_TIMEOUT env var when set', () => {
411+
process.env.CODECEPT_WORKERS_TIMEOUT = '90000'
412+
expect(workers._getWorkersTimeoutMs()).to.equal(90000)
413+
})
414+
415+
it('uses workersTimeout from config when env is not set', () => {
416+
workers.codecept.config.workersTimeout = 120000
417+
expect(workers._getWorkersTimeoutMs()).to.equal(120000)
418+
})
419+
420+
it('env var takes precedence over config', () => {
421+
process.env.CODECEPT_WORKERS_TIMEOUT = '15000'
422+
workers.codecept.config.workersTimeout = 999999
423+
expect(workers._getWorkersTimeoutMs()).to.equal(15000)
424+
})
425+
426+
it('returns 0 when env var is "0" so the timeout can be disabled in run()', () => {
427+
process.env.CODECEPT_WORKERS_TIMEOUT = '0'
428+
expect(workers._getWorkersTimeoutMs()).to.equal(0)
429+
})
430+
431+
it('falls back to default when env var is non-numeric', () => {
432+
process.env.CODECEPT_WORKERS_TIMEOUT = 'not-a-number'
433+
expect(workers._getWorkersTimeoutMs()).to.equal(600000)
434+
})
435+
})
385436
})

typings/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,13 @@ declare namespace CodeceptJS {
360360
*/
361361
teardownAll?: (() => Promise<void>) | boolean | string
362362

363+
/**
364+
* Overall timeout for `run-workers` (milliseconds). After this time, any worker
365+
* still running is force-terminated and the run finishes. Default: `600000` (10 min).
366+
* Set to `0` to disable. Can also be overridden via `CODECEPT_WORKERS_TIMEOUT` env var.
367+
*/
368+
workersTimeout?: number
369+
363370
/** Enable [localized test commands](https://codecept.io/translation/) */
364371
translation?: string
365372

0 commit comments

Comments
 (0)