undici icon indicating copy to clipboard operation
undici copied to clipboard

Memory leak when aborting fetch requests

Open jamesdiacono opened this issue 2 years ago • 5 comments
trafficstars

Bug Description

A memory leak occurs when frequently aborting fetch requests.

Reproducible By

Run the following code in Node.js.

import undici from "undici";

(function attack() {
    const controller = new AbortController();
    undici.fetch(
        "http://idonotexist:9999",
        {signal: controller.signal}
    ).catch(attack);
    controller.abort();
}());

In less than a minute, I see memory usage of the Node.js process climb to several gigabytes.

Expected Behavior

Memory usage to remain stable, as is the case when you comment out the controller.abort(); statement.

Environment

$ uname -a
Darwin MacBook-Pro.local 22.5.0 Darwin Kernel Version 22.5.0: Thu Jun  8 22:22:20 PDT 2023; root:xnu-8796.121.3~7/RELEASE_ARM64_T6000 arm64 arm Darwin
$ node -v
v20.4.0

jamesdiacono avatar Jul 28 '23 03:07 jamesdiacono

looks like we are not clearing up resources in case of an abort

mcollina avatar Jul 28 '23 07:07 mcollina

Does the same happens for request or others, or only within fetch?

I’ll try to look at it later Today

metcoder95 avatar Jul 28 '23 07:07 metcoder95

cc @ronag

KhafraDev avatar Jul 28 '23 16:07 KhafraDev

@metcoder95 I'm sorry, I don't know. I have never used request.

jamesdiacono avatar Aug 04 '23 05:08 jamesdiacono

No worries! But I was referring to undici.request 😅

metcoder95 avatar Aug 06 '23 12:08 metcoder95

Yes, as @KhafraDev mentioned it is basically https://github.com/nodejs/node/issues/43655

It is because of pendingUnhandledRejections keeping references on the promises, so that node can remove it it later from the WeakMap.

https://github.com/nodejs/node/blob/64c6d97463c29bade4d6081683dab2cd7cda298d/lib/internal/process/promises.js#L161

Uzlopak avatar Jan 29 '24 03:01 Uzlopak

@KhafraDev

Can or should we even implement a solution in undici?

Uzlopak avatar Jan 29 '24 04:01 Uzlopak

Can - yes (?), as your PR did that.

Should - yes, we can always revert the change once it's fixed upstream. We've implemented plenty of workarounds for bugs in node core in the past.

KhafraDev avatar Jan 29 '24 04:01 KhafraDev

This is a bug, which needs to be fixed in node core, as it is a "bug" in the event loop. This bug is basically introduced by the logic which handles the unhandledPromiseRejected warning. Basically this line results in this issue:

https://github.com/nodejs/node/blob/544cfc5ef151bca8d625fbccc581200a77b00bc0/lib/internal/process/promises.js#L161

So because we keep a reference to the promise for "at least one tick" v8 can not garbage collect the promise. So what happens is that before the event loop can go to the "next" iteration, it gets another promise to handle and starts again processing the same iteration of the event loop as processPromiseRejections returns true.

https://github.com/nodejs/node/blob/544cfc5ef151bca8d625fbccc581200a77b00bc0/lib/internal/process/task_queues.js#L96

Uzlopak avatar Feb 13 '24 18:02 Uzlopak

The previous promises should be collectable, no? Why is it staying alive? If that's the case you should be able to reproduce it just with an abortcontroller or a self-rejecting promise chain.

mcollina avatar Feb 13 '24 18:02 mcollina

Exactly. You dont need fetch. You can use the example in the corresponding node issue https://github.com/nodejs/node/issues/43655

Uzlopak avatar Feb 13 '24 18:02 Uzlopak

Then, we close this.

mcollina avatar Feb 13 '24 20:02 mcollina