Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/node_task_queue.cc
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,15 @@ void PromiseRejectCallback(PromiseRejectMessage message) {
"unhandled", unhandledRejections,
"handledAfter", rejectionsHandledAfter);
} else if (event == kPromiseResolveAfterResolved) {
value = message.GetValue();
// The multipleResolves event was deprecated in Node.js v17 and removed.
// No need to call into JavaScript for this event as it's a no-op.
// Fixes: https://github.com/nodejs/node/issues/51452
return;
} else if (event == kPromiseRejectAfterResolved) {
value = message.GetValue();
// The multipleResolves event was deprecated in Node.js v17 and removed.
// No need to call into JavaScript for this event as it's a no-op.
// Fixes: https://github.com/nodejs/node/issues/51452
return;
} else {
return;
}
Expand Down
79 changes: 79 additions & 0 deletions test/parallel/test-promise-race-memory-leak.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';

// Test for memory leak when racing immediately-resolving Promises
// Refs: https://github.com/nodejs/node/issues/51452

const common = require('../common');
const assert = require('assert');

// This test verifies that Promise.race() with immediately-resolving promises
// does not cause unbounded memory growth.
//
// Root cause: When Promise.race() settles, V8 attempts to resolve the other
// promises in the race, triggering 'multipleResolves' events. These events
// are queued in nextTick, but if the event loop never gets a chance to drain
// the queue (tight loop), memory grows unbounded.

async function promiseValue(value) {
return value;
}

async function testPromiseRace() {
const iterations = 100000;
const memBefore = process.memoryUsage().heapUsed;

for (let i = 0; i < iterations; i++) {
await Promise.race([promiseValue('foo'), promiseValue('bar')]);

// Allow event loop to drain nextTick queue periodically
if (i % 1000 === 0) {
await new Promise(setImmediate);
}
}

const memAfter = process.memoryUsage().heapUsed;
const growth = memAfter - memBefore;
const growthMB = growth / 1024 / 1024;

console.log(`Memory growth: ${growthMB.toFixed(2)} MB`);

// Memory growth should be reasonable (< 50MB for 100k iterations)
// Without the fix, this would grow 100s of MBs
assert.ok(growthMB < 50,
`Excessive memory growth: ${growthMB.toFixed(2)} MB (expected < 50 MB)`);
}

async function testPromiseAny() {
const iterations = 100000;
const memBefore = process.memoryUsage().heapUsed;

for (let i = 0; i < iterations; i++) {
await Promise.any([promiseValue('foo'), promiseValue('bar')]);

// Allow event loop to drain nextTick queue periodically
if (i % 1000 === 0) {
await new Promise(setImmediate);
}
}

const memAfter = process.memoryUsage().heapUsed;
const growth = memAfter - memBefore;
const growthMB = growth / 1024 / 1024;

console.log(`Memory growth (any): ${growthMB.toFixed(2)} MB`);

// Memory growth should be reasonable
assert.ok(growthMB < 50,
`Excessive memory growth: ${growthMB.toFixed(2)} MB (expected < 50 MB)`);
}

// Run tests
(async () => {
console.log('Testing Promise.race() memory leak...');
await testPromiseRace();

console.log('Testing Promise.any() memory leak...');
await testPromiseAny();

console.log('All tests passed!');
})().catch(common.mustNotCall());