Skip to content

Commit

Permalink
fs: deprecate closing FileHandle on garbage collection
Browse files Browse the repository at this point in the history
Closing the FileHandle on garbage collection is a bad practice.
Runtime deprecate and indicate that an error will be thrown in
the future.

PR-URL: nodejs#28396
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
  • Loading branch information
jasnell committed Feb 5, 2020
1 parent 018c3e8 commit 2d8febc
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 2 deletions.
33 changes: 32 additions & 1 deletion doc/api/deprecations.md
Expand Up @@ -2538,7 +2538,6 @@ an officially supported API.
changes:
- version: v13.0.0
pr-url: https://github.com/nodejs/node/pull/29061
description: Runtime deprecation.
-->
Type: Runtime
Expand Down Expand Up @@ -2569,6 +2568,37 @@ accordingly instead to avoid the ambigiuty.
To maintain existing behaviour `response.finished` should be replaced with
`response.writableEnded`.
<a id="DEP00XX"></a>
### DEP00XX: Closing fs.FileHandle on garbage collection
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/28396
description: Runtime deprecation.
-->
Type: Runtime
Allowing a [`fs.FileHandle`][] object to be closed on garbage collection is
deprecated. In the future, doing so may result in a thrown error that will
terminate the process.
Please ensure that all `fs.FileHandle` objects are explicitly closed using
`FileHandle.prototype.close()` when the `fs.FileHandle` is no longer needed:
```js
const fsPromises = require('fs').promises;
async function openAndClose() {
let filehandle;
try {
filehandle = await fsPromises.open('thefile.txt', 'r');
} finally {
if (filehandle !== undefined)
await filehandle.close();
}
}
```
[`--pending-deprecation`]: cli.html#cli_pending_deprecation
[`--throw-deprecation`]: cli.html#cli_throw_deprecation
[`Buffer.allocUnsafeSlow(size)`]: buffer.html#buffer_class_method_buffer_allocunsafeslow_size
Expand Down Expand Up @@ -2606,6 +2636,7 @@ To maintain existing behaviour `response.finished` should be replaced with
[`domain`]: domain.html
[`ecdh.setPublicKey()`]: crypto.html#crypto_ecdh_setpublickey_publickey_encoding
[`emitter.listenerCount(eventName)`]: events.html#events_emitter_listenercount_eventname
[`fs.FileHandle`]: fs.html#fs_class_filehandle
[`fs.access()`]: fs.html#fs_fs_access_path_mode_callback
[`fs.createReadStream()`]: fs.html#fs_fs_createreadstream_path_options
[`fs.createWriteStream()`]: fs.html#fs_fs_createwritestream_path_options
Expand Down
8 changes: 8 additions & 0 deletions src/env-inl.h
Expand Up @@ -888,6 +888,14 @@ inline bool Environment::owns_inspector() const {
return flags_ & kOwnsInspector;
}

bool Environment::filehandle_close_warning() const {
return emit_filehandle_warning_;
}

void Environment::set_filehandle_close_warning(bool on) {
emit_filehandle_warning_ = on;
}

inline uint64_t Environment::thread_id() const {
return thread_id_;
}
Expand Down
4 changes: 4 additions & 0 deletions src/env.h
Expand Up @@ -1073,6 +1073,9 @@ class Environment : public MemoryRetainer {
inline node_module* extra_linked_bindings_head();
inline const Mutex& extra_linked_bindings_mutex() const;

inline bool filehandle_close_warning() const;
inline void set_filehandle_close_warning(bool on);

inline void ThrowError(const char* errmsg);
inline void ThrowTypeError(const char* errmsg);
inline void ThrowRangeError(const char* errmsg);
Expand Down Expand Up @@ -1288,6 +1291,7 @@ class Environment : public MemoryRetainer {
bool trace_sync_io_ = false;
bool emit_env_nonstring_warning_ = true;
bool emit_err_name_warning_ = true;
bool emit_filehandle_warning_ = true;
size_t async_callback_scope_depth_ = 0;
std::vector<double> destroy_async_id_list_;

Expand Down
11 changes: 11 additions & 0 deletions src/node_file.cc
Expand Up @@ -205,10 +205,21 @@ inline void FileHandle::Close() {
// If the close was successful, we still want to emit a process warning
// to notify that the file descriptor was gc'd. We want to be noisy about
// this because not explicitly closing the FileHandle is a bug.

env()->SetUnrefImmediate([detail](Environment* env) {
ProcessEmitWarning(env,
"Closing file descriptor %d on garbage collection",
detail.fd);
if (env->filehandle_close_warning()) {
env->set_filehandle_close_warning(false);
ProcessEmitDeprecationWarning(
env,
"Closing a FileHandle object on garbage collection is deprecated. "
"Please close FileHandle objects explicitly using "
"FileHandle.prototype.close(). In the future, an error will be "
"thrown if a file descriptor is closed during garbage collection.",
"DEP00XX").IsNothing();
}
});
}

Expand Down
9 changes: 8 additions & 1 deletion test/parallel/test-fs-filehandle.js
Expand Up @@ -19,13 +19,20 @@ let fdnum;
assert.strictEqual(ctx.errno, undefined);
}

const deprecationWarning =
'Closing a FileHandle object on garbage collection is deprecated. ' +
'Please close FileHandle objects explicitly using ' +
'FileHandle.prototype.close(). In the future, an error will be ' +
'thrown if a file descriptor is closed during garbage collection.';

common.expectWarning({
'internal/test/binding': [
'These APIs are for internal testing only. Do not use them.'
],
'Warning': [
`Closing file descriptor ${fdnum} on garbage collection`
]
],
'DeprecationWarning': [[deprecationWarning, 'DEP00XX']]
});

global.gc();
Expand Down
39 changes: 39 additions & 0 deletions test/parallel/test-fs-promises-file-handle-close.js
@@ -0,0 +1,39 @@
// Flags: --expose-gc --no-warnings
'use strict';

// Test that a runtime warning is emitted when a FileHandle object
// is allowed to close on garbage collection. In the future, this
// test should verify that closing on garbage collection throws a
// process fatal exception.

const common = require('../common');
const assert = require('assert');
const { promises: fs } = require('fs');

const warning =
'Closing a FileHandle object on garbage collection is deprecated. ' +
'Please close FileHandle objects explicitly using ' +
'FileHandle.prototype.close(). In the future, an error will be ' +
'thrown if a file descriptor is closed during garbage collection.';

async function doOpen() {
const fh = await fs.open(__filename);

common.expectWarning({
Warning: [[`Closing file descriptor ${fh.fd} on garbage collection`]],
DeprecationWarning: [[warning, 'DEP00XX']]
});

return fh;
}

// Perform the file open assignment within a block.
// When the block scope exits, the file handle will
// be eligible for garbage collection.
{
doOpen().then(common.mustCall((fd) => {
assert.strictEqual(typeof fd, 'object');
}));
}

setTimeout(() => global.gc(), 10);

0 comments on commit 2d8febc

Please sign in to comment.