Skip to content

fix: remove per-request queue listeners in abstract session - #1102

Open
Rinse12 wants to merge 1 commit into
ipfs:mainfrom
Rinse12:fix/abstract-session-queue-listener-leak
Open

fix: remove per-request queue listeners in abstract session#1102
Rinse12 wants to merge 1 commit into
ipfs:mainfrom
Rinse12:fix/abstract-session-queue-listener-leak

Conversation

@Rinse12

@Rinse12 Rinse12 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Important

Reframed 2026-08-12: this is hygiene, not a leak fix. The justification I originally gave
was wrong — see the retraction on #1101. A listener-bearing EventTarget is collected in
Gecko; the cycle collector reclaims it. Measured with a FinalizationRegistry, a real
Queue with all three of these listeners still attached and nothing else referencing it is
collected 100% of the time. The queues that were being retained were held by a separate
main-event bug, and patching that releases them whether or not this PR is applied.

Retracted specifically: "every queue stays a GC root", the 53.5%-of-live-heap figure, and
the ~930/h rate.

What the change is for

AbstractSession.retrieve() adds three listeners to a per-request Queue and never removes
them, while removing the provider and abort listeners a few lines away in the same
finally. This makes the teardown consistent: what the method registers, the method releases.

It is worth having on its own terms — cleanup should not depend on the surrounding collector
being clever enough to make the omission harmless — but I want to be straight that it does not
fix a measured leak, and you should feel free to close it if you would rather not carry the
extra bookkeeping for that.

Notes

  • The three handlers were inline arrow functions; they are now named consts, which is the only
    reason the diff looks larger than three added lines.
  • Event types come from QueueJobFailure / QueueJobSuccess, already exported from
    @libp2p/utils; the queue's job-options type is pulled into a local alias so the listener
    signatures can reuse it.
  • No test: with the mechanism claim retracted there is no observable behaviour to pin here
    beyond "the listener list is empty afterwards", which listenerCount() already reports.
  • packages/utils does not typecheck from a clean npm install on main (unbuilt
    @helia/interface siblings); I verified this change introduces no new diagnostics in
    abstract-session.ts beyond those already present.

AbstractSession.retrieve() creates a Queue per block request and registers
'failure', 'success' and 'idle' listeners on it, but the finally block only calls
queue.clear() - which splices the job array and touches no listeners.

Queue extends TypedEventEmitter, which extends the native EventTarget. In a
browser a listener-bearing EventTarget is kept alive by its own preserved
wrapper, so every Queue ever created stays a GC root, along with the jobs,
provider records and promise chains reachable from it. In a long-lived tab this
grows without bound - roughly 930 retained queues per hour in the app where this
was found, 29,273 of them after 31.5 hours, carrying 2.35M Promise and 767k
AsyncGenerator objects between them.

Keep references to the three handlers and remove them in the same finally that
already removes the 'provider' and 'abort' listeners.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant