Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions lib/workers.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { createRuns } from './command/run-multiple/collection.js'

const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js')

const WORKER_TIMEOUT_MINUTES = 10

const initializeCodecept = async (configPath, options = {}) => {
const config = await mainConfig.load(configPath || '.')
const codecept = new Codecept(config, { ...options, skipDefaultListeners: true })
Expand Down Expand Up @@ -501,6 +503,26 @@ class Workers extends EventEmitter {
return runHook(this.codecept.config.teardownAll, 'teardownAll')
}

/**
* Resolves the overall `run-workers` timeout in milliseconds.
*
* Resolution order:
* 1. `CODECEPT_WORKERS_TIMEOUT` env var (numeric, ms)
* 2. `workersTimeout` in codecept.conf (ms)
* 3. Default: 600000 (10 minutes)
*
* Set the value to `0` (or any non-positive number) to disable the timeout entirely.
*
* @returns {number} timeout in milliseconds
*/
_getWorkersTimeoutMs() {
const envTimeout = parseInt(process.env.CODECEPT_WORKERS_TIMEOUT, 10)
if (Number.isFinite(envTimeout)) return envTimeout
const configTimeout = this.codecept?.config?.workersTimeout
if (Number.isFinite(configTimeout)) return configTimeout
return WORKER_TIMEOUT_MINUTES * 60 * 1000
}

async run() {
await this._ensureInitialized()
recorder.startUnlessRunning()
Expand All @@ -521,22 +543,27 @@ class Workers extends EventEmitter {
// Workers are already running, this is just a placeholder step
})

// Add overall timeout to prevent infinite hanging
const overallTimeout = setTimeout(() => {
console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...')
workerThreads.forEach(w => {
try {
w.terminate()
} catch (e) {
// ignore
}
})
this._finishRun()
}, 600000) // 10 minutes
// Overall timeout to prevent infinite hanging. See _getWorkersTimeoutMs() for resolution rules.
const overallTimeoutMs = this._getWorkersTimeoutMs()
let overallTimeout
if (overallTimeoutMs > 0) {
overallTimeout = setTimeout(() => {
const minutes = (overallTimeoutMs / 60000).toFixed(1)
console.error(`[Main] Overall timeout reached (${minutes} minutes). Force terminating remaining workers...`)
workerThreads.forEach(w => {
try {
w.terminate()
} catch (e) {
// ignore
}
})
this._finishRun()
}, overallTimeoutMs)
}

return new Promise(resolve => {
this.on('end', () => {
clearTimeout(overallTimeout)
if (overallTimeout) clearTimeout(overallTimeout)
resolve()
})
})
Expand Down
51 changes: 51 additions & 0 deletions test/unit/worker_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,55 @@ describe('Workers', function () {

workers.run()
})

describe('_getWorkersTimeoutMs', () => {
let savedEnv
let workers

beforeEach(() => {
savedEnv = process.env.CODECEPT_WORKERS_TIMEOUT
delete process.env.CODECEPT_WORKERS_TIMEOUT
workers = new Workers(1, { by: 'test', testConfig: './test/data/sandbox/codecept.workers.conf.js' })
// Stub the codecept config so we don't need to await initialization for this pure-logic test
workers.codecept = { config: {} }
})

afterEach(() => {
if (savedEnv === undefined) {
delete process.env.CODECEPT_WORKERS_TIMEOUT
} else {
process.env.CODECEPT_WORKERS_TIMEOUT = savedEnv
}
})

it('returns default 10 minutes when nothing is configured', () => {
expect(workers._getWorkersTimeoutMs()).to.equal(600000)
})

it('uses CODECEPT_WORKERS_TIMEOUT env var when set', () => {
process.env.CODECEPT_WORKERS_TIMEOUT = '90000'
expect(workers._getWorkersTimeoutMs()).to.equal(90000)
})

it('uses workersTimeout from config when env is not set', () => {
workers.codecept.config.workersTimeout = 120000
expect(workers._getWorkersTimeoutMs()).to.equal(120000)
})

it('env var takes precedence over config', () => {
process.env.CODECEPT_WORKERS_TIMEOUT = '15000'
workers.codecept.config.workersTimeout = 999999
expect(workers._getWorkersTimeoutMs()).to.equal(15000)
})

it('returns 0 when env var is "0" so the timeout can be disabled in run()', () => {
process.env.CODECEPT_WORKERS_TIMEOUT = '0'
expect(workers._getWorkersTimeoutMs()).to.equal(0)
})

it('falls back to default when env var is non-numeric', () => {
process.env.CODECEPT_WORKERS_TIMEOUT = 'not-a-number'
expect(workers._getWorkersTimeoutMs()).to.equal(600000)
})
})
})
7 changes: 7 additions & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,13 @@ declare namespace CodeceptJS {
*/
teardownAll?: (() => Promise<void>) | boolean | string

/**
* Overall timeout for `run-workers` (milliseconds). After this time, any worker
* still running is force-terminated and the run finishes. Default: `600000` (10 min).
* Set to `0` to disable. Can also be overridden via `CODECEPT_WORKERS_TIMEOUT` env var.
*/
workersTimeout?: number

/** Enable [localized test commands](https://codecept.io/translation/) */
translation?: string

Expand Down