GeezerVison is showing github.com — http2: fix graceful session close · nodejs/node@a365564
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/http2/core.js +9-2Lines changed: 9 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1068,6 +1068,7 @@ function setupHandle(socket, type, options) { | 1068 | 1068 | if (typeof options.selectPadding === 'function') | 1069 | 1069 | this[kSelectPadding] = options.selectPadding; | 1070 | 1070 | handle.consume(socket._handle); | | 1071 | + handle.ongracefulclosecomplete = this[kMaybeDestroy].bind(this, null); | 1071 | 1072 |
| 1072 | 1073 | this[kHandle] = handle; | 1073 | 1074 | if (this[kNativeFields]) { | @@ -1589,6 +1590,10 @@ class Http2Session extends EventEmitter { | 1589 | 1590 | if (typeof callback === 'function') | 1590 | 1591 | this.once('close', callback); | 1591 | 1592 | this.goaway(); | | 1593 | + const handle = this[kHandle]; | | 1594 | + if (handle) { | | 1595 | + handle.setGracefulClose(); | | 1596 | + } | 1592 | 1597 | this[kMaybeDestroy](); | 1593 | 1598 | } | 1594 | 1599 |
| @@ -1609,11 +1614,13 @@ class Http2Session extends EventEmitter { | 1609 | 1614 | // * session is closed and there are no more pending or open streams | 1610 | 1615 | [kMaybeDestroy](error) { | 1611 | 1616 | if (error == null) { | | 1617 | + const handle = this[kHandle]; | | 1618 | + const hasPendingData = !!handle && handle.hasPendingData(); | 1612 | 1619 | const state = this[kState]; | 1613 | 1620 | // Do not destroy if we're not closed and there are pending/open streams | 1614 | 1621 | if (!this.closed || | 1615 | 1622 | state.streams.size > 0 || | 1616 | | - state.pendingStreams.size > 0) { | | 1623 | + state.pendingStreams.size > 0 || hasPendingData) { | 1617 | 1624 | return; | 1618 | 1625 | } | 1619 | 1626 | } | @@ -3300,7 +3307,7 @@ function socketOnClose() { | 3300 | 3307 | state.streams.forEach((stream) => stream.close(NGHTTP2_CANCEL)); | 3301 | 3308 | state.pendingStreams.forEach((stream) => stream.close(NGHTTP2_CANCEL)); | 3302 | 3309 | session.close(); | 3303 | | - session[kMaybeDestroy](err); | | 3310 | + closeSession(session, NGHTTP2_NO_ERROR, err); | 3304 | 3311 | } | 3305 | 3312 | } | 3306 | 3313 |
| |
Collapse file Copy file name to clipboardExpand all lines: src/env_properties.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 285 | 285 | V(onsignal_string, "onsignal") \ | 286 | 286 | V(onunpipe_string, "onunpipe") \ | 287 | 287 | V(onwrite_string, "onwrite") \ | | 288 | + V(ongracefulclosecomplete_string, "ongracefulclosecomplete") \ | 288 | 289 | V(openssl_error_stack, "opensslErrorStack") \ | 289 | 290 | V(options_string, "options") \ | 290 | 291 | V(order_string, "order") \ | |
Collapse file Copy file name to clipboardExpand all lines: src/node_http2.cc +58-1Lines changed: 58 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -559,7 +559,8 @@ Http2Session::Http2Session(Http2State* http2_state, | 559 | 559 | : AsyncWrap(http2_state->env(), wrap, AsyncWrap::PROVIDER_HTTP2SESSION), | 560 | 560 | js_fields_(http2_state->env()->isolate()), | 561 | 561 | session_type_(type), | 562 | | - http2_state_(http2_state) { | | 562 | + http2_state_(http2_state), | | 563 | + graceful_close_initiated_(false) { | 563 | 564 | MakeWeak(); | 564 | 565 | statistics_.session_type = type; | 565 | 566 | statistics_.start_time = uv_hrtime(); | @@ -765,6 +766,24 @@ void Http2Stream::EmitStatistics() { | 765 | 766 | }); | 766 | 767 | } | 767 | 768 |
| | 769 | +void Http2Session::HasPendingData(const FunctionCallbackInfo<Value>& args) { | | 770 | + Http2Session* session; | | 771 | + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); | | 772 | + args.GetReturnValue().Set(session->HasPendingData()); | | 773 | +} | | 774 | + | | 775 | +bool Http2Session::HasPendingData() const { | | 776 | + nghttp2_session* session = session_.get(); | | 777 | + int want_write = nghttp2_session_want_write(session); | | 778 | + // It is expected that want_read will alway be 0 if graceful | | 779 | + // session close is initiated and goaway frame is sent. | | 780 | + int want_read = nghttp2_session_want_read(session); | | 781 | + if (want_write == 0 && want_read == 0) { | | 782 | + return false; | | 783 | + } | | 784 | + return true; | | 785 | +} | | 786 | + | 768 | 787 | void Http2Session::EmitStatistics() { | 769 | 788 | if (!HasHttp2Observer(env())) [[likely]] { | 770 | 789 | return; | @@ -1743,6 +1762,7 @@ void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) { | 1743 | 1762 | void Http2Session::OnStreamAfterWrite(WriteWrap* w, int status) { | 1744 | 1763 | Debug(this, "write finished with status %d", status); | 1745 | 1764 |
| | 1765 | + MaybeNotifyGracefulCloseComplete(); | 1746 | 1766 | CHECK(is_write_in_progress()); | 1747 | 1767 | set_write_in_progress(false); | 1748 | 1768 |
| @@ -1965,6 +1985,7 @@ uint8_t Http2Session::SendPendingData() { | 1965 | 1985 | if (!res.async) { | 1966 | 1986 | set_write_in_progress(false); | 1967 | 1987 | ClearOutgoing(res.err); | | 1988 | + MaybeNotifyGracefulCloseComplete(); | 1968 | 1989 | } | 1969 | 1990 |
| 1970 | 1991 | MaybeStopReading(); | @@ -3476,6 +3497,8 @@ void Initialize(Local<Object> target, | 3476 | 3497 | SetProtoMethod(isolate, session, "receive", Http2Session::Receive); | 3477 | 3498 | SetProtoMethod(isolate, session, "destroy", Http2Session::Destroy); | 3478 | 3499 | SetProtoMethod(isolate, session, "goaway", Http2Session::Goaway); | | 3500 | + SetProtoMethod( | | 3501 | + isolate, session, "hasPendingData", Http2Session::HasPendingData); | 3479 | 3502 | SetProtoMethod(isolate, session, "settings", Http2Session::Settings); | 3480 | 3503 | SetProtoMethod(isolate, session, "request", Http2Session::Request); | 3481 | 3504 | SetProtoMethod( | @@ -3496,6 +3519,8 @@ void Initialize(Local<Object> target, | 3496 | 3519 | "remoteSettings", | 3497 | 3520 | Http2Session::RefreshSettings<nghttp2_session_get_remote_settings, | 3498 | 3521 | false>); | | 3522 | + SetProtoMethod( | | 3523 | + isolate, session, "setGracefulClose", Http2Session::SetGracefulClose); | 3499 | 3524 | SetConstructorFunction(context, target, "Http2Session", session); | 3500 | 3525 |
| 3501 | 3526 | Local<Object> constants = Object::New(isolate); | @@ -3550,6 +3575,38 @@ void Initialize(Local<Object> target, | 3550 | 3575 | nghttp2_set_debug_vprintf_callback(NgHttp2Debug); | 3551 | 3576 | #endif | 3552 | 3577 | } | | 3578 | + | | 3579 | +void Http2Session::SetGracefulClose(const FunctionCallbackInfo<Value>& args) { | | 3580 | + Http2Session* session; | | 3581 | + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); | | 3582 | + CHECK_NOT_NULL(session); | | 3583 | + // Set the graceful close flag | | 3584 | + session->SetGracefulCloseInitiated(true); | | 3585 | + | | 3586 | + Debug(session, "Setting graceful close initiated flag"); | | 3587 | +} | | 3588 | + | | 3589 | +void Http2Session::MaybeNotifyGracefulCloseComplete() { | | 3590 | + nghttp2_session* session = session_.get(); | | 3591 | + | | 3592 | + if (!IsGracefulCloseInitiated()) { | | 3593 | + return; | | 3594 | + } | | 3595 | + | | 3596 | + int want_write = nghttp2_session_want_write(session); | | 3597 | + int want_read = nghttp2_session_want_read(session); | | 3598 | + bool should_notify = (want_write == 0 && want_read == 0); | | 3599 | + | | 3600 | + if (should_notify) { | | 3601 | + Debug(this, "Notifying JS after write in graceful close mode"); | | 3602 | + | | 3603 | + // Make the callback to JavaScript | | 3604 | + HandleScope scope(env()->isolate()); | | 3605 | + MakeCallback(env()->ongracefulclosecomplete_string(), 0, nullptr); | | 3606 | + } | | 3607 | + | | 3608 | + return; | | 3609 | +} | 3553 | 3610 | } // namespace http2 | 3554 | 3611 | } // namespace node | 3555 | 3612 |
| |
Collapse file Copy file name to clipboardExpand all lines: src/node_http2.h +15Lines changed: 15 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -712,6 +712,7 @@ class Http2Session : public AsyncWrap, | 712 | 712 | static void Consume(const v8::FunctionCallbackInfo<v8::Value>& args); | 713 | 713 | static void Receive(const v8::FunctionCallbackInfo<v8::Value>& args); | 714 | 714 | static void Destroy(const v8::FunctionCallbackInfo<v8::Value>& args); | | 715 | + static void HasPendingData(const v8::FunctionCallbackInfo<v8::Value>& args); | 715 | 716 | static void Settings(const v8::FunctionCallbackInfo<v8::Value>& args); | 716 | 717 | static void Request(const v8::FunctionCallbackInfo<v8::Value>& args); | 717 | 718 | static void SetNextStreamID(const v8::FunctionCallbackInfo<v8::Value>& args); | @@ -723,6 +724,7 @@ class Http2Session : public AsyncWrap, | 723 | 724 | static void Ping(const v8::FunctionCallbackInfo<v8::Value>& args); | 724 | 725 | static void AltSvc(const v8::FunctionCallbackInfo<v8::Value>& args); | 725 | 726 | static void Origin(const v8::FunctionCallbackInfo<v8::Value>& args); | | 727 | + static void SetGracefulClose(const v8::FunctionCallbackInfo<v8::Value>& args); | 726 | 728 |
| 727 | 729 | template <get_setting fn, bool local> | 728 | 730 | static void RefreshSettings(const v8::FunctionCallbackInfo<v8::Value>& args); | @@ -735,6 +737,7 @@ class Http2Session : public AsyncWrap, | 735 | 737 |
| 736 | 738 | BaseObjectPtr<Http2Ping> PopPing(); | 737 | 739 | bool AddPing(const uint8_t* data, v8::Local<v8::Function> callback); | | 740 | + bool HasPendingData() const; | 738 | 741 |
| 739 | 742 | BaseObjectPtr<Http2Settings> PopSettings(); | 740 | 743 | bool AddSettings(v8::Local<v8::Function> callback); | @@ -785,6 +788,13 @@ class Http2Session : public AsyncWrap, | 785 | 788 |
| 786 | 789 | Statistics statistics_ = {}; | 787 | 790 |
| | 791 | + bool IsGracefulCloseInitiated() const { | | 792 | + return graceful_close_initiated_; | | 793 | + } | | 794 | + void SetGracefulCloseInitiated(bool value) { | | 795 | + graceful_close_initiated_ = value; | | 796 | + } | | 797 | + | 788 | 798 | private: | 789 | 799 | void EmitStatistics(); | 790 | 800 |
| @@ -951,8 +961,13 @@ class Http2Session : public AsyncWrap, | 951 | 961 | void CopyDataIntoOutgoing(const uint8_t* src, size_t src_length); | 952 | 962 | void ClearOutgoing(int status); | 953 | 963 |
| | 964 | + void MaybeNotifyGracefulCloseComplete(); | | 965 | + | 954 | 966 | friend class Http2Scope; | 955 | 967 | friend class Http2StreamListener; | | 968 | + | | 969 | + // Flag to indicate that JavaScript has initiated a graceful closure | | 970 | + bool graceful_close_initiated_ = false; | 956 | 971 | }; | 957 | 972 |
| 958 | 973 | struct Http2SessionPerformanceEntryTraits { | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-http2-client-rststream-before-connect.js +11-6Lines changed: 11 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -5,16 +5,23 @@ if (!common.hasCrypto) | 5 | 5 | common.skip('missing crypto'); | 6 | 6 | const assert = require('assert'); | 7 | 7 | const h2 = require('http2'); | | 8 | +let client; | 8 | 9 |
| 9 | 10 | const server = h2.createServer(); | 10 | 11 | server.on('stream', (stream) => { | 11 | | - stream.on('close', common.mustCall()); | 12 | | - stream.respond(); | 13 | | - stream.end('ok'); | | 12 | + stream.on('close', common.mustCall(() => { | | 13 | + client.close(); | | 14 | + server.close(); | | 15 | + })); | | 16 | + stream.on('error', common.expectsError({ | | 17 | + code: 'ERR_HTTP2_STREAM_ERROR', | | 18 | + name: 'Error', | | 19 | + message: 'Stream closed with error code NGHTTP2_PROTOCOL_ERROR' | | 20 | + })); | 14 | 21 | }); | 15 | 22 |
| 16 | 23 | server.listen(0, common.mustCall(() => { | 17 | | - const client = h2.connect(`http://localhost:${server.address().port}`); | | 24 | + client = h2.connect(`http://localhost:${server.address().port}`); | 18 | 25 | const req = client.request(); | 19 | 26 | const closeCode = 1; | 20 | 27 |
| @@ -52,8 +59,6 @@ server.listen(0, common.mustCall(() => { | 52 | 59 | req.on('close', common.mustCall(() => { | 53 | 60 | assert.strictEqual(req.destroyed, true); | 54 | 61 | assert.strictEqual(req.rstCode, closeCode); | 55 | | - server.close(); | 56 | | - client.close(); | 57 | 62 | })); | 58 | 63 |
| 59 | 64 | req.on('error', common.expectsError({ | |
Collapse file Copy file name to clipboard +48Lines changed: 48 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const common = require('../common'); | | 4 | +if (!common.hasCrypto) | | 5 | + common.skip('missing crypto'); | | 6 | +const assert = require('assert'); | | 7 | +const h2 = require('http2'); | | 8 | + | | 9 | +const server = h2.createServer(); | | 10 | +let session; | | 11 | + | | 12 | +server.on('session', common.mustCall(function(s) { | | 13 | + session = s; | | 14 | + session.on('close', common.mustCall(function() { | | 15 | + server.close(); | | 16 | + })); | | 17 | +})); | | 18 | + | | 19 | +server.listen(0, common.mustCall(function() { | | 20 | + const port = server.address().port; | | 21 | + | | 22 | + const url = `http://localhost:${port}`; | | 23 | + const client = h2.connect(url, common.mustCall(function() { | | 24 | + const headers = { | | 25 | + ':path': '/', | | 26 | + ':method': 'GET', | | 27 | + ':scheme': 'http', | | 28 | + ':authority': `localhost:${port}` | | 29 | + }; | | 30 | + const request = client.request(headers); | | 31 | + request.on('response', common.mustCall(function(headers) { | | 32 | + assert.strictEqual(headers[':status'], 200); | | 33 | + }, 1)); | | 34 | + request.on('end', common.mustCall(function() { | | 35 | + client.close(); | | 36 | + })); | | 37 | + request.end(); | | 38 | + request.resume(); | | 39 | + })); | | 40 | + client.on('goaway', common.mustCallAtLeast(1)); | | 41 | +})); | | 42 | + | | 43 | +server.once('request', common.mustCall(function(request, response) { | | 44 | + response.on('finish', common.mustCall(function() { | | 45 | + session.close(); | | 46 | + })); | | 47 | + response.end(); | | 48 | +})); |
|