Skip to content

Commit 0c9737e

Browse files
authored
Fix retries and error notification in workers (#8079)
* Add tests that show that retries are broken in jest-worker * Re-send requests to workers after they're re-created to retry * Refactor request handling in workers * Add changelog entry * Fix lint issues * Improve e2e test
1 parent b0cbcbf commit 0c9737e

File tree

6 files changed

+124
-4
lines changed

6 files changed

+124
-4
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixes
66

77
- `[jest-cli]` export functions compatible with `import {default}` ([#8080](https://github.com/facebook/jest/pull/8080))
8+
- `[jest-worker]`: Fix retries and error notification in workers ([#8079](https://github.com/facebook/jest/pull/8079))
89

910
### Chore & Maintenance
1011

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import path from 'path';
9+
import os from 'os';
10+
import {cleanup, writeFiles} from '../Utils';
11+
import runJest from '../runJest';
12+
13+
const DIR = path.resolve(os.tmpdir(), 'fatal-worker-error');
14+
15+
beforeEach(() => cleanup(DIR));
16+
afterAll(() => cleanup(DIR));
17+
18+
const NUMBER_OF_TESTS_TO_FORCE_USING_WORKERS = 25;
19+
20+
test('fails a test that terminates the worker with a fatal error', () => {
21+
const testFiles = {
22+
'__tests__/fatalWorkerError.test.js': `
23+
test('fatal worker error', () => {
24+
process.exit(134);
25+
});
26+
`,
27+
};
28+
29+
for (let i = 0; i <= NUMBER_OF_TESTS_TO_FORCE_USING_WORKERS; i++) {
30+
testFiles[`__tests__/test${i}.test.js`] = `
31+
test('test ${i}', () => {});
32+
`;
33+
}
34+
35+
writeFiles(DIR, {
36+
...testFiles,
37+
'package.json': '{}',
38+
});
39+
40+
const {status, stderr} = runJest(DIR, ['--maxWorkers=2']);
41+
42+
const numberOfTestsPassed = (stderr.match(/\bPASS\b/g) || []).length;
43+
44+
expect(status).not.toBe(0);
45+
expect(numberOfTestsPassed).toBe(Object.keys(testFiles).length - 1);
46+
expect(stderr).toContain('FAIL __tests__/fatalWorkerError.test.js');
47+
expect(stderr).toContain('Call retries were exceeded');
48+
});

packages/jest-worker/src/workers/ChildProcessWorker.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@ export default class ChildProcessWorker implements WorkerInterface {
4343
private _child!: ChildProcess;
4444
private _options: WorkerOptions;
4545
private _onProcessEnd!: OnEnd;
46+
private _request: ChildMessage | null;
4647
private _retries!: number;
4748

4849
constructor(options: WorkerOptions) {
4950
this._options = options;
51+
this._request = null;
52+
5053
this.initialize();
5154
}
5255

@@ -142,13 +145,23 @@ export default class ChildProcessWorker implements WorkerInterface {
142145
onExit(exitCode: number) {
143146
if (exitCode !== 0) {
144147
this.initialize();
148+
149+
if (this._request) {
150+
this._child.send(this._request);
151+
}
145152
}
146153
}
147154

148155
send(request: ChildMessage, onProcessStart: OnStart, onProcessEnd: OnEnd) {
149156
onProcessStart(this);
150-
this._onProcessEnd = onProcessEnd;
151-
157+
this._onProcessEnd = (...args) => {
158+
// Clean the request to avoid sending past requests to workers that fail
159+
// while waiting for a new request (timers, unhandled rejections...)
160+
this._request = null;
161+
return onProcessEnd(...args);
162+
};
163+
164+
this._request = request;
152165
this._retries = 0;
153166
this._child.send(request);
154167
}

packages/jest-worker/src/workers/NodeThreadsWorker.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ export default class ExperimentalWorker implements WorkerInterface {
2727
private _worker!: Worker;
2828
private _options: WorkerOptions;
2929
private _onProcessEnd!: OnEnd;
30+
private _request: ChildMessage | null;
3031
private _retries!: number;
3132

3233
constructor(options: WorkerOptions) {
3334
this._options = options;
35+
this._request = null;
3436
this.initialize();
3537
}
3638

@@ -126,13 +128,23 @@ export default class ExperimentalWorker implements WorkerInterface {
126128
onExit(exitCode: number) {
127129
if (exitCode !== 0) {
128130
this.initialize();
131+
132+
if (this._request) {
133+
this._worker.postMessage(this._request);
134+
}
129135
}
130136
}
131137

132138
send(request: ChildMessage, onProcessStart: OnStart, onProcessEnd: OnEnd) {
133139
onProcessStart(this);
134-
this._onProcessEnd = onProcessEnd;
135-
140+
this._onProcessEnd = (...args) => {
141+
// Clean the request to avoid sending past requests to workers that fail
142+
// while waiting for a new request (timers, unhandled rejections...)
143+
this._request = null;
144+
return onProcessEnd(...args);
145+
};
146+
147+
this._request = request;
136148
this._retries = 0;
137149

138150
this._worker.postMessage(request);

packages/jest-worker/src/workers/__tests__/ChildProcessWorker.test.js

+23
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,29 @@ it('sends the task to the child process', () => {
151151
expect(forkInterface.send.mock.calls[1][0]).toEqual(request);
152152
});
153153

154+
it('resends the task to the child process after a retry', () => {
155+
const worker = new Worker({
156+
forkOptions: {},
157+
maxRetries: 3,
158+
workerPath: '/tmp/foo/bar/baz.js',
159+
});
160+
161+
const request = [CHILD_MESSAGE_CALL, false, 'foo', []];
162+
163+
worker.send(request, () => {}, () => {});
164+
165+
// Skipping call "0" because it corresponds to the "initialize" one.
166+
expect(forkInterface.send.mock.calls[1][0]).toEqual(request);
167+
168+
const previousForkInterface = forkInterface;
169+
forkInterface.emit('exit');
170+
171+
expect(forkInterface).not.toBe(previousForkInterface);
172+
173+
// Skipping call "0" because it corresponds to the "initialize" one.
174+
expect(forkInterface.send.mock.calls[1][0]).toEqual(request);
175+
});
176+
154177
it('calls the onProcessStart method synchronously if the queue is empty', () => {
155178
const worker = new Worker({
156179
forkOptions: {},

packages/jest-worker/src/workers/__tests__/NodeThreadsWorker.test.js

+23
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,29 @@ it('sends the task to the child process', () => {
160160
expect(worker._worker.postMessage.mock.calls[1][0]).toEqual(request);
161161
});
162162

163+
it('resends the task to the child process after a retry', () => {
164+
const worker = new Worker({
165+
forkOptions: {},
166+
maxRetries: 3,
167+
workerPath: '/tmp/foo/bar/baz.js',
168+
});
169+
170+
const request = [CHILD_MESSAGE_CALL, false, 'foo', []];
171+
172+
worker.send(request, () => {}, () => {});
173+
174+
// Skipping call "0" because it corresponds to the "initialize" one.
175+
expect(worker._worker.postMessage.mock.calls[1][0]).toEqual(request);
176+
177+
const previousWorker = worker._worker;
178+
worker._worker.emit('exit');
179+
180+
expect(worker._worker).not.toBe(previousWorker);
181+
182+
// Skipping call "0" because it corresponds to the "initialize" one.
183+
expect(worker._worker.postMessage.mock.calls[1][0]).toEqual(request);
184+
});
185+
163186
it('calls the onProcessStart method synchronously if the queue is empty', () => {
164187
const worker = new Worker({
165188
forkOptions: {},

0 commit comments

Comments
 (0)