Skip to content

Racing immediately-resolving Promises leads to memory leak #51452

Open
@cefn

Description

@cefn

Version

21.5.0

Platform

Linux penguin 6.1.55-06877-gc83437f2949f #1 SMP PREEMPT_DYNAMIC Thu Dec 14 19:17:39 PST 2023 x86_64 GNU/Linux

Subsystem

async_hooks or async/await

What steps will reproduce the bug?

Run the following code. Failure is quicker and less likely to interfere with system stability if you run with a low heap ceiling like this, but it will fail without...

node --max-old-space-size=16 test/examples/ephemeralPromiseMemoryLeak.js
//ephemeralPromiseMemoryLeak.js

async function promiseValue(value) {
  return value;
}

async function run() {
  for (;;) {
    await Promise.race([promiseValue("foo"), promiseValue("bar")]);
  }
}

run();

An equivalent OOM is created if you substitute Promise.any for Promise.race...

async function promiseValue(value) {
  return value;
}

async function run() {
  for (;;) {
    await Promise.any([promiseValue("foo"), promiseValue("bar")]);
  }
}

run();

How often does it reproduce? Is there a required condition?

It always fails.

What is the expected behavior? Why is that the expected behavior?

I would expect it not to accumulate references in memory and fail.

What do you see instead?

Fails with the following error

✗ node test/examples/ephemeralPromiseMemoryLeak.js

<--- Last few GCs --->

[31511:0x6de4330]    39874 ms: Mark-Compact (reduce) 2048.2 (2083.6) -> 2047.3 (2083.9) MB, 1308.67 / 0.00 ms  (average mu = 0.071, current mu = 0.001) allocation failure; scavenge might not succeed


<--- JS stacktrace --->

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

 1: 0xcc062a node::OOMErrorHandler(char const*, v8::OOMDetails const&) [node]
 2: 0x104eb90 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
 3: 0x104ee77 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
 4: 0x126e0b5  [node]
 5: 0x126e58e  [node]
 6: 0x12837b6 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::internal::GarbageCollectionReason, char const*) [node]
 7: 0x12842d9  [node]
 8: 0x12848e8  [node]
 9: 0x19d4311  [node]
[1]    31511 abort (core dumped)  node test/examples/ephemeralPromiseMemoryLeak.js

Additional information

If the promiseValue call incorporates an explicit scheduling on the event loop, there is no memory leak...

// setImmediateNoLeak.js

function promiseValue(value) {
  return new Promise((resolve) => {
    setImmediate(() => resolve(value));
  });
}

async function run() {
  for (;;) {
    await Promise.race([promiseValue("foo"), promiseValue("bar")]);
  }
}

run();

If the promiseValue call isn't composed via a Promise.race there is no leak...

// noRaceNoLeak.js

async function promiseValue(value) {
  return value;
}

async function run() {
  for (;;) {
    await promiseValue("foo");
    await promiseValue("bar");
  }
}

run();

Maybe obviously, but putting it here for completeness, if you don't use an async await loop, but compose the loop itself with setImmediate there is no leak...

// noLoopNoLeak.js

async function promiseValue(value) {
  return value;
}

async function doRace() {
  await Promise.race([promiseValue("foo"), promiseValue("bar")]);
}

function run() {
  doRace().then(() => setImmediate(run));
}

run();

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmed-bugIssues with confirmed bugs.performanceIssues and PRs related to the performance of Node.js.promisesIssues and PRs related to ECMAScript promises.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions