Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs: add support for async iterators to fs.writeFile #38525

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

yagipy
Copy link
Contributor

@yagipy yagipy commented May 3, 2021

Fixes: #38075

Checklist

  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@github-actions github-actions bot added fs Issues and PRs related to the fs subsystem / file system. needs-ci PRs that need a full CI run. labels May 3, 2021
@yagipy yagipy force-pushed the add-support-for-async-iterators-to-fs-writeFile branch from 675ffcd to 9a563a1 Compare May 3, 2021 23:17
@yagipy yagipy marked this pull request as ready for review May 4, 2021 04:48
@yagipy yagipy force-pushed the add-support-for-async-iterators-to-fs-writeFile branch from 9a563a1 to c9bba3f Compare May 4, 2021 08:33
@yagipy yagipy requested a review from Ayase-252 May 4, 2021 08:39
lib/fs.js Outdated
Comment on lines 2063 to 2070
if (writeErr) {
if (isUserFd) {
callback(writeErr);
} else {
fs.close(fd, (err) => {
callback(aggregateTwoErrors(err, writeErr));
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that in general it would make more sense to continue iterating only after a write has finished.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. As it is this will have issues with backpressure, risking flooding the destination with pending write requests. Also, I'm missing the bit about exiting the for await if the fs.write() errors on any single iteration.

lib/fs.js Outdated
);
}
fs.close(fd, callback);
})();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no catch handler on the promise being returned here.

lib/fs.js Outdated
@@ -2051,7 +2053,29 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
}
return;
}
// write(fd, buffer, offset, length, position, callback)
if (isCustomIterable(buffer)) {
(async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be clearer and more maintainable to separate this out into a separate top-level function.

lib/fs.js Outdated
}
);
}
fs.close(fd, callback);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close here is problematic because writes may still be pending from the loop.

@@ -95,3 +96,82 @@ fs.open(filename4, 'w+', common.mustSucceed((fd) => {

process.nextTick(() => controller.abort());
}

{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to separate the tests for the new functionality into a separate isolated test file.

Copy link
Member

@jasnell jasnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good idea but the implementation needs work.

@yagipy yagipy force-pushed the add-support-for-async-iterators-to-fs-writeFile branch 5 times, most recently from a7cc927 to 3313df8 Compare May 8, 2021 13:51
@yagipy yagipy force-pushed the add-support-for-async-iterators-to-fs-writeFile branch from 3313df8 to 785d76d Compare May 8, 2021 13:57
lib/fs.js Outdated
if (writeErr) {
handleWriteAllErrorCallback(fd, isUserFd, writeErr, callback);
} else {
writeAll(fd, isUserFd, buffer, offset,
Copy link
Member

@Linkgoron Linkgoron May 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that there's a mistake here that also exists in the fs/promises version, where the code assumes that the whole chunk has been written but it might have only been partially written.

@yagipy yagipy force-pushed the add-support-for-async-iterators-to-fs-writeFile branch 2 times, most recently from ba733d5 to ccdb3d1 Compare May 9, 2021 02:17
@yagipy yagipy force-pushed the add-support-for-async-iterators-to-fs-writeFile branch from ccdb3d1 to 6f21783 Compare May 9, 2021 02:50
@yagipy yagipy force-pushed the add-support-for-async-iterators-to-fs-writeFile branch from 176573e to c0b87d6 Compare May 15, 2021 05:17
@yagipy yagipy requested a review from Linkgoron May 15, 2021 08:12
@yagipy
Copy link
Contributor Author

yagipy commented May 15, 2021

@Linkgoron
I am currently working on the following response.
Are the following mistakes feasible in testing?
#38525 (comment)

I added the following test based on this test, but I was not able to reproduce the mistake. 😓 (The current implementation will pass.)

{
  const filenameLargeBuffer = join(tmpdir.path, 'testLargeBuffer.txt');
  const largeBuffer = {
    expected: 'dogs running'.repeat(512 * 1024),
    *[Symbol.iterator]() {
      yield Buffer.from('dogs running'.repeat(512 * 1024), 'utf8');
    }
  };

  fs.writeFile(
    filenameLargeBuffer, largeBuffer, common.mustSucceed(() => {
      const data = fs.readFileSync(filenameLargeBuffer, 'utf-8');
      assert.strictEqual(largeBuffer.expected, data);
    })
  );
}

@Linkgoron
Copy link
Member

@Linkgoron
I am currently working on the following response.
Are the following mistakes feasible in testing?
#38525 (comment)

I added the following test based on this test, but I was not able to reproduce the mistake. 😓 (The current implementation will pass.)

{
  const filenameLargeBuffer = join(tmpdir.path, 'testLargeBuffer.txt');
  const largeBuffer = {
    expected: 'dogs running'.repeat(512 * 1024),
    *[Symbol.iterator]() {
      yield Buffer.from('dogs running'.repeat(512 * 1024), 'utf8');
    }
  };

  fs.writeFile(
    filenameLargeBuffer, largeBuffer, common.mustSucceed(() => {
      const data = fs.readFileSync(filenameLargeBuffer, 'utf-8');
      assert.strictEqual(largeBuffer.expected, data);
    })
  );
}

It's quite difficult to reproduce, and I'm not sure in which cases write actually stops in the middle.

@yagipy yagipy requested a review from jasnell May 17, 2021 20:59
@yagipy
Copy link
Contributor Author

yagipy commented May 22, 2021

@Linkgoron
Fixed an issue where chunks might be partially written, referring to #38615.

@jasnell
Copy link
Member

jasnell commented May 24, 2021

@ronag @mcollina ... would definitely appreciate you taking a look at this one.

Copy link
Member

@ronag ronag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot going on here and I’m not sure I fully agree with all of it. I’m a little bit overloaded the next few days. I won’t block this but would appreciate if we could wait a few days before landing this.

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work! I've left a few notes on the implementation.

writeAllCustomIterable(
fd, isUserFd, buffer, offset, length, signal, encoding, callback)
.catch((reason) => {
handleWriteAllErrorCallback(fd, isUserFd, reason, callback);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would schedule this via process.nextTick. If this throws synchronously for whatever reason, it will lead to an unhandled rejection.

}
});
}

async function writeAllCustomIterable(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use async with a callback. Mixing those two can only lead to bugs. I would recommend having a promisified version of fs.write() and just use async/await.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcollina Thanks for your suggestion 👍 . I'm curious about the advantages of having a promisified version of fs.write() and using async/await. Could you elaborate on why this approach might be better than mixing async with a callback? I'm interested in understanding the potential benefits and how it could prevent bugs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fs Issues and PRs related to the fs subsystem / file system. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

fs: add support for async iterators to fs.writeFile
7 participants