Repository navigation
fs: behaviour of readFile and writeFile with file descriptors #23433
Description
- Version: v11.0.0-pre
- Platform: MacOS, Ubuntu (I have tried in these two operating systems personally)
- Subsystem: fs
This problem has been discussed multiple times without reaching any conclusion. That is why opening a separate issue to discuss.
Documented Expectations
As per our docs
readFile is defined as
Asynchronously reads the entire contents of a file.
and writeFile is defined as
Asynchronously writes data to a file, replacing the file if it already exists.
Current State of readFile & writeFile:
When a regular file path is passed:
The file is either read from the beginning or completely overwritten. This complies with the documentation.
When a file descriptor is passed:
readFile
The file is read from the current position in the file (the current position is maintained by the file descriptor itself).
readFile problem reproduction
'use strict';const fs = require('fs');
const assert = require('assert');
const buffer = Buffer.alloc(5);/* Open the file descriptor. */
const fd = fs.openSync(__filename, 'r');/* Read only the first five characters. */
assert.deepStrictEqual(fs.readSync(fd, buffer, 0, 5), 5);
assert.deepStrictEqual(buffer.toString('UTF-8'), '\'use ');/* As per the docs, both of these should be the same. */
assert.deepStrictEqual(
fs.readFileSync(fd).toString(),
fs.readFileSync(__filename).toString()
);
writeFile
Writes from the beginning of the file, without replacing the contents of the file. For example, if the existing content in the file isABCDand the newly written data is12, then the actual contents of the file after both thewriteFiles is12CD.
writeFile problem reproduction
'use strict';const fs = require('fs');
const assert = require('assert');/* Write only five characters. */
fs.writeFileSync('/tmp/test1', 'Hello');
assert.deepStrictEqual(fs.readFileSync('/tmp/test1').toString(), 'Hello');/* Write something else. */
fs.writeFileSync('/tmp/test1', 'Sun');
assert.deepStrictEqual(fs.readFileSync('/tmp/test1').toString(), 'Sun');/* Open the file descriptor. */
const fd = fs.openSync('/tmp/test', 'w');/* Write only five characters. */
assert.deepStrictEqual(fs.writeSync(fd, 'Hello'), 5);
assert.deepStrictEqual(fs.readFileSync('/tmp/test').toString(), 'Hello');/* Write something else. */
fs.writeFileSync(fd, 'Sun');
assert.deepStrictEqual(fs.readFileSync('/tmp/test').toString(), 'Sun');
These two behaviours deviate from the documented expectations.
Cases for changing the behaviour to work from the beginning
#9671 (It presents the case with readFile)
Cases for keeping the current behaviour
Its intuitive for people to expect read/write from the current position till the end of the file. (For example, read a header from a file and then decide whether to read the rest of the file or not.)
Proposed courses of action
- Land fs: make writeFile consistent with readFile wrt fd #23709, which makes
writeFilealso to write from the current file position just likereadFilereads from the current file position, and be done with it. (semver-major) - Make the
readFileandwriteFileto work from the beginning of the file. (semver-major)
a. If the file descriptor is not seekable, an error will be thrown from libuv. - Variant of 2, if the file descriptor doesn't correspond to a regular file (basically a non-seekable file descriptor), throw an error. Otherwise, try to work from the beginning of the file. (semver-major)
- Remove fd support, its just a helper function for a not so oftenly used use-case, and there are valid reasons to support both seeking and non-seeking, so whatever we choose someone will need to write their own wrapper to make functions that work as they expect. Let there be an npm package to do this, or several. (semver-major)
- Just leave the current behaviour as it is with the doc changes in doc: clarify fd behaviour with {read,write}File #23706.
This is not an exhaustive list. Please feel free to propose more ideas.
PS: This comment by @sam-github summarises my personal expectations from the API.
| Name | Option 1 | Option 2 | Option 3 | Option 4 | Option 5 |
|---|---|---|---|---|---|
| @thefourtheye | X |
X |
|||
| @mcollina | X |
X |
|||
| @addaleax | X |
X |
|||
| @TimothyGu | X |
||||
| @mhdawson | X |
X |
|||
| @Trott | |||||
| @ChALkeR | X + warn * |
X |
|||
| @rvagg | X |
||||
| @cjihrig | X |
X |
|||
| @joyeecheung | X |
||||
| @apapirovski | X |
||||
| @Fishrock123 | X |
||||
| Total | 9 |
1 |
1 |
2 |
4 |
cc @nodejs/fs
Tagging @nodejs/tsc (Might be a bit early, but this has been discussed before. So tagging to get more inputs)
Tagging @seishun @vsemozhetbyt @sam-github @jorangreef @addaleax from the previous discussions.
If needed we can tag the collaborators to get their expectations.
Previous discussions on this topic happened in #9671, #10809, #10853, #13572,
#22554
Activity
sam-github commentedon Oct 12, 2018
Another option:
vsemozhetbyt commentedon Oct 12, 2018
See also #22554 for various behaviour of
fs.writeFile()variants (sync / callback / promise ). Promise variant adds even more discrepancy: complete rewriting vs positional rewriting vs appending.vsemozhetbyt commentedon Oct 12, 2018
As for a use case for
fs.writeFile(fd). Say, I have a long process with many iterative tasks. After each task, the script writes a current task identifier to a state file, to save a progress and to be able to restore the state. This identifier should be overwritten, not appended as in a log file, so that it can be easily required not by reading and parsing all the huge log file, but via one smallfs.readFile()call. Using a path would have an overhead of a constant file reopening. But we cannot use a file descriptor in this case for now.thefourtheye commentedon Oct 12, 2018
@sam-github I included that option in the OP.
@vsemozhetbyt I have included that issue also in the list of references.
Let's start recording what we think would be the expected behaviour of
readFile&writeFilein the table in OP.thefourtheye commentedon Oct 16, 2018
I checked few other languages to see how they handle file descriptors.
Python
Other languages which I checked were Ruby, Java, C, and C++. They all provide their own abstractions over file descriptors. Using file descriptors are not recommended, so not a lot of fd based functions are available in them.
Trott commentedon Oct 16, 2018
I think step zero should be to update the documentation to specify the current behavior. If we change that behavior, fine, but there should be no reason to not have correct docs while that is being discussed. Is there already a PR open to update the docs? If not, I'm happy to do it or let @thefourtheye or anyone else do it.
vsemozhetbyt commentedon Oct 16, 2018
Would it be a bit confusing to state 3 different behaviors in docs? Especially callback/sync fd variant (merging) vs promise fd variant (appending)?
Trott commentedon Oct 17, 2018
It will be a bit confusing, but hopefully less confusing than the current situation of having the same behavior but incorrectly documented.
thefourtheye commentedon Oct 17, 2018
@Trott I got a PR up to clarify the behaviour of file descriptors with
readFileandwriteFile, #23706.mcollina commentedon Oct 24, 2018
I'm for deprecating
readFile(fd)andwriteFile(fd), if it does not break on CITGM. If somebody is deailing with file descriptors, I think they can use the barebone methodsread()andwrite().Trott commentedon Oct 24, 2018
Adding
tsc-agendalabel per discussion in the TSC meeting today where conclusion was "Get more engagement, particularly from @nodejs/tsc, but leave on agenda in case no consensus is reached."Trott commentedon Oct 24, 2018
I find all four proposed options acceptable (assuming Option 1 involves documenting the current behavior, which given the open PR, it seems like it does).
26 remaining items
rvagg commentedon Nov 7, 2018
I'd be OK with going the option 1 + warning route through a single major cycle for a pure option 1. I'm concerned about performance implications though, if we need to reach down in to do an lseek to figure out whether a warning is warranted, can we put that in the path in such a way as to not make reads slower? Since here's a bunch of layers of indirection between
writeFile()and the actual operations perhaps this is just going to add too much complication?Still also fine with just a pure option 1 though, that seems to be consensus here unless anyone else wants to jump to the option 1 + warn bandwagon and put together a PR that implements it?
thefourtheye commentedon Nov 12, 2018
@ChALkeR Seek is not supported by libuv. @bnoordhuis's comment in this thread
kind of gives a reason why it is not included in libuv. Do you mean, using
*seekto get the details from our C++ layer? I believe that would be very difficult to maintain.Yes, I agree. However, in this particular case, we don't have a straight forward to decide when to actually warn. Would it be very bad if we warn people once who use these functions with file descriptors?That said,#23709 is going to write from the current file position always, it doesn't matter what that is. So, if someone is usingwriteFilewith a file descriptor, warning them saying "data is going to be written from the current file position instead of the current behaviour which is to write from position zero", would be okay, right?Trott commentedon Nov 13, 2018
This seems to be progressing to a conclusion. I'm going to remove the
tsc-agendalabel (since I'm the one who added it). Feel free to re-add it if you think it should stay on or go back on the agenda.kghost commentedon Dec 9, 2018
I think seek is needed #24923
thefourtheye commentedon Dec 11, 2018
@kghost The outcome of this issue is that
readFileandwriteFilewill not seek and read/write from the current file position. You can always read and write from specific positions of a file withfs.readandfs.writefunctions.ChALkeR commentedon Dec 12, 2018
@thefourtheye Sorry for the delay 😞 .
After further thoughts about this, I withdraw my suggestion — the increased maintaining costs indeed would not pay off here.
thefourtheye commentedon Dec 17, 2018
Out of the 18 TSC members (including @Trott), 12 have expressed their opinions in the table in the OP. With six abstentions and nine votes for Option 1, Option 1 wins. @nodejs/tsc please let me know, if there are any objections here.
Thanks everyone for participating and driving this issue to closure.
doc: update behaviour of fs.writeFile
doc: update behaviour of fs.writeFile