Files
node/test/parallel/test-fs-promises-file-handle-aggregate-errors.js
Shelley Vohr e324419933 fs: write files in one thread pool round trip
fs.writeFile(path, data) took three libuv thread pool round trips
(open, write, close), each its own request with its own queue wait,
completion callback and JS/C++ crossing, and fs.promises.writeFile()
did the same through a FileHandle. For the small files applications
write most, the round trips are the cost, and each occupies a pool slot
that concurrent fs, dns.lookup() and crypto work is also queueing for.

Add WriteFileJob next to ReadFileJob: an AsyncWrap + ThreadPoolWork
that opens, writes the whole buffer (looping on short writes) and
closes as one pool task, keeping the buffer alive until it is done.
fs.writeFile() uses it for path arguments without flush;
fs.promises.writeFile() additionally keeps data above one write chunk
(and iterables) on the FileHandle path, so large writes stay abortable
between chunks as before. File descriptors, FileHandles, flush: true
and an active VFS keep their existing paths.

Behavior is otherwise kept: open failures report syscall 'open' with
the path, write failures 'write'; permission errors are delivered
through the callback/promise; an abort signalled while the write is in
flight is still reported as an AbortError; the job is an FSREQCALLBACK
resource for async_hooks and emits the 'write' fs trace event.

Tests that used fs.writeFile() as a proxy for open/close trace events,
or injected FileHandle faults for path-based writes, are adjusted to
keep testing what they test.

The job holds the buffer's backing store, so the memory stays valid if
the buffer is detached or collected before the write finishes; a
resizable ArrayBuffer could still have its pages decommitted by a
shrink, so its contents are copied when the job is created.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65489
Refs: https://github.com/nodejs/node/pull/65327
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
2026-09-03 15:28:39 +00:00

77 lines
2.4 KiB
JavaScript

'use strict';
// Flags: --expose-internals
const common = require('../common');
const tmpdir = require('../common/tmpdir');
// The following tests validate aggregate errors are thrown correctly
// when both an operation and close throw.
const {
readFile,
writeFile,
truncate,
lchmod,
} = require('fs/promises');
const {
FileHandle,
} = require('internal/fs/promises');
const assert = require('assert');
const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd');
let count = 0;
async function createFile() {
const filePath = tmpdir.resolve(`aggregate_errors_${++count}.txt`);
// Larger than one read chunk (512 KiB), so that readFile(path) reads it
// through a FileHandle (small files are read in a single native round trip
// that does not involve FileHandle.prototype).
await writeFile(filePath, 'content'.repeat(100_000));
return filePath;
}
async function checkAggregateError(op) {
try {
const filePath = await createFile();
Object.defineProperty(FileHandle.prototype, 'fd', {
get: function() {
// Close is set by using a setter,
// so it needs to be set on the instance.
const originalClose = this.close;
this.close = async () => {
// close the file
await originalClose.call(this);
const closeError = new Error('CLOSE_ERROR');
closeError.code = 456;
throw closeError;
};
const opError = new Error('INTERNAL_ERROR');
opError.code = 123;
throw opError;
}
});
await assert.rejects(op(filePath), common.mustCall((err) => {
assert.strictEqual(err.name, 'AggregateError');
assert.strictEqual(err.code, 123);
assert.strictEqual(err.errors.length, 2);
assert.strictEqual(err.errors[0].message, 'INTERNAL_ERROR');
assert.strictEqual(err.errors[1].message, 'CLOSE_ERROR');
return true;
}));
} finally {
Object.defineProperty(FileHandle.prototype, 'fd', originalFd);
}
}
(async function() {
tmpdir.refresh();
await checkAggregateError((filePath) => truncate(filePath));
await checkAggregateError((filePath) => readFile(filePath));
// More than one write chunk (512 KiB), so that writeFile(path) goes through
// a FileHandle as well.
await checkAggregateError((filePath) => writeFile(filePath, '123'.repeat(200_000)));
if (common.isMacOS) {
await checkAggregateError((filePath) => lchmod(filePath, 0o777));
}
})().then(common.mustCall());