Skip to content

Commit

Permalink
fix(jest-worker): fix hanging after process.exit() in thread workers
Browse files Browse the repository at this point in the history
  • Loading branch information
gluxon committed Nov 6, 2022
1 parent a0703c9 commit f274baf
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 59 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -5,7 +5,7 @@
### Fixes

- `[jest-mock]` Treat cjs modules as objects so they can be mocked ([#13513](https://github.com/facebook/jest/pull/13513))
- `[jest-worker]` Throw an error instead of hanging when jest workers are killed ([#13566](https://github.com/facebook/jest/pull/13566))
- `[jest-worker]` Throw an error instead of hanging when jest workers terminate unexpectedly ([#13566](https://github.com/facebook/jest/pull/13566))

### Chore & Maintenance

Expand Down
12 changes: 12 additions & 0 deletions packages/jest-worker/src/workers/NodeThreadsWorker.ts
Expand Up @@ -254,6 +254,18 @@ export default class ExperimentalWorker
this._worker.postMessage(this._request);
}
} else {
// If the worker thread exits while a request is still pending, throw an
// error. This is unexpected and tests may not have run to completion.
const isRequestStillPending = !!this._request;
if (isRequestStillPending) {
this._onProcessEnd(
new Error(
'A Jest worker thread exited unexpectedly before finishing tests for an unknown reason. One of the ways this can happen is if process.exit() was called in testing code.',
),
null,
);
}

this._shutdown();
}
}
Expand Down
105 changes: 51 additions & 54 deletions packages/jest-worker/src/workers/__tests__/WorkerEdgeCases.test.ts
Expand Up @@ -385,69 +385,66 @@ describe.each([
);
});
});
});

// This describe block only applies to the child process worker since it's
// generally not possible for external processes to abruptly kill a thread of
// another process.
describe('should not hang on external process kill', () => {
let worker: ChildProcessWorker;

beforeEach(() => {
const options = {
childWorkerPath: processChildWorkerPath,
maxRetries: 0,
silent: true,
workerPath: join(__dirname, '__fixtures__', 'SelfKillWorker'),
} as unknown as WorkerOptions;

worker = new ChildProcessWorker(options);
});
describe('should not hang when worker is killed or unexpectedly terminated', () => {
let worker: ChildProcessWorker | ThreadsWorker;

afterEach(async () => {
await new Promise<void>(resolve => {
setTimeout(async () => {
if (worker) {
worker.forceExit();
await worker.waitForExit();
}
beforeEach(() => {
const options = {
childWorkerPath: processChildWorkerPath,
maxRetries: 0,
silent: true,
workerPath: join(__dirname, '__fixtures__', 'SelfKillWorker'),
} as unknown as WorkerOptions;

resolve();
}, 500);
worker = new ChildProcessWorker(options);
});
});

// Regression test for https://github.com/facebook/jest/issues/13183
test('onEnd callback is called', async () => {
let onEndPromiseResolve: () => void;
let onEndPromiseReject: (err: Error) => void;
const onEndPromise = new Promise<void>((resolve, reject) => {
onEndPromiseResolve = resolve;
onEndPromiseReject = reject;
});
afterEach(async () => {
await new Promise<void>(resolve => {
setTimeout(async () => {
if (worker) {
worker.forceExit();
await worker.waitForExit();
}

const onStart = jest.fn();
const onEnd = jest.fn((err: Error | null) => {
if (err) {
return onEndPromiseReject(err);
}
onEndPromiseResolve();
resolve();
}, 500);
});
});
const onCustom = jest.fn();

await worker.waitForWorkerReady();
// Regression test for https://github.com/facebook/jest/issues/13183
test('onEnd callback is called', async () => {
let onEndPromiseResolve: () => void;
let onEndPromiseReject: (err: Error) => void;
const onEndPromise = new Promise<void>((resolve, reject) => {
onEndPromiseResolve = resolve;
onEndPromiseReject = reject;
});

// The SelfKillWorker simulates an external process calling SIGTERM on it,
// but just SIGTERMs itself underneath the hood to make this test easier.
worker.send(
[CHILD_MESSAGE_CALL, true, 'selfKill', []],
onStart,
onEnd,
onCustom,
);
const onStart = jest.fn();
const onEnd = jest.fn((err: Error | null) => {
if (err) {
return onEndPromiseReject(err);
}
onEndPromiseResolve();
});
const onCustom = jest.fn();

// The onEnd callback should be called when the child process exits.
await expect(onEndPromise).rejects.toBeInstanceOf(Error);
expect(onEnd).toHaveBeenCalled();
await worker.waitForWorkerReady();

// The SelfKillWorker simulates an external process calling SIGTERM on it,
// but just SIGTERMs itself underneath the hood to make this test easier.
worker.send(
[CHILD_MESSAGE_CALL, true, 'selfKill', []],
onStart,
onEnd,
onCustom,
);

// The onEnd callback should be called when the child process exits.
await expect(onEndPromise).rejects.toBeInstanceOf(Error);
expect(onEnd).toHaveBeenCalled();
});
});
});
Expand Up @@ -7,12 +7,9 @@
const {isMainThread} = require('worker_threads');

async function selfKill() {
// This test is intended for the child process worker. If the Node.js worker
// thread mode is accidentally tested instead, let's prevent a confusing
// situation where process.kill stops the Jest test harness itself.
if (!isMainThread) {
// process.exit is documented to only stop the current thread rather than
// the process in a worker_threads environment.
// the entire process in a worker_threads environment.
process.exit();
}

Expand Down

0 comments on commit f274baf

Please sign in to comment.