fs: remove FileHandle stream close listener - #64229
ishaanlabs-gg wants to merge 3 commits into
Conversation
| options.fd.on('close', FunctionPrototypeBind(stream.close, stream)); | ||
| stream[kHandleCloseListener] = FunctionPrototypeBind(stream.close, stream); | ||
| stream[kHandleCloseListenerCleanup] = FunctionPrototypeBind( | ||
| cleanupHandleCloseListener, stream); |
There was a problem hiding this comment.
This is not needed if not autoclosing. move it inside the if.
98b30c4 to
59c7463
Compare
|
Addressed the review feedback by only installing the FileHandle close listener for |
| stream.once('close', stream[kHandleCloseListenerCleanup]); | ||
| stream.once('end', stream[kHandleCloseListenerCleanup]); | ||
| stream.once('finish', stream[kHandleCloseListenerCleanup]); | ||
| stream.once('error', stream[kHandleCloseListenerCleanup]); |
There was a problem hiding this comment.
I'm pretty sure you don't need to listen for close here, since there will always be either an end (reader) or finish (writer), or an error. Listening for close just doubles-up the call (which is fine because the function protects against being called multiple times, but unnecessary)
968e2ad to
610d021
Compare
|
Updated this to address the follow-up review: the stream no longer registers a FileHandle Validation: Result: focused test passed. |
s/node/pull/64229 Signed-off-by: Rasad Regmi <[email protected]> Title: fs: fix FileHandle close listener/ref leak in streams Description: `fileHandle.createReadStream()`/`createWriteStream()` with `autoClose: false` leaks a `'close'` listener (and an internal reference) on the `FileHandle` every time the stream finishes on its own, because `autoClose: false` also disables `autoDestroy`, so the stream never reaches `_destroy()` — the only place that previously released them. Enough such streams on one long-lived handle trips `MaxListenersExceededWarning`. `importFd()` in `lib/internal/fs/streams.js` now builds a single idempotent `releaseHandleRef` shared between `FileHandleOperations .close()` (the normal destroy path, unchanged for the default `autoClose: true` behavior) and a `finished()`-based safety net registered after the stream's readable/writable state is initialized, for streams that finish without ever calling `destroy()`. `finished()` correctly resolves on `'end'`/`'finish'` alone when the stream won't emit `'close'` (see `willEmitClose()` in `internal/streams/utils.js`), and the shared idempotency guard means it's safe even if a stream is also explicitly closed after finishing on its own — the exact scenario that caused a prior fix (nodejs#64227) to fs: fix FileHandle close listener/ref leak in streams createReadStream()/createWriteStream({ autoClose: false }) created from a FileHandle attached a 'close' listener and took a reference on the handle in importFd(), but only released either when the stream went through _destroy(). Since autoClose: false also disables autoDestroy, a stream that finishes on its own never reaches _destroy(), so the listener and reference leaked. Creating enough such streams on a long-lived handle (e.g. repeated ranged reads) eventually tripped MaxListenersExceededWarning. A prior fix (fixed in 64227, reverted in 65387) released the reference again on the stream's 'end'/'finish'/'error' independent of the destroy path, which could unref the handle twice if the stream was later also explicitly closed/destroyed - a normal thing to do after a stream naturally ends. This introduces a single idempotent release function shared by both paths (the normal destroy path and a finished()-based safety net for autoClose: false streams that never reach _destroy()), so the reference and listener are released exactly once regardless of how the stream ends. Fixes: nodejs#64214 Refs: nodejs#64227 Refs: nodejs#65387 Refs: nodejs#64229 Signed-off-by: Rasad Regmi <[email protected]> Manual Git Steps (for you to run) cd "/home/rasadregmi/Desktop/Open Source Contributions Repos/node" git checkout -b fix-filehandle-stream-close-listener-leak main git add lib/internal/fs/streams.js test/parallel/test-fs-promises-file-hand git commit -s git push fork fix-filehandle-stream-close-listener-leak Commit Message fs: fix FileHandle close listener/ref leak in streams createReadStream()/createWriteStream({ autoClose: false }) created from a FileHandle attached a 'close' listener and took a reference on the handle in importFd(), but only released either when the stream went through _destroy(). Since autoClose: false also disables autoDestroy, a stream that finishes on its own never reaches _destroy(), so the listener and reference leaked. Creating enough such streams on a long-lived handle (e.g. repeated ranged reads) eventually tripped MaxListenersExceededWarning. A prior fix (fixed in 64227, reverted in 65387) released the reference again on the stream's 'end'/'finish'/'error' independent of the destroy path, which could unref the handle twice if the stream was later also explicitly closed/destroyed - a normal thing to do after a stream naturally ends. This introduces a single idempotent release function shared by both paths (the normal destroy path and a finished()-based safety net for autoClose: false streams that never reach _destroy()), so the reference and listener are released exactly once regardless of how the stream ends. Fixes: nodejs#64214 Refs: nodejs#64227 Refs: nodejs#65387 Refs: nodejs#64229 Signed-off-by: Rasad Regmi <[email protected]>
createReadStream()/createWriteStream({ autoClose: false }) created
from a FileHandle attached a 'close' listener and took a reference
on the handle in importFd(), but only released either when the
stream went through _destroy(). Since autoClose: false also
disables autoDestroy, a stream that finishes on its own never
reaches _destroy(), so the listener and reference leaked. Creating
enough such streams on a long-lived handle (e.g. repeated ranged
reads) eventually tripped MaxListenersExceededWarning.
A prior fix (fixed in 64227, reverted in 65387) released the
reference again on the stream's 'end'/'finish'/'error' independent
of the destroy path, which could unref the handle twice if the
stream was later also explicitly closed/destroyed - a normal thing
to do after a stream naturally ends.
This introduces a single idempotent release function shared by both
paths (the normal destroy path and a finished()-based safety net for
autoClose: false streams that never reach _destroy()), so the
reference and listener are released exactly once regardless of how
the stream ends.
Fixes: nodejs#64214
Refs: nodejs#64227
Refs: nodejs#65387
Refs: nodejs#64229
Signed-off-by: Rasad Regmi <[email protected]>
Fixes: #64214
This removes the extra
FileHandleclose listener that can be left behind bystreams created with
autoClose: false.The listener still lets the stream close if the
FileHandleis closed first,but
autoClose: falsestreams now remove that listener and release the extrastream reference when the stream finishes or closes on its own.
The change also keeps the default
autoClosebehavior covered so read andwrite streams still close their
FileHandleas before.Tests:
./out/Release/node --check lib/internal/fs/streams.js./out/Release/node test/parallel/test-fs-promises-file-handle-stream.jspython3 tools/test.py -J --mode=release parallel/test-fs-promises-file-handle-stream