You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Duplex.from({ writable, readable }) breaks on backpressure #44925
import{Duplex,PassThrough,Readable,Transform}from'node:stream';describe('Duplex.from({ writable, readable })',()=>{it('flushes stream after filling buffer',async()=>{// Simple pass-through as a placeholder for more complex setupconstthrough=newPassThrough({objectMode: true});// Stream prepared values, pipe through simple duplex and async transformer for backpressureconststream=Readable.from(['foo','bar'],{objectMode: true}).pipe(Duplex.from({writable: through,readable: through})).pipe(newTransform({objectMode: true,highWaterMark: 1,// Setting 1 to force backpressure after a single itemtransform(chunk,encoding,callback){setTimeout(()=>callback(null,chunk),0);}}));// This never finishes when high water mark is reachedconstresult=awaitstream.toArray();expect(result).toEqual(['foo','bar']);});});
How often does it reproduce? Is there a required condition?
The stream will temporarily stop reading data from the underlying resource, and toArray method is an async function, so nodejs will consider the promise is not resolved yet, and exit with code 13. You can set:
You're calling await stream.toArray() inside an async function - i.e., a promise - but what awaits that promise? Try excluding vitest and using only built-in modules.
I think you are missing the point. It is not about a specific highWaterMark number, it is about resuming the stream after the buffer limit is reached (btw. I have updated the test in the original comment).
And vitest and toArray here are also irrelevant - the stream never correctly resumes after backpressure occurs. That is the reason the promise is never resolved. If you change hightWaterMark so that the backpressure is never an issue the test passes.
Without vitest and promises (I expect to see foo and bar in the console, but get only foo):
const{ PassThrough, Duplex, Transform, Readable }=require('node:stream');// Simple pass-through as a placeholder for more complex setupconstthrough=newPassThrough({objectMode: true});// Stream prepared values, pipe through simple duplex and async transformer for backpressureReadable.from(['foo','bar'],{objectMode: true}).pipe(Duplex.from({writable: through,readable: through})).pipe(newTransform({objectMode: true,highWaterMark: 1,// Setting 1 to force backpressure after a single itemtransform(chunk,encoding,callback){setTimeout(()=>callback(null,chunk),0);}})).on('data',chunk=>console.log(chunk));
changed the title [-]Duplex.from({ writable, readable }) breaks when the internal buffers are full[/-][+]Duplex.from({ writable, readable }) breaks on backpressure[/+]on Oct 9, 2022
Tried to clone NodeJS repository to write a test for this and found out there are other things broken :(. First thing I did was to change this line to make some existing test fail and start from there
Interesting, if I implement the logic with new Duplex also works fine with setTimeout.
It can be further narrow down to Duplex.from is not compatible to setTimeout?
import{Duplex,PassThrough,Readable,Transform}from'node:stream';// Hold node process for 3s to see resultsetTimeout(()=>{},2999)// Simple pass-through as a placeholder for more complex setupconstthrough=newPassThrough({objectMode: true,highWaterMark: 1,transform(chunk,encoding,callback){console.log('passthrough',chunk)callback(null,chunk)}});// Self implement of duplexconstduplex=newDuplex({readableObjectMode: true,writableObjectMode: true,read(size){returnthis.push(passthrough.read(size))},write(chunk,encoding,callback){passthrough.write(chunk,encoding,callback)}})// Stream prepared values, pipe through simple duplex and async transformer for backpressureReadable.from(['foo','bar','baz'])// working with self implemented duplex.pipe(duplex)// not working with Duplex.from// .pipe(Duplex.from({// writable: passthrough,// readable: passthrough// })).pipe(newTransform({objectMode: true,highWaterMark: 1,// Setting 1 to force backpressure after a single itemtransform(chunk,encoding,callback){console.log('transform',chunk)// setTimeout is not working with Duplex.fromsetTimeout(()=>{callback(null,chunk)},100);}})).on('data',(chunk)=>{console.log('onData',chunk)})
The duplexify logic is too complicated and hard to follow. Hope my finding give some insight to the others to troubleshoot deeper.
I have added failing test here - https://github.com/pavelhoral/node/tree/duplex-issue / pavelhoral@69c7752. The interesting thing is that it fails even without forcing the asynchronous processing (i.e. setTimeout), which I thought was significant part of the issue. So I am not that sure about anything anymore 🗡️
I think this is not associated with toArray, either use Promise or nextTick can not reproduce the problem. And I have found the difference between use setTimeout and do not use: https://github.com/nodejs/node/blob/main/lib/internal/streams/transform.js#L181-L184
Using setTimeout will first trigger this.push(val), but val is null and second is foo.
But if you do not use setTimeout, the first is foo and second is bar. I'm trying to find more info, but the logic is too complicated
Yes, it should be when the value is resolved or pushed in the next event cycle. Then, the stream do not process remaining.
The symptoms is using readable.toArray which is async iterators and setTimeout.
Unlike the above two, process.nextTick is resolved in the current event cycle. So, it does cause any problem.
Activity
pavelhoral commentedon Oct 8, 2022
Maybe a better issue title would be
Duplex.from({ writable, readable })breaks on backpressure?xtx1130 commentedon Oct 9, 2022
In
objectMode,highWaterMarkdefault set to16, please see: https://github.com/nodejs/node/blob/v18.10.0/lib/internal/streams/state.js#L15-L17The stream will temporarily stop reading data from the underlying resource, and
toArraymethod is an async function, so nodejs will consider the promise is not resolved yet, and exit with code 13. You can set:to avoid this.
rluvaton commentedon Oct 9, 2022
@xtx1130 this sounds like a bug, no?
bnoordhuis commentedon Oct 9, 2022
You're calling
await stream.toArray()inside an async function - i.e., a promise - but what awaits that promise? Try excluding vitest and using only built-in modules.pavelhoral commentedon Oct 9, 2022
I think you are missing the point. It is not about a specific
highWaterMarknumber, it is about resuming the stream after the buffer limit is reached (btw. I have updated the test in the original comment).And
vitestandtoArrayhere are also irrelevant - the stream never correctly resumes after backpressure occurs. That is the reason the promise is never resolved. If you changehightWaterMarkso that the backpressure is never an issue the test passes.pavelhoral commentedon Oct 9, 2022
Without vitest and promises (I expect to see
fooandbarin the console, but get onlyfoo):[-]Duplex.from({ writable, readable }) breaks when the internal buffers are full[/-][+]Duplex.from({ writable, readable }) breaks on backpressure[/+]climba03003 commentedon Oct 11, 2022
Seems like
setTimeoutinside thetransformbreak something.Changing
setTimeouttoprocess.nextTickworks perfectly without error.targos commentedon Oct 11, 2022
@nodejs/streams
pavelhoral commentedon Oct 11, 2022
Tried to clone NodeJS repository to write a test for this and found out there are other things broken :(. First thing I did was to change this line to make some existing test fail and start from there
node/test/parallel/test-stream-duplex-from.js
Line 143 in 36805e8
ronag commentedon Oct 11, 2022
I'm a little low on time atm. But if someone can improve tests (i.e. add failing tests), then I could try to have a quick look at this.
climba03003 commentedon Oct 11, 2022
Interesting, if I implement the logic with
new Duplexalso works fine withsetTimeout.It can be further narrow down to
Duplex.fromis not compatible tosetTimeout?The
duplexifylogic is too complicated and hard to follow. Hope my finding give some insight to the others to troubleshoot deeper.pavelhoral commentedon Oct 11, 2022
I have added failing test here - https://github.com/pavelhoral/node/tree/duplex-issue / pavelhoral@69c7752. The interesting thing is that it fails even without forcing the asynchronous processing (i.e.
setTimeout), which I thought was significant part of the issue. So I am not that sure about anything anymore 🗡️cc @ronag
P.S.: there is another issue in tests there #44925 (comment) that should be fixed - pavelhoral@d5c069b
1 remaining item
xtx1130 commentedon Oct 11, 2022
I think this is not associated with
toArray, either usePromiseornextTickcan not reproduce the problem. And I have found the difference between usesetTimeoutand do not use: https://github.com/nodejs/node/blob/main/lib/internal/streams/transform.js#L181-L184Using
setTimeoutwill first triggerthis.push(val), butvalisnulland second isfoo.But if you do not use
setTimeout, the first isfooand second isbar. I'm trying to find more info, but the logic is too complicatedclimba03003 commentedon Oct 11, 2022
Yes, it should be when the value is resolved or pushed in the
next event cycle. Then, the stream do not process remaining.The symptoms is using
readable.toArraywhich isasync iteratorsandsetTimeout.Unlike the above two,
process.nextTickis resolved in the currentevent cycle. So, it does cause any problem.climba03003 commentedon Oct 11, 2022
@xtx1130
Removed all the unnecessary code. It should be small enough to see the symptoms.
Async IteratorssetTimeout,setImmediatextx1130 commentedon Oct 11, 2022
@climba03003
highWaterMarkparam is needed I think.climba03003 commentedon Oct 11, 2022
@xtx1130 Updated
pavelhoral commentedon Oct 14, 2022
Does this have anything to do with the observed behaviour?
node/lib/internal/streams/transform.js
Line 99 in bda460d
Maybe the issue has to do more with
Transform/PassThroughthanDuplex.from/Duplexify?ronag commentedon Oct 23, 2022
Simpler tes:
Seems related to pause, tick, tick and resume... somehow... note there has to be 2 ticks before resume.
stream: fix duplexify premature destroy
stream: fix duplexify premature destroy
stream: fix duplexify premature destroy
stream: fix duplexify premature destroy