-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(jest-worker): fix hanging when workers are killed or unexpectedly exit #13566
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -343,7 +343,7 @@ export default class ChildProcessWorker | |
} | ||
} | ||
|
||
private _onExit(exitCode: number | null) { | ||
private _onExit(exitCode: number | null, signal: NodeJS.Signals | null) { | ||
this._workerReadyPromise = undefined; | ||
this._resolveWorkerReady = undefined; | ||
|
||
|
@@ -372,6 +372,46 @@ export default class ChildProcessWorker | |
this._child.send(this._request); | ||
} | ||
} else { | ||
// At this point, it's not clear why the child process exited. There could | ||
// be several reasons: | ||
Comment on lines
+375
to
+376
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While we can reasonably guess when the Node.js internal max heap limit has been hit, it's difficult to guess other failure scenarios from There was a previous discussion of this in the linked issue: #13183 (comment) I feel propagating an error in the |
||
// | ||
// 1. The child process exited successfully after finishing its work. | ||
// This is the most likely case. | ||
// 2. The child process crashed in a manner that wasn't caught through | ||
// any of the heuristic-based checks above. | ||
// 3. The child process was killed by another process or daemon unrelated | ||
// to Jest. For example, oom-killer on Linux may have picked the child | ||
// process to kill because overall system memory is constrained. | ||
// | ||
// If there's a pending request to the child process in any of those | ||
// situations, the request still needs to be handled in some manner before | ||
// entering the shutdown phase. Otherwise the caller expecting a response | ||
// from the worker will never receive indication that something unexpected | ||
// happened and hang forever. | ||
// | ||
// In normal operation, the request is handled and cleared before the | ||
// child process exits. If it's still present, it's not clear what | ||
// happened and probably best to throw an error. In practice, this usually | ||
// happens when the child process is killed externally. | ||
Comment on lines
+392
to
+395
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if there's a better design pattern to guarantee
The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. happy to take a follow-up if you figure out a better pattern for this. But I think it's fine as is as well |
||
// | ||
// There's a reasonable argument that the child process should be retried | ||
// with request re-sent in this scenario. However, if the problem was due | ||
// to situations such as oom-killer attempting to free up system | ||
// resources, retrying would exacerbate the problem. | ||
const isRequestStillPending = !!this._request; | ||
if (isRequestStillPending) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I observed a few tests within Jest fail without the Kept the check just in case. It felt safer to only throw an error in situations we're certain something went wrong.
|
||
// If a signal is present, we can be reasonably confident the process | ||
// was killed externally. Log this fact so it's more clear to users that | ||
// something went wrong externally, rather than a bug in Jest itself. | ||
const error = new Error( | ||
signal != null | ||
? `A jest worker process (pid=${this._child.pid}) was terminated by another process: signal=${signal}, exitCode=${exitCode}. Operating system logs may contain more information on why this occurred.` | ||
: `A jest worker process (pid=${this._child.pid}) crashed for an unknown reason: exitCode=${exitCode}`, | ||
); | ||
|
||
this._onProcessEnd(error, null); | ||
} | ||
|
||
this._shutdown(); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
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. | ||
process.exit(); | ||
} | ||
|
||
process.kill(process.pid); | ||
} | ||
|
||
module.exports = { | ||
selfKill, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this
else
branch has gone through several changes in the past.signal === "SIGABRT"
: fix: when an out of memory event occurs process should exit correctly #13054