Skip to content

Commit ceae148

Browse files
committed
feat(jest-worker): child interval heartbeats
1 parent 8f1e3fd commit ceae148

4 files changed

Lines changed: 32 additions & 15 deletions

File tree

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const SIGTERM_EXIT_CODE = SIGNAL_BASE_EXIT_CODE + 15;
3333
// How long to wait after SIGTERM before sending SIGKILL
3434
const SIGKILL_DELAY = 500;
3535

36+
const CHILD_HEARTBEAT_INTERVAL = 1_000;
37+
3638
/**
3739
* This class wraps the child process and provides a nice interface to
3840
* communicate with. It takes care of:
@@ -128,8 +130,15 @@ export default class ChildProcessWorker implements WorkerInterface {
128130
false,
129131
this._options.workerPath,
130132
this._options.setupArgs,
133+
CHILD_HEARTBEAT_INTERVAL,
131134
]);
132135

136+
setInterval(() => {
137+
if (process && process.send) {
138+
process.send([PARENT_MESSAGE_HEARTBEAT]);
139+
}
140+
}, 1000);
141+
133142
this._child = child;
134143

135144
this._retries++;
@@ -147,8 +156,6 @@ export default class ChildProcessWorker implements WorkerInterface {
147156
error.stack!,
148157
{type: 'WorkerError'},
149158
]);
150-
151-
this.monitorHeartbeat(() => this.monitorHeartbeatError());
152159
}
153160
}
154161

@@ -174,7 +181,7 @@ export default class ChildProcessWorker implements WorkerInterface {
174181

175182
if (this._options.inspector !== undefined) {
176183
error = new Error(
177-
`Test worker was unresponsive for 10 seconds. There was an inspector connected so we were able to capture stack frames before it was terminated.`,
184+
`Test worker was unresponsive for ${this._options.workerHeartbeatTimeout} milliseconds. There was an inspector connected so we were able to capture stack frames before it was terminated.`,
178185
);
179186
this._options.inspector.on('Debugger.paused', (message: any) => {
180187
const callFrames = message.params.callFrames.slice(0, 20);
@@ -208,7 +215,7 @@ export default class ChildProcessWorker implements WorkerInterface {
208215
});
209216
} else {
210217
error = new Error(
211-
`Test worker was unresponsive for 10 seconds. There was no inspector connected so we were unable to capture stack frames before it was terminated.`,
218+
`Test worker was unresponsive for ${this._options.workerHeartbeatTimeout} milliseconds. There was no inspector connected so we were unable to capture stack frames before it was terminated.`,
212219
);
213220
}
214221
// @ts-expect-error: adding custom properties to errors.
@@ -227,12 +234,10 @@ export default class ChildProcessWorker implements WorkerInterface {
227234

228235
switch (response[0]) {
229236
case PARENT_MESSAGE_OK:
230-
clearTimeout(this._heartbeatTimeout);
231237
this._onProcessEnd(null, response[1]);
232238
break;
233239

234240
case PARENT_MESSAGE_CLIENT_ERROR:
235-
clearTimeout(this._heartbeatTimeout);
236241
error = response[4];
237242

238243
if (error != null && typeof error === 'object') {
@@ -253,7 +258,6 @@ export default class ChildProcessWorker implements WorkerInterface {
253258
break;
254259

255260
case PARENT_MESSAGE_SETUP_ERROR:
256-
clearTimeout(this._heartbeatTimeout);
257261
error = new Error('Error when calling setup: ' + response[2]);
258262

259263
error.type = response[1];
@@ -266,7 +270,6 @@ export default class ChildProcessWorker implements WorkerInterface {
266270
this.monitorHeartbeat(() => this.monitorHeartbeatError());
267271
break;
268272
case PARENT_MESSAGE_CUSTOM:
269-
clearTimeout(this._heartbeatTimeout);
270273
this._onCustomMessage(response[1]);
271274
break;
272275
default:
@@ -288,6 +291,7 @@ export default class ChildProcessWorker implements WorkerInterface {
288291
}
289292
} else {
290293
this._shutdown();
294+
clearTimeout(this._heartbeatTimeout);
291295
}
292296
}
293297

@@ -323,6 +327,7 @@ export default class ChildProcessWorker implements WorkerInterface {
323327
SIGKILL_DELAY,
324328
);
325329
this._exitPromise.then(() => clearTimeout(sigkillTimeout));
330+
clearTimeout(this._heartbeatTimeout);
326331
}
327332

328333
getWorkerId(): number {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
WorkerOptions,
2727
} from '../types';
2828

29+
const CHILD_HEARTBEAT_INTERVAL = 1_000;
30+
2931
export default class ExperimentalWorker implements WorkerInterface {
3032
private _worker!: Worker;
3133
private _options: WorkerOptions;
@@ -110,6 +112,7 @@ export default class ExperimentalWorker implements WorkerInterface {
110112
false,
111113
this._options.workerPath,
112114
this._options.setupArgs,
115+
CHILD_HEARTBEAT_INTERVAL,
113116
]);
114117

115118
this._retries++;
@@ -203,12 +206,10 @@ export default class ExperimentalWorker implements WorkerInterface {
203206

204207
switch (response[0]) {
205208
case PARENT_MESSAGE_OK:
206-
clearTimeout(this._heartbeatTimeout);
207209
this._onProcessEnd(null, response[1]);
208210
break;
209211

210212
case PARENT_MESSAGE_CLIENT_ERROR:
211-
clearTimeout(this._heartbeatTimeout);
212213
error = response[4];
213214

214215
if (error != null && typeof error === 'object') {
@@ -230,7 +231,6 @@ export default class ExperimentalWorker implements WorkerInterface {
230231
this._onProcessEnd(error, null);
231232
break;
232233
case PARENT_MESSAGE_SETUP_ERROR:
233-
clearTimeout(this._heartbeatTimeout);
234234
error = new Error('Error when calling setup: ' + response[2]);
235235

236236
// @ts-expect-error: adding custom properties to errors.
@@ -243,11 +243,9 @@ export default class ExperimentalWorker implements WorkerInterface {
243243
this.monitorHeartbeat(() => this.monitorHeartbeatError());
244244
break;
245245
case PARENT_MESSAGE_CUSTOM:
246-
clearTimeout(this._heartbeatTimeout);
247246
this._onCustomMessage(response[1]);
248247
break;
249248
default:
250-
clearTimeout(this._heartbeatTimeout);
251249
throw new TypeError('Unexpected response from worker: ' + response[0]);
252250
}
253251
}
@@ -261,6 +259,7 @@ export default class ExperimentalWorker implements WorkerInterface {
261259
}
262260
} else {
263261
this._shutdown();
262+
clearTimeout(this._heartbeatTimeout);
264263
}
265264
}
266265

@@ -271,6 +270,7 @@ export default class ExperimentalWorker implements WorkerInterface {
271270
forceExit(): void {
272271
this._forceExited = true;
273272
this._worker.terminate();
273+
clearTimeout(this._heartbeatTimeout);
274274
}
275275

276276
send(

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
let file: string | null = null;
2222
let setupArgs: Array<unknown> = [];
2323
let initialized = false;
24+
let monitorHeartbeat: NodeJS.Timeout;
25+
let heartbeatIntervalValue: number;
2426

2527
/**
2628
* This file is a small bootstrapper for workers. It sets up the communication
@@ -41,6 +43,10 @@ const messageListener: NodeJS.MessageListener = request => {
4143
const init: ChildMessageInitialize = request;
4244
file = init[2];
4345
setupArgs = request[3];
46+
heartbeatIntervalValue = request[4];
47+
monitorHeartbeat = setInterval(() => {
48+
sendParentMessageHeartbeat();
49+
}, heartbeatIntervalValue);
4450
break;
4551

4652
case CHILD_MESSAGE_CALL:
@@ -115,6 +121,7 @@ function end(): void {
115121
function exitProcess(): void {
116122
// Clean up open handles so the process ideally exits gracefully
117123
process.removeListener('message', messageListener);
124+
clearInterval(monitorHeartbeat);
118125
}
119126

120127
function execMethod(method: string, args: Array<unknown>): void {
@@ -156,7 +163,6 @@ function execFunction(
156163
onError: (error: Error) => void,
157164
): void {
158165
let result;
159-
sendParentMessageHeartbeat();
160166

161167
try {
162168
result = fn.apply(ctx, args);

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
let file: string | null = null;
2424
let setupArgs: Array<unknown> = [];
2525
let initialized = false;
26+
let monitorHeartbeat: NodeJS.Timeout;
27+
let heartbeatIntervalValue: number;
2628

2729
/**
2830
* This file is a small bootstrapper for workers. It sets up the communication
@@ -43,6 +45,10 @@ const messageListener = (request: any) => {
4345
const init: ChildMessageInitialize = request;
4446
file = init[2];
4547
setupArgs = request[3];
48+
heartbeatIntervalValue = request[4];
49+
monitorHeartbeat = setInterval(() => {
50+
sendParentMessageHeartbeat();
51+
}, heartbeatIntervalValue);
4652
break;
4753

4854
case CHILD_MESSAGE_CALL:
@@ -117,6 +123,7 @@ function end(): void {
117123
function exitProcess(): void {
118124
// Clean up open handles so the worker ideally exits gracefully
119125
parentPort!.removeListener('message', messageListener);
126+
clearInterval(monitorHeartbeat);
120127
}
121128

122129
function execMethod(method: string, args: Array<unknown>): void {
@@ -158,7 +165,6 @@ function execFunction(
158165
onError: (error: Error) => void,
159166
): void {
160167
let result;
161-
sendParentMessageHeartbeat();
162168

163169
try {
164170
result = fn.apply(ctx, args);

0 commit comments

Comments
 (0)