Skip to content

Commit 839964e

Browse files
pipobscuretargos
authored andcommitted
fs: allow correct handling of burst in fs-events with AsyncIterator
PR-URL: #58490 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent 7e34aa4 commit 839964e

7 files changed

Lines changed: 131 additions & 8 deletions

File tree

‎doc/api/errors.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,13 @@ Path is a directory.
13651365
An attempt has been made to read a file whose size is larger than the maximum
13661366
allowed size for a `Buffer`.
13671367

1368+
<aid="ERR_FS_WATCH_QUEUE_OVERFLOW"></a>
1369+
1370+
### `ERR_FS_WATCH_QUEUE_OVERFLOW`
1371+
1372+
The number of file system events queued without being handled exceeded the size specified in
1373+
`maxQueue` in `fs.watch()`.
1374+
13681375
<aid="ERR_HTTP2_ALTSVC_INVALID_ORIGIN"></a>
13691376

13701377
### `ERR_HTTP2_ALTSVC_INVALID_ORIGIN`

‎doc/api/fs.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,6 +1797,11 @@ added:
17971797
filename passed to the listener. **Default:** `'utf8'`.
17981798
* `signal` {AbortSignal} An {AbortSignal} used to signal when the watcher
17991799
should stop.
1800+
* `maxQueue` {number} Specifies the number of events to queue between iterations
1801+
of the {AsyncIterator} returned. **Default:** `2048`.
1802+
* `overflow` {string} Either `'ignore'` or `'throw'` when there are more events to be
1803+
queued than `maxQueue` allows. `'ignore'` means overflow events are dropped and a
1804+
warning is emitted, while `'throw'` means to throw an exception. **Default:** `'ignore'`.
18001805
* Returns: {AsyncIterator} of objects with the properties:
18011806
* `eventType` {string} The type of change
18021807
* `filename` {string|Buffer|null} The name of the file changed.

‎lib/internal/errors.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,7 @@ E('ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY',
12191219
E('ERR_FS_CP_UNKNOWN','Cannot copy an unknown file type',SystemError);
12201220
E('ERR_FS_EISDIR','Path is a directory',SystemError,HideStackFramesError);
12211221
E('ERR_FS_FILE_TOO_LARGE','File size (%s) is greater than 2 GiB',RangeError);
1222+
E('ERR_FS_WATCH_QUEUE_OVERFLOW','fs.watch() queued more than %d events',Error);
12221223
E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN',
12231224
'HTTP/2 ALTSVC frames require a valid origin',TypeError);
12241225
E('ERR_HTTP2_ALTSVC_LENGTH',

‎lib/internal/fs/watchers.js‎

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
'use strict';
22

33
const{
4+
ArrayPrototypePush,
5+
ArrayPrototypeShift,
6+
Error,
47
FunctionPrototypeCall,
58
ObjectDefineProperty,
69
ObjectSetPrototypeOf,
@@ -12,9 +15,11 @@ const {
1215
AbortError,
1316
UVException,
1417
codes: {
18+
ERR_FS_WATCH_QUEUE_OVERFLOW,
1519
ERR_INVALID_ARG_VALUE,
1620
},
1721
}=require('internal/errors');
22+
1823
const{
1924
kEmptyObject,
2025
}=require('internal/util');
@@ -45,6 +50,8 @@ const {
4550
validateBoolean,
4651
validateObject,
4752
validateUint32,
53+
validateInteger,
54+
validateOneOf,
4855
}=require('internal/validators');
4956

5057
const{
@@ -309,11 +316,15 @@ async function* watch(filename, options = kEmptyObject) {
309316
persistent =true,
310317
recursive =false,
311318
encoding ='utf8',
319+
maxQueue =2048,
320+
overflow ='ignore',
312321
signal,
313322
}=options;
314323

315324
validateBoolean(persistent,'options.persistent');
316325
validateBoolean(recursive,'options.recursive');
326+
validateInteger(maxQueue,'options.maxQueue');
327+
validateOneOf(overflow,'options.overflow',['ignore','error']);
317328
validateAbortSignal(signal,'options.signal');
318329

319330
if(encoding&&!isEncoding(encoding)){
@@ -325,10 +336,11 @@ async function* watch(filename, options = kEmptyObject) {
325336
thrownewAbortError(undefined,{cause: signal.reason});
326337

327338
consthandle=newFSEvent();
328-
let{ promise, resolve, reject }=PromiseWithResolvers();
339+
let{ promise, resolve }=PromiseWithResolvers();
340+
constqueue=[];
329341
constoncancel=()=>{
330342
handle.close();
331-
reject(newAbortError(undefined,{cause: signal?.reason}));
343+
resolve();
332344
};
333345

334346
try{
@@ -345,11 +357,20 @@ async function* watch(filename, options = kEmptyObject) {
345357
});
346358
error.filename=filename;
347359
handle.close();
348-
reject(error);
360+
ArrayPrototypePush(queue,error);
361+
resolve();
349362
return;
350363
}
351-
352-
resolve({ eventType, filename });
364+
if(queue.length<maxQueue){
365+
ArrayPrototypePush(queue,{__proto__: null, eventType, filename });
366+
resolve();
367+
}elseif(overflow==='error'){
368+
queue.length=0;
369+
ArrayPrototypePush(queue,newERR_FS_WATCH_QUEUE_OVERFLOW(maxQueue));
370+
resolve();
371+
}else{
372+
process.emitWarning('fs.watch maxQueue exceeded');
373+
}
353374
};
354375

355376
consterr=handle.start(path,persistent,recursive,encoding);
@@ -367,10 +388,20 @@ async function* watch(filename, options = kEmptyObject) {
367388
}
368389

369390
while(!signal?.aborted){
370-
yieldawaitpromise;
371-
({ promise, resolve, reject }=PromiseWithResolvers());
391+
awaitpromise;
392+
while(queue.length){
393+
constitem=ArrayPrototypeShift(queue);
394+
if(iteminstanceofError){
395+
throwitem;
396+
}else{
397+
yielditem;
398+
}
399+
}
400+
({ promise, resolve }=PromiseWithResolvers());
401+
}
402+
if(signal?.aborted){
403+
thrownewAbortError(undefined,{cause: signal?.reason});
372404
}
373-
thrownewAbortError(undefined,{cause: signal?.reason});
374405
}finally{
375406
handle.close();
376407
signal?.removeEventListener('abort',oncancel);

‎test/parallel/parallel.status‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ test-domain-throw-error-then-throw-from-uncaught-exception-handler: PASS, FLAKY
7777
test-domain-with-abort-on-uncaught-exception: PASS, FLAKY
7878
# https://github.com/nodejs/node/issues/54346
7979
test-esm-loader-hooks-inspect-wait: PASS, FLAKY
80+
test-fs-promises-watch-iterator: SKIP
8081
# https://github.com/nodejs/node/issues/50050
8182
test-tick-processor-arguments: SKIP
8283
# https://github.com/nodejs/node/issues/54534
@@ -85,6 +86,7 @@ test-runner-run-watch: PASS, FLAKY
8586
[$system==freebsd]
8687
# https://github.com/nodejs/node/issues/54346
8788
test-esm-loader-hooks-inspect-wait: PASS, FLAKY
89+
test-fs-promises-watch-iterator: SKIP
8890

8991
[$system==aix]
9092
# https://github.com/nodejs/node/issues/54346
@@ -95,6 +97,7 @@ test-esm-loader-hooks-inspect-wait: PASS, FLAKY
9597
test-child-process-fork-net-server: SKIP
9698
test-cli-node-options: SKIP
9799
test-cluster-shared-leak: SKIP
100+
test-fs-promises-watch-iterator: SKIP
98101
test-http-writable-true-after-close: SKIP
99102
test-http2-connect-method: SKIP
100103
test-net-error-twice: SKIP
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
// This tests that when there is a burst of fs watch events, the events
3+
// emitted after the consumer receives the initial event and before the
4+
// control returns back to fs.watch() can be queued up and show up
5+
// in the next iteration.
6+
constcommon=require('../common');
7+
const{ watch, writeFile }=require('fs/promises');
8+
constfs=require('fs');
9+
constassert=require('assert');
10+
const{ join }=require('path');
11+
const{ setTimeout }=require('timers/promises');
12+
consttmpdir=require('../common/tmpdir');
13+
14+
classWatchTestCase{
15+
constructor(dirName,files){
16+
this.dirName=dirName;
17+
this.files=files;
18+
}
19+
getdirPath(){returntmpdir.resolve(this.dirName);}
20+
filePath(fileName){returnjoin(this.dirPath,fileName);}
21+
22+
asyncrun(){
23+
awaitPromise.all([this.watchFiles(),this.writeFiles()]);
24+
assert(!this.files.length);
25+
}
26+
asyncwatchFiles(){
27+
constwatcher=watch(this.dirPath);
28+
forawait(constevtofwatcher){
29+
constidx=this.files.indexOf(evt.filename);
30+
if(idx<0)continue;
31+
this.files.splice(idx,1);
32+
awaitsetTimeout(common.platformTimeout(100));
33+
if(!this.files.length)break;
34+
}
35+
}
36+
asyncwriteFiles(){
37+
for(constfileNameof[...this.files]){
38+
awaitwriteFile(this.filePath(fileName),Date.now()+fileName.repeat(1e4));
39+
}
40+
awaitsetTimeout(common.platformTimeout(100));
41+
}
42+
}
43+
44+
constkCases=[
45+
// Watch on a directory should callback with a filename on supported systems
46+
newWatchTestCase(
47+
'watch1',
48+
['foo','bar','baz']
49+
),
50+
];
51+
52+
tmpdir.refresh();
53+
54+
for(consttestCaseofkCases){
55+
fs.mkdirSync(testCase.dirPath);
56+
testCase.run().then(common.mustCall());
57+
}

‎test/parallel/test-fs-promises-watch.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,25 @@ assert.rejects(
123123
},
124124
{code: 'ERR_INVALID_ARG_TYPE'}).then(common.mustCall());
125125

126+
assert.rejects(
127+
async()=>{
128+
// eslint-disable-next-line no-unused-vars, no-empty
129+
forawait(const_ofwatch('',{maxQueue: 'silly'})){}
130+
},
131+
{code: 'ERR_INVALID_ARG_TYPE'}).then(common.mustCall());
132+
assert.rejects(
133+
async()=>{
134+
// eslint-disable-next-line no-unused-vars, no-empty
135+
forawait(const_ofwatch('',{overflow: 1})){}
136+
},
137+
{code: 'ERR_INVALID_ARG_VALUE'}).then(common.mustCall());
138+
assert.rejects(
139+
async()=>{
140+
// eslint-disable-next-line no-unused-vars, no-empty
141+
forawait(const_ofwatch('',{overflow: 'barf'})){}
142+
},
143+
{code: 'ERR_INVALID_ARG_VALUE'}).then(common.mustCall());
144+
126145
(async()=>{
127146
constac=newAbortController();
128147
const{ signal }=ac;

0 commit comments

Comments
 (0)