Skip to content

AbortSignal.any() leaks when any of the provided signal is long-lived #55351

@mika-fischer

Description

@mika-fischer

Version

v22.9.0

Platform

Microsoft Windows NT 10.0.22631.0 x64

Subsystem

No response

What steps will reproduce the bug?

const ac = new AbortController();

let i = 0;
function run() {
    AbortSignal.any([ac.signal]);
    if (++i % 100_000 === 0) {
        const mem = process.memoryUsage().rss / 1024 / 1024;
        const kDependantSignals = Object.getOwnPropertySymbols(ac.signal).filter(
            (s) => s.toString() === 'Symbol(kDependantSignals)'
        )[0];
        const signals = ac.signal[kDependantSignals];
        console.log(`${i} - ${mem.toFixed(2)} MiB - ${signals?.size} signals`);
    }
    setImmediate(run);
}
run();

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

Always

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

AbortSignal.any should not case memory leaks

What do you see instead?

❯ node ..\test.js
node .\test.js
100000 - 82.29 MiB - 100000 signals
200000 - 97.98 MiB - 200000 signals
300000 - 126.98 MiB - 300000 signals
400000 - 131.91 MiB - 400000 signals
500000 - 152.07 MiB - 500000 signals
600000 - 162.52 MiB - 600000 signals
700000 - 208.69 MiB - 700000 signals
800000 - 219.15 MiB - 800000 signals
900000 - 221.80 MiB - 900000 signals
1000000 - 244.98 MiB - 1000000 signals
1100000 - 285.09 MiB - 1100000 signals
1200000 - 265.71 MiB - 1200000 signals
1300000 - 265.71 MiB - 1300000 signals
1400000 - 311.45 MiB - 1400000 signals
1500000 - 378.47 MiB - 1500000 signals
1600000 - 377.95 MiB - 1600000 signals
1700000 - 378.01 MiB - 1700000 signals
1800000 - 378.65 MiB - 1800000 signals
1900000 - 406.42 MiB - 1900000 signals
2000000 - 423.68 MiB - 2000000 signals
2100000 - 503.71 MiB - 2100000 signals
2200000 - 503.72 MiB - 2200000 signals
2300000 - 464.14 MiB - 2300000 signals
2400000 - 464.20 MiB - 2400000 signals
2500000 - 464.20 MiB - 2500000 signals
2600000 - 464.20 MiB - 2600000 signals
2700000 - 481.40 MiB - 2700000 signals
2800000 - 548.31 MiB - 2800000 signals
2900000 - 611.93 MiB - 2900000 signals
3000000 - 678.66 MiB - 3000000 signals
3100000 - 685.27 MiB - 3100000 signals
3200000 - 685.36 MiB - 3200000 signals
3300000 - 685.36 MiB - 3300000 signals
3400000 - 685.36 MiB - 3400000 signals
3500000 - 685.36 MiB - 3500000 signals
3600000 - 686.01 MiB - 3600000 signals
3700000 - 686.08 MiB - 3700000 signals
3800000 - 748.08 MiB - 3800000 signals
3900000 - 775.59 MiB - 3900000 signals
...

Additional information

It's pretty clear that AbortSignal.any attaches the combined signal to all its parent signals. But it only gets removed if the parent signals are actually aborted. If there is a long living signal among the parents, for instance something like a SIGINT handler, then it keeps accumulating WeakRefs to no longer existing AbortSignals.

Related issues:

Metadata

Metadata

Assignees

No one assigned

    Labels

    abortcontrollerIssues and PRs related to the AbortController APIconfirmed-bugIssues with confirmed bugs.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions