Uh oh!
There was an error while loading. Please reload this page.
stream: avoid drain for sync streams - #32887
Conversation
Previously a sync writable receiving chunks larger than highwatermark would unecessarily ping pong needDrain.
ronag
commented
Apr 16, 2020
Not sure if this should be a semver-major. |
nodejs-github-bot
commented
Apr 17, 2020
lpinca
commented
Apr 17, 2020
This looks great if it does not cause issues in user-land (it will almost certainly break ws tests). |
ronag
commented
Apr 17, 2020
I'll run a CITGM. |
CITGM looks good apart from |
lpinca
commented
Apr 18, 2020
It should be just a matter of fixing those two failing tests (not sure how), core functionality should not be broken. |
lpinca
commented
Apr 18, 2020
Perhaps wait until #32780 lands so I don't have to fix them twice. |
ronag
commented
Apr 22, 2020
@nodejs/streams |
| // We must ensure that previous needDrain will not be reset to false. | ||
| if (!ret) | ||
| state.needDrain = true; |
There was a problem hiding this comment.
I would add a few comments about this change here.
There was a problem hiding this comment.
I'm not sure what kind of comment?
mcollina
commented
Apr 22, 2020
I tagged this as "dont-land" on everything but v14.x. |
Do not rely on the `'drain'` event for synchronous writes. Refs: nodejs/node#32887
I've fixed the failing tests: websockets/ws@18d773d. |
nodejs-github-bot
commented
Apr 25, 2020
Previously a sync writable receiving chunks larger than highwatermark would unecessarily ping pong needDrain. PR-URL: #32887 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
ronag
commented
Apr 25, 2020
Landed in 003fb53 |
Previously a sync writable receiving chunks larger than highwatermark would unecessarily ping pong needDrain. PR-URL: #32887 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
Previously a sync writable receiving chunks larger than highwatermark would unecessarily ping pong needDrain. PR-URL: #32887 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
mscdex
commented
Aug 9, 2020
@ronag This PR (found after bisecting on v14.x) seems to be causing issues for |
mcollina
commented
Aug 10, 2020
@mscdex +1 in reverting this. I would love to have a regression test added (even in ssh2 and we can add it to citgm) so that we do not regress again. |
I'm ok with revert + a comment in the code. Just a note here, reverting this might have performance impact e.g. when piping from a file stream (which has a larger highwater mark) to a sync transform stream (which has a smaller highwatermark) e.g. for hashing. It's a bit strange to me that this would cause breakage. Is ssh2 doing something funky with streams? |
p-j
commented
Aug 10, 2020
Hi, |
mcollina
commented
Aug 10, 2020
None of those examples are self-contained :/, they need an active ssh server to run. |
I've stripped down the relevant Source code'use strict';const{ Duplex, Transform }=require('stream');const{ connect, createServer }=require('net');const{ inherits }=require('util');functionChannel(protoStream,socket){conststreamOpts={highWaterMark: 2*1024*1024,allowHalfOpen: false};Duplex.call(this,streamOpts);socket.on('drain',()=>{console.log('ondrain()','waitClientDrain',this._waitSocketDrain);if(this._waitSocketDrain){this._waitSocketDrain=false;if(this._chunk)this._write(this._chunk,null,this._chunkcb);elseif(this._chunkcb)this._chunkcb();}});this._protoStream=protoStream;// outgoing datathis._waitSocketDrain=false;this._chunk=undefined;this._chunkcb=undefined;}inherits(Channel,Duplex);Channel.prototype._read=function(n){};Channel.prototype._write=function(data,encoding,cb){constprotoStream=this._protoStream;constpacketSize=64*1024;constlen=data.length;letp=0;while(len-p>0){letsliceLen=len-p;if(sliceLen>packetSize)sliceLen=packetSize;constslice=data.slice(p,p+sliceLen);constret=protoStream.sendData(slice);console.log(`Channel._write() ret = ${ret} after writing ${slice.length} byte(s)`);p+=sliceLen;if(!ret){this._waitSocketDrain=true;this._chunk=undefined;this._chunkcb=cb;break;}}console.log(`Channel._write() outside of loop; p=${p} len=${len} waitSocketDrain=${this._waitSocketDrain}`);if(len-p>0){if(p>0){// partialletbuf=Buffer.allocUnsafe(len-p);data.copy(buf,0,p);this._chunk=buf;}else{this._chunk=data;}this._chunkcb=cb;return;}if(!this._waitSocketDrain)cb();};functionProtocolStream(){Transform.call(this,{highWaterMark: 32*1024});}inherits(ProtocolStream,Transform);ProtocolStream.prototype.sendData=function(data){returnthis.push(data);};createServer(function(socket){this.close();}).listen(0,'127.0.0.1',function(){const{ address, port }=this.address();connect(port,address,function(){console.log('Client connected');constprotoStream=newProtocolStream();constchannel=newChannel(protoStream,this);channel.on('finish',()=>{console.log('============ Channel finish');this.destroy();});channel.write(Buffer.alloc(128*1024));protoStream.pipe(this).pipe(protoStream);channel.end(Buffer.alloc(128*1024));});});Executing the test on node v10.19.0 results in the following output: $ node-v10.19.0 stream-issue.jsClient connectedChannel._write() ret = false after writing 65536 byte(s)Channel._write() outside of loop; p=65536 len=131072 waitSocketDrain=trueondrain() waitClientDrain trueChannel._write() ret = false after writing 65536 byte(s)Channel._write() outside of loop; p=65536 len=65536 waitSocketDrain=trueondrain() waitClientDrain trueChannel._write() ret = false after writing 65536 byte(s)Channel._write() outside of loop; p=65536 len=131072 waitSocketDrain=trueondrain() waitClientDrain trueChannel._write() ret = false after writing 65536 byte(s)Channel._write() outside of loop; p=65536 len=65536 waitSocketDrain=trueondrain() waitClientDrain true============ Channel finish
$Executing the test on node v14.1.0 or later results in the following output: $ node-v14.7.0 stream-issue.jsClient connectedChannel._write() ret = false after writing 65536 byte(s)Channel._write() outside of loop; p=65536 len=131072 waitSocketDrain=true(... and the process never exits) |
Looks like it ends up making an incorrect assumption regarding whether or not I think this is weird/incorrect/bug usage and node streams are behaving correctly. However, I guess it is is breaking change so if no one feels strongly about the performance loss I think we just revert it. |
mcollina
commented
Aug 12, 2020
theophilusx
commented
Aug 12, 2020
via email
wrt to ssh2 and being hard to spot. My testing found this error to be
(or appear to be) intermittent. i.e. I could run the same test 5 times
and it would not fail until the 5th time (with same test input), but
sometimes it would fail on the first test, or second etc. When I tried
testing with input data sizes which increased by 10k per run, if I
started with a relatively small size e.g. 90k, it would not fail (I wold
kill the test when it reached 50Mb). If on the other hand, I started
with 200k or 500k, it would usually fail immediately.
I also found that if I was connecting with a destination of localhost,
rather than a remote sftp server or one running on a vbox image, I did
not see the failure, though I didn't test as extensively with this
target.
It is quite likely the existing tests just didn't trigger the issue.
So fixing in ssh2 will be a little challenging given how difficult it is
to know if the issue is fixed or just hasn't been triggered. Reverting
and putting into v15 would at least buy some time to try and find the
right fix for ssh2. Just my 2c - ssh2/ssh2-streams author may have a
different view. Happy to assist with testing if that is at all helpful. |
Not to do the "+1" thing here, but we've seen this regression in the wild with the (quite popular) Is there any chance of getting this reverted? |
Previously a sync writable receiving chunks
larger than highwatermark would unecessarily
ping pong needDrain.
300% improvement for sync streams when chunks are bigger than HWM.
Checklist
make -j4 test(UNIX), orvcbuild test(Windows) passes