-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
http2: ServerHttp2Session#close
does not allow "any existing streams to complete on their own"
#42713
Comments
It seems "fixed" in |
The documentation for
I don't understand how that could possibly be compatible with forcibly ending an existing stream when |
I will double check, but I believe I was also seeing this issue on 17.x.x. When I did the original diagnosis in another library that led to @murgatroid99 finding the issue with node I was developing locally on OSX with node 17. |
@artificial-aidan Are you getting the unexpected behavior in v17? I'm on linux, and it's working fine. |
I will check later today, not at a computer right now. But that's what I recall. |
So this is more confusing. @murgatroid99 using the example you provided I don't see the issue on Edit: I just confirmed this is the case. On |
Yes.
|
This is not 100% fixed. The following code sample demonstrates a remaining behavior difference between Node 16.6 and 17.9 that breaks grpc-js: const http2 = require('http2');
const {
HTTP2_HEADER_PATH,
HTTP2_HEADER_STATUS,
HTTP2_HEADER_TE,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_AUTHORITY
} = http2.constants;
const server = http2.createServer();
server.on('stream', (stream, headers) => {
stream.on('wantTrailers', () => {
console.log('sending trailers');
stream.sendTrailers({ xyz: 'abc' });
});
setTimeout(() => {
stream.respond({[HTTP2_HEADER_STATUS]: 200}, {waitForTrailers: true});
stream.write('some data');
stream.end();
}, 2000);
});
server.on('session', session => {
setTimeout(() => {
server.close(() => console.log('server close completed'));
session.close(() => console.log('session close completed'));
}, 1000);
});
server.listen(0, () => {
const port = server.address().port;
const client = http2.connect(`http://localhost:${port}`);
client.socket.on('close', () => {
console.log('Client socket closed');
})
const startTime = new Date();
const req = client.request({
[HTTP2_HEADER_PATH]: '/'
});
req.end();
req.on('response', (headers) => {
console.log(headers[HTTP2_HEADER_STATUS]);
});
req.on('data', (chunk) => {console.log('received data')});
req.on('end', () => {console.log('received end')});
req.on('trailers', trailers => {console.log('received trailers')});
req.on('close', () => {
const endTime = new Date();
console.log(`Stream closed with code ${req.rstCode} after ${endTime - startTime}ms`);
});
}); On Node 16.6.2, this outputs
On Node 17.9.0, it outputs
Note the lack of the |
@murgatroid99 Thanks for the update. I'll try to look at it pretty soon (traveling atm) |
@murgatroid99 just tried to reproduce the issue and I'm always getting the rafaelgss@rafaelgss:~/repos/os/node$ node -v
v17.9.0
rafaelgss@rafaelgss:~/repos/os/node$ node issue.js
sending trailers
200
received data
received trailers
received end
Stream closed with code 0 after 2020ms
session close completed
server close completed
Client socket closed |
I screwed something up with synchronizing changes I made to my test code and my comment. I also do not see the issue with the test code I put into that comment, but I do see the issue if I include Test codeconst http2 = require('http2');
const {
HTTP2_HEADER_PATH,
HTTP2_HEADER_STATUS,
HTTP2_HEADER_TE,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_AUTHORITY
} = http2.constants;
const server = http2.createServer();
server.on('stream', (stream, headers) => {
stream.on('wantTrailers', () => {
console.log('sending trailers');
stream.sendTrailers({ xyz: 'abc' });
});
setTimeout(() => {
stream.respond({[HTTP2_HEADER_STATUS]: 200}, {waitForTrailers: true});
stream.write('some data');
stream.end();
}, 2000);
});
server.on('session', session => {
setTimeout(() => {
server.close(() => console.log('server close completed'));
session.close(() => console.log('session close completed'));
}, 1000);
});
server.listen(0, () => {
const port = server.address().port;
const client = http2.connect(`http://localhost:${port}`);
client.socket.on('close', () => {
console.log('Client socket closed');
})
const startTime = new Date();
const req = client.request({
[HTTP2_HEADER_PATH]: '/',
[HTTP2_HEADER_METHOD]: 'POST'
});
req.end();
req.on('response', (headers) => {
console.log(headers[HTTP2_HEADER_STATUS]);
});
req.on('data', (chunk) => {console.log('received data')});
req.on('end', () => {console.log('received end')});
req.on('trailers', trailers => {console.log('received trailers')});
req.on('close', () => {
const endTime = new Date();
console.log(`Stream closed with code ${req.rstCode} after ${endTime - startTime}ms`);
});
}); |
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: nodejs#42713
I think #45153 may fix this issue |
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: nodejs#42713
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: #42713 PR-URL: #45153 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: #42713 PR-URL: #45153 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: #42713 PR-URL: #45153 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: nodejs#42713 PR-URL: nodejs#45153 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> (cherry picked from commit 71bdecb)
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: nodejs#42713 PR-URL: nodejs#45153 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> (cherry picked from commit 71bdecb)
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: #42713 PR-URL: #45153 Backport-PR-URL: #45660 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: #42713 PR-URL: #45153 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: #42713 PR-URL: #45153 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: nodejs/node#42713 PR-URL: nodejs/node#45153 Backport-PR-URL: nodejs/node#45660 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: nodejs/node#42713 PR-URL: nodejs/node#45153 Backport-PR-URL: nodejs/node#45660 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
After the stream has been marked as closed by the nghttp2 stack, there might be still pending data to be sent: trailing headers is an example of this. In that case, avoid reentering the nghttp2 stack synchronously to allow the data to be written before actually closing the stream. Fixes: #42713 PR-URL: #45153 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This reverts commit dee882e. As it's causing: nodejs#46234. nodejs#42713 to be reopened.
This seems to have been re-broken in #46721. Can we reopen this issue? |
Ping on this again. It seems that there has not been any activity since it was re-broken. |
I have been debugging this, here is what causes this problem, as far as I can tell:
One option I see to fix that is to handle the failure of Update: I have tried adding an async delay in various parts of this call stack, and it always causes a few other tests to fail. It is not yet clear if those failures are legitimate problems, or if those tests have unreasonably narrow expectations. Update 2: I have been looking in to those test failures, here is what I have found so far:
Update 3: The timeout and unexpected end events appear to be caused by delaying the callback in addition to the call to |
A detailed analysis of the cause of this bug is in my linked comment on the corresponding issue. The primary fix is the new setImmediate call in Http2Stream#_destroy, which prevents a re-entrant call into Http2Session::SendPendingData when sending trailers after the Http2Session has been shut down, allowing the trailer data to be flushed properly before the socket is closed. As a result of this change, writes can be initiated later in the lifetime of the Http2Session. So, when a JSStreamSocket is used as the underlying socket reference for an Http2Session, it needs to be able to accept write calls after it is closed. In addition, now that outgoing data can be flushed differently after a session is closed, in two tests clients receive errors that they previously did not receive. I believe the new errors are more correct, so I changed the tests to match. Fixes: #42713 Refs: #42713 (comment) PR-URL: #50202 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
A detailed analysis of the cause of this bug is in my linked comment on the corresponding issue. The primary fix is the new setImmediate call in Http2Stream#_destroy, which prevents a re-entrant call into Http2Session::SendPendingData when sending trailers after the Http2Session has been shut down, allowing the trailer data to be flushed properly before the socket is closed. As a result of this change, writes can be initiated later in the lifetime of the Http2Session. So, when a JSStreamSocket is used as the underlying socket reference for an Http2Session, it needs to be able to accept write calls after it is closed. In addition, now that outgoing data can be flushed differently after a session is closed, in two tests clients receive errors that they previously did not receive. I believe the new errors are more correct, so I changed the tests to match. Fixes: #42713 Refs: #42713 (comment) PR-URL: #50202 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
A detailed analysis of the cause of this bug is in my linked comment on the corresponding issue. The primary fix is the new setImmediate call in Http2Stream#_destroy, which prevents a re-entrant call into Http2Session::SendPendingData when sending trailers after the Http2Session has been shut down, allowing the trailer data to be flushed properly before the socket is closed. As a result of this change, writes can be initiated later in the lifetime of the Http2Session. So, when a JSStreamSocket is used as the underlying socket reference for an Http2Session, it needs to be able to accept write calls after it is closed. In addition, now that outgoing data can be flushed differently after a session is closed, in two tests clients receive errors that they previously did not receive. I believe the new errors are more correct, so I changed the tests to match. Fixes: nodejs#42713 Refs: nodejs#42713 (comment) PR-URL: nodejs#50202 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
A detailed analysis of the cause of this bug is in my linked comment on the corresponding issue. The primary fix is the new setImmediate call in Http2Stream#_destroy, which prevents a re-entrant call into Http2Session::SendPendingData when sending trailers after the Http2Session has been shut down, allowing the trailer data to be flushed properly before the socket is closed. As a result of this change, writes can be initiated later in the lifetime of the Http2Session. So, when a JSStreamSocket is used as the underlying socket reference for an Http2Session, it needs to be able to accept write calls after it is closed. In addition, now that outgoing data can be flushed differently after a session is closed, in two tests clients receive errors that they previously did not receive. I believe the new errors are more correct, so I changed the tests to match. Fixes: #42713 Refs: #42713 (comment) PR-URL: #50202 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
A detailed analysis of the cause of this bug is in my linked comment on the corresponding issue. The primary fix is the new setImmediate call in Http2Stream#_destroy, which prevents a re-entrant call into Http2Session::SendPendingData when sending trailers after the Http2Session has been shut down, allowing the trailer data to be flushed properly before the socket is closed. As a result of this change, writes can be initiated later in the lifetime of the Http2Session. So, when a JSStreamSocket is used as the underlying socket reference for an Http2Session, it needs to be able to accept write calls after it is closed. In addition, now that outgoing data can be flushed differently after a session is closed, in two tests clients receive errors that they previously did not receive. I believe the new errors are more correct, so I changed the tests to match. Fixes: nodejs/node#42713 Refs: nodejs/node#42713 (comment)
Version
v16.7.0 (and later)
Platform
Linux mlumish.svl.corp.google.com 5.15.15-1rodete2-amd64 #1 SMP Debian 5.15.15-1rodete2 (2022-02-23) x86_64 GNU/Linux
Subsystem
http2
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
100%
What is the expected behavior?
The correct output, as seen on Node 16.6 and earlier:
What do you see instead?
Additional information
As noted above, this works as expected in Node versions 16.6 and earlier.
This only happens if the
stream.respond
call is delayed in thestream
handler. If that is not delayed butstream.end
is delayed, this failure does not occur.Increasing the delay before responding in the
stream
event handler increases the time before theclose
event is logged, even though the result of the call is fully determined once the server session is closed, making this bug worse for long-running workloads.The text was updated successfully, but these errors were encountered: