Skip to content

Commit 60f6375

Browse files
idecpojer
andauthored
Support workerIdleMemoryLimit=0 to always restart worker child processes between test files (#15740)
Co-authored-by: Christoph Nakazawa <[email protected]>
1 parent 2a436b8 commit 60f6375

File tree

8 files changed

+76
-17
lines changed

8 files changed

+76
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Features
44

55
- `[jest-config]` Allow `testMatch` to take a string value
6+
- `[jest-worker]` Let `workerIdleMemoryLimit` accept 0 to always restart worker child processes
67

78
### Fixes
89

packages/jest-config/src/__tests__/stringToBytes.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import stringToBytes from '../stringToBytes';
99

1010
describe('numeric input', () => {
11+
test('0 should be 0', () => {
12+
expect(stringToBytes(0)).toBe(0);
13+
});
14+
1115
test('> 1 represents bytes', () => {
1216
expect(stringToBytes(50.8)).toBe(50);
1317
});
@@ -33,6 +37,10 @@ describe('numeric input', () => {
3337

3438
describe('string input', () => {
3539
describe('numeric passthrough', () => {
40+
test('0 should be 0', () => {
41+
expect(stringToBytes('0')).toBe(0);
42+
});
43+
3644
test('> 1 represents bytes', () => {
3745
expect(stringToBytes('50.8')).toBe(50);
3846
});
@@ -55,10 +63,8 @@ describe('string input', () => {
5563
});
5664

5765
describe('parsing', () => {
58-
test('0% should throw an error', () => {
59-
expect(() => stringToBytes('0%', 51)).toThrow(
60-
'Unexpected numerical input',
61-
);
66+
test('0%', () => {
67+
expect(stringToBytes('0%', 51)).toBe(0);
6268
});
6369

6470
test('30%', () => {

packages/jest-config/src/stringToBytes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ function stringToBytes(
6868
}
6969

7070
if (typeof input === 'number') {
71-
if (input <= 1 && input > 0) {
71+
if (input === 0) {
72+
return 0;
73+
} else if (input <= 1 && input > 0) {
7274
if (percentageReference) {
7375
return Math.floor(input * percentageReference);
7476
} else {

packages/jest-core/src/testSchedulerHelper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function shouldRunInBand(
4848

4949
return (
5050
// When specifying a memory limit, workers should be used
51-
!workerIdleMemoryLimit &&
51+
workerIdleMemoryLimit === undefined &&
5252
(oneWorkerOrLess ||
5353
oneTestOrLess ||
5454
(tests.length <= 20 && timings.length > 0 && areFastTests))

packages/jest-worker/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ export type WorkerOptions = {
175175
* a job is complete. So you could have a resource limit of 500MB but an idle
176176
* limit of 50MB. The latter will only trigger if after a job has completed the
177177
* memory usage hasn't returned back down under 50MB.
178+
*
179+
* Special case: setting this to 0 will restart the worker process after each
180+
* job completes, providing complete process isolation between test files
181+
* regardless of memory usage.
178182
*/
179183
idleMemoryLimit?: number;
180184
/**

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

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export default class ChildProcessWorker
9090
this._stdout = null;
9191
this._stderr = null;
9292
this._childIdleMemoryUsage = null;
93-
this._childIdleMemoryUsageLimit = options.idleMemoryLimit || null;
93+
this._childIdleMemoryUsageLimit = options.idleMemoryLimit ?? null;
9494

9595
this._childWorkerPath =
9696
options.childWorkerPath || require.resolve('./processChild');
@@ -342,13 +342,16 @@ export default class ChildProcessWorker
342342
this._childIdleMemoryUsage &&
343343
this._childIdleMemoryUsage > limit
344344
) {
345-
this.state = WorkerStates.RESTARTING;
346-
347-
this.killChild();
345+
this._restart();
348346
}
349347
}
350348
}
351349

350+
private _restart(): void {
351+
this.state = WorkerStates.RESTARTING;
352+
this.killChild();
353+
}
354+
352355
private _onExit(exitCode: number | null, signal: NodeJS.Signals | null) {
353356
this._workerReadyPromise = undefined;
354357
this._resolveWorkerReady = undefined;
@@ -440,11 +443,16 @@ export default class ChildProcessWorker
440443
this._request = null;
441444

442445
if (
443-
this._childIdleMemoryUsageLimit &&
446+
this._childIdleMemoryUsageLimit !== null &&
444447
this._child.connected &&
445448
hasRequest
446449
) {
447-
this.checkMemoryUsage();
450+
if (this._childIdleMemoryUsageLimit === 0) {
451+
// Special case: `idleMemoryLimit` of `0` means always restart.
452+
this._restart();
453+
} else {
454+
this.checkMemoryUsage();
455+
}
448456
}
449457

450458
return onProcessEnd(...args);
@@ -542,17 +550,17 @@ export default class ChildProcessWorker
542550
* Gets updated memory usage and restarts if required
543551
*/
544552
checkMemoryUsage(): void {
545-
if (this._childIdleMemoryUsageLimit) {
553+
if (this._childIdleMemoryUsageLimit === null) {
554+
console.warn(
555+
'Memory usage of workers can only be checked if a limit is set',
556+
);
557+
} else {
546558
this._memoryUsageCheck = true;
547559
this._child.send([CHILD_MESSAGE_MEM_USAGE], err => {
548560
if (err) {
549561
console.error('Unable to check memory usage', err);
550562
}
551563
});
552-
} else {
553-
console.warn(
554-
'Memory usage of workers can only be checked if a limit is set',
555-
);
556564
}
557565
}
558566

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
PARENT_MESSAGE_MEM_USAGE,
2020
PARENT_MESSAGE_OK,
2121
type WorkerOptions,
22+
WorkerStates,
2223
} from '../../types';
2324

2425
jest.useFakeTimers();
@@ -674,3 +675,39 @@ it('should check for memory limits and restart if above absolute limit', async (
674675
expect(totalmem).not.toHaveBeenCalled();
675676
expect(forkInterface.kill).toHaveBeenCalledTimes(1);
676677
});
678+
679+
it('should restart immediately when limit is 0 without checking memory', () => {
680+
const worker = new Worker({
681+
forkOptions: {},
682+
idleMemoryLimit: 0,
683+
maxRetries: 3,
684+
workerPath: '/tmp/foo',
685+
} as WorkerOptions);
686+
687+
const onProcessStart = jest.fn();
688+
const onProcessEnd = jest.fn();
689+
const onCustomMessage = jest.fn();
690+
691+
worker.send(
692+
[CHILD_MESSAGE_CALL, false, 'test', []] as ChildMessageCall,
693+
onProcessStart,
694+
onProcessEnd,
695+
onCustomMessage,
696+
);
697+
698+
expect(onProcessStart).toHaveBeenCalledTimes(1);
699+
expect(onProcessEnd).not.toHaveBeenCalled();
700+
expect(onCustomMessage).not.toHaveBeenCalled();
701+
702+
forkInterface.emit('message', [PARENT_MESSAGE_OK]);
703+
expect(onProcessEnd).toHaveBeenCalledTimes(1);
704+
expect(onCustomMessage).not.toHaveBeenCalled();
705+
706+
expect(totalmem).not.toHaveBeenCalled();
707+
expect(forkInterface.send).not.toHaveBeenCalledWith(
708+
[CHILD_MESSAGE_MEM_USAGE],
709+
expect.any(Function),
710+
);
711+
expect(worker.state).toBe(WorkerStates.RESTARTING);
712+
expect(forkInterface.kill).toHaveBeenCalledTimes(1);
713+
});

website/versioned_docs/version-29.7/Configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2463,6 +2463,7 @@ Specifies the memory limit for workers before they are recycled and is primarily
24632463

24642464
After the worker has executed a test the memory usage of it is checked. If it exceeds the value specified the worker is killed and restarted. The limit can be specified in a number of different ways and whatever the result is `Math.floor` is used to turn it into an integer value:
24652465

2466+
- `0` - Always restart the worker between tests.
24662467
- `<= 1` - The value is assumed to be a percentage of system memory. So 0.5 sets the memory limit of the worker to half of the total system memory
24672468
- `\> 1` - Assumed to be a fixed byte value. Because of the previous rule if you wanted a value of 1 byte (I don't know why) you could use `1.1`.
24682469
- With units

0 commit comments

Comments
 (0)