Skip to content

Commit 200dadb

Browse files
authored
fix(pool): handle worker start failures gracefully (#9337)
1 parent 245542b commit 200dadb

File tree

3 files changed

+60
-15
lines changed

3 files changed

+60
-15
lines changed

packages/vitest/src/node/pools/pool.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Span } from '@opentelemetry/api'
12
import type { ContextTestEnvironment } from '../../types/worker'
23
import type { Logger } from '../logger'
34
import type { StateManager } from '../state'
@@ -118,21 +119,27 @@ export class Pool {
118119
WORKER_START_TIMEOUT,
119120
)
120121

121-
await runner.start({ workerId: task.context.workerId }).finally(() => clearTimeout(id))
122+
await runner.start({ workerId: task.context.workerId })
123+
.catch(error =>
124+
resolver.reject(
125+
new Error(`[vitest-pool]: Failed to start ${task.worker} worker for test files ${formatFiles(task)}.`, { cause: error }),
126+
),
127+
)
128+
.finally(() => clearTimeout(id))
122129
}
123130

124-
const span = runner.startTracesSpan(`vitest.worker.${method}`)
125-
// Start running the test in the worker
126-
runner.request(method, task.context)
131+
let span: Span | undefined
132+
133+
if (!resolver.isRejected) {
134+
span = runner.startTracesSpan(`vitest.worker.${method}`)
135+
136+
// Start running the test in the worker
137+
runner.request(method, task.context)
138+
}
127139

128140
await resolver.promise
129-
.catch((error) => {
130-
span.recordException(error)
131-
throw error
132-
})
133-
.finally(() => {
134-
span.end()
135-
})
141+
.catch(error => span?.recordException(error))
142+
.finally(() => span?.end())
136143

137144
const index = this.activeTasks.indexOf(activeTask)
138145
if (index !== -1) {
@@ -158,7 +165,7 @@ export class Pool {
158165
)
159166

160167
this.exitPromises.push(
161-
runner.stop()
168+
runner.stop({ force: resolver.isRejected })
162169
.then(() => clearTimeout(id))
163170
.catch(error => this.logger.error(`[vitest-pool]: Failed to terminate ${task.worker} worker for test files ${formatFiles(task)}.`, error)),
164171
)
@@ -281,7 +288,17 @@ function withResolvers() {
281288
reject = rej
282289
})
283290

284-
return { resolve, reject, promise }
291+
const resolver = {
292+
promise,
293+
resolve,
294+
reject: (reason: unknown) => {
295+
resolver.isRejected = true
296+
reject(reason)
297+
},
298+
isRejected: false,
299+
}
300+
301+
return resolver
285302
}
286303

287304
function formatFiles(task: PoolTask) {

packages/vitest/src/node/pools/poolRunner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum RunnerState {
1515
IDLE = 'idle',
1616
STARTING = 'starting',
1717
STARTED = 'started',
18+
START_FAILURE = 'start_failure',
1819
STOPPING = 'stopping',
1920
STOPPED = 'stopped',
2021
}
@@ -231,7 +232,7 @@ export class PoolRunner {
231232
this._state = RunnerState.STARTED
232233
}
233234
catch (error: any) {
234-
this._state = RunnerState.IDLE
235+
this._state = RunnerState.START_FAILURE
235236
startSpan?.recordException(error)
236237
throw error
237238
}

test/config/test/pool.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { SerializedConfig } from 'vitest'
22
import type { TestUserConfig } from 'vitest/node'
33
import { normalize } from 'pathe'
4-
import { assert, describe, expect, test } from 'vitest'
4+
import { assert, describe, expect, test, vi } from 'vitest'
55
import { runVitest, StableTestFileOrderSorter } from '../../test-utils'
66

77
describe.each(['forks', 'threads', 'vmThreads', 'vmForks'])('%s', async (pool) => {
@@ -102,6 +102,33 @@ test('non-isolated happy-dom worker pool receives all testfiles at once', async
102102
`)
103103
})
104104

105+
test('worker start failure should not hang', async () => {
106+
const stop = vi.fn()
107+
108+
const { stdout, stderr } = await runVitest({
109+
root: './fixtures/pool',
110+
include: ['a.test.ts'],
111+
pool: {
112+
name: 'pool-with-crashing-workers',
113+
// @ts-expect-error -- intentional
114+
createPoolWorker: () => ({
115+
start: () => Promise.reject(new Error('Mock')),
116+
stop,
117+
on() {},
118+
off() {},
119+
send() {},
120+
}),
121+
},
122+
})
123+
124+
expect(stderr).toContain('Error: [vitest-pool]: Failed to start pool-with-crashing-workers worker for test files')
125+
expect(stderr).toContain('a.test.ts')
126+
expect(stderr).toContain('Caused by: Error: Mock')
127+
expect(stdout).toContain('Errors 1 error')
128+
129+
expect(stop).toHaveBeenCalled()
130+
})
131+
105132
async function getConfig<T = SerializedConfig>(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}): Promise<T> {
106133
let config: T | undefined
107134

0 commit comments

Comments
 (0)