Skip to content

Commit 9af5b7e

Browse files
committed
feat: determine where OOM came from
1 parent e1cab41 commit 9af5b7e

5 files changed

Lines changed: 30 additions & 6 deletions

File tree

packages/engine-multi/src/errors.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,21 @@ export class AutoinstallError extends EngineError {
7171
}
7272
}
7373

74+
// Where an OOM kill came from:
75+
// - 'heap' the V8 heap limit (thread resourceLimits or --max-old-space-size)
76+
// - 'cgroup' the OS cgroup memory.max ceiling (kernel SIGKILL)
77+
export type OOMSource = 'heap' | 'cgroup';
78+
7479
export class OOMError extends EngineError {
7580
severity = 'kill';
7681
name = 'OOMError';
7782
message;
83+
source: OOMSource;
7884

79-
constructor() {
85+
constructor(source: OOMSource = 'heap') {
8086
super();
8187

88+
this.source = source;
8289
this.message = `Run exceeded maximum memory usage`;
8390
}
8491
}

packages/engine-multi/src/util/serialize-error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export default (error: any) => {
44
name: error.name,
55
type: error.type,
66
subtype: error.subtype,
7+
source: error.source,
78
severity: error.severity || 'crash',
89
};
910
};

packages/engine-multi/src/worker/pool.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,14 @@ function createPool(script: string, options: PoolOptions = {}, logger: Logger) {
197197
// the cgroup's OOM counter before falling back to scraping stderr.
198198
const handle = worker.pid ? cgroups[worker.pid] : null;
199199
if (handle && hasOomKill(handle)) {
200+
logger.error(
201+
`pool: worker ${worker.pid} was killed by the OS for exceeding ` +
202+
`its cgroup memory limit (${options.cgroupMemoryLimitMb}mb)`
203+
);
200204
killWorker(worker);
201205
// restore a placeholder to the queue
202206
finish(false);
203-
reject(new OOMError());
207+
reject(new OOMError('cgroup'));
204208
return;
205209
}
206210

@@ -214,6 +218,9 @@ function createPool(script: string, options: PoolOptions = {}, logger: Logger) {
214218
if (worker.stderr && worker.stderr?.readableLength > 0) {
215219
for await (const line of rl) {
216220
if (line.match(/JavaScript heap out of memory/)) {
221+
logger.error(
222+
`pool: worker ${worker.pid} exceeded the V8 heap limit`
223+
);
217224
killWorker(worker);
218225
// restore a placeholder to the queue
219226
finish(false);
@@ -301,6 +308,8 @@ function createPool(script: string, options: PoolOptions = {}, logger: Logger) {
301308
// @ts-ignore
302309
e.severity = evt.error.severity;
303310
e.name = evt.error.name;
311+
// @ts-ignore preserve the OOM source ('heap'/'cgroup') across IPC
312+
e.source = evt.error.source;
304313
reject(e);
305314

306315
finish(worker);

packages/engine-multi/test/worker/cgroup-enforcement.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ linuxOnly(
2828
async (t) => {
2929
const pool = createPool(workerPath, { cgroupMemoryLimitMb }, logger);
3030

31-
await t.throwsAsync(() => pool.exec('blowNativeMemory', []), {
31+
const err = await t.throwsAsync(() => pool.exec('blowNativeMemory', []), {
3232
name: 'OOMError',
3333
});
34+
t.is((err as any).source, 'cgroup');
3435

3536
await pool.destroy();
3637
}

packages/engine-multi/test/worker/pool.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,11 @@ test('throw if memory limit is exceeded', async (t) => {
179179

180180
try {
181181
await pool.exec('blowMemory', [], { memoryLimitMb: 100 });
182+
t.fail('expected the run to OOM');
182183
} catch (e: any) {
183184
t.is(e.message, 'Run exceeded maximum memory usage');
184185
t.is(e.name, 'OOMError');
186+
t.is(e.source, 'heap');
185187
}
186188
});
187189

@@ -200,9 +202,13 @@ test('child process should not have --max-old-space-size when memoryLimitMb is n
200202
test('pool recovers after process-level OOM', async (t) => {
201203
const pool = createPool(workerPath, { memoryLimitMb: 50 }, logger);
202204

203-
await t.throwsAsync(() => pool.exec('blowMemory', [], { memoryLimitMb: 20 }), {
204-
name: 'OOMError',
205-
});
205+
const err = await t.throwsAsync(
206+
() => pool.exec('blowMemory', [], { memoryLimitMb: 20 }),
207+
{
208+
name: 'OOMError',
209+
}
210+
);
211+
t.is((err as any).source, 'heap');
206212

207213
// Pool should still be functional after the OOM
208214
const result = await pool.exec('test', [42]);

0 commit comments

Comments
 (0)