From c19c5b217b1a97b767ba25a8d49f906231486eff Mon Sep 17 00:00:00 2001 From: Santiago Gimeno Date: Tue, 21 Jan 2025 16:39:45 +0100 Subject: [PATCH 1/2] lib: add diagnostic channels to http2 --- lib/internal/http2/core.js | 93 +++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 5809b38433..6d269db2b8 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -187,6 +187,18 @@ const { UV_EOF } = internalBinding('uv'); const { StreamPipe } = internalBinding('stream_pipe'); const { _connectionListener: httpConnectionListener } = http; + +const dc = require('diagnostics_channel'); +const onClientStreamCreatedChannel = dc.channel('http2.client.stream.created'); +const onClientStreamStartChannel = dc.channel('http2.client.stream.start'); +const onClientStreamErrorChannel = dc.channel('http2.client.stream.error'); +const onClientStreamFinishChannel = dc.channel('http2.client.stream.finish'); +const onClientStreamCloseChannel = dc.channel('http2.client.stream.close'); +const onServerStreamStartChannel = dc.channel('http2.server.stream.start'); +const onServerStreamErrorChannel = dc.channel('http2.server.stream.error'); +const onServerStreamFinishChannel = dc.channel('http2.server.stream.finish'); +const onServerStreamCloseChannel = dc.channel('http2.server.stream.close'); + let debug = require('internal/util/debuglog').debuglog('http2', (fn) => { debug = fn; }); @@ -375,9 +387,22 @@ function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) { stream.end(); stream[kState].flags |= STREAM_FLAGS_HEAD_REQUEST; } + + if (onServerStreamStartChannel.hasSubscribers) { + onServerStreamStartChannel.publish({ + stream, + headers, + }); + } } else { // eslint-disable-next-line no-use-before-define stream = new ClientHttp2Stream(session, handle, id, {}); + if (onClientStreamCreatedChannel.hasSubscribers) { + onClientStreamCreatedChannel.publish({ + stream, + }); + } + if (endOfStream) { stream.push(null); } @@ -416,6 +441,16 @@ function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) { reqAsync.runInAsyncScope(process.nextTick, null, emit, stream, event, obj, flags, headers); else process.nextTick(emit, stream, event, obj, flags, headers); + + if (event === 'response') { + if (onClientStreamFinishChannel.hasSubscribers) { + onClientStreamFinishChannel.publish({ + stream, + headers, + flags, + }); + } + } } if (endOfStream) { stream.push(null); @@ -766,7 +801,14 @@ function requestOnConnect(headers, options) { } return; } + this[kInit](ret.id(), ret); + if (onClientStreamStartChannel.hasSubscribers) { + onClientStreamStartChannel.publish({ + stream: this, + headers, + }); + } } // Validates that priority options are correct, specifically: @@ -1851,6 +1893,13 @@ class ClientHttp2Session extends Http2Session { } else { onConnect(); } + + if (onClientStreamCreatedChannel.hasSubscribers) { + onClientStreamCreatedChannel.publish({ + stream, + }); + } + return stream; } } @@ -1925,6 +1974,7 @@ const kSubmitRstStream = 1; const kForceRstStream = 2; function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { + const type = stream.session[kType]; const state = stream[kState]; state.flags |= STREAM_FLAGS_CLOSED; state.rstCode = code; @@ -1955,6 +2005,20 @@ function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { else stream.once('finish', finishFn); } + + if (type === NGHTTP2_SESSION_CLIENT) { + if (onClientStreamCloseChannel.hasSubscribers) { + onClientStreamCloseChannel.publish({ + stream, + code, + }); + } + } else if (onServerStreamCloseChannel.hasSubscribers) { + onServerStreamCloseChannel.publish({ + stream, + code, + }); + } } function finishCloseStream(code) { @@ -2381,6 +2445,21 @@ class Http2Stream extends Duplex { setImmediate(() => { session[kMaybeDestroy](); }); + if (err) { + if (session[kType] === NGHTTP2_SESSION_CLIENT) { + if (onClientStreamErrorChannel.hasSubscribers) { + onClientStreamErrorChannel.publish({ + stream: this, + error: err, + }); + } + } else if (onServerStreamErrorChannel.hasSubscribers) { + onServerStreamErrorChannel.publish({ + stream: this, + error: err, + }); + } + } callback(err); } // The Http2Stream can be destroyed if it has closed and if the readable @@ -2766,6 +2845,13 @@ class ServerHttp2Stream extends Http2Stream { stream[kState].flags |= STREAM_FLAGS_HEAD_REQUEST; process.nextTick(callback, null, stream, headers, 0); + + if (onServerStreamStartChannel.hasSubscribers) { + onServerStreamStartChannel.publish({ + stream, + headers, + }); + } } // Initiate a response on this Http2Stream @@ -2813,8 +2899,13 @@ class ServerHttp2Stream extends Http2Stream { } const ret = this[kHandle].respond(headersList, streamOptions); - if (ret < 0) + if (ret < 0) { this.destroy(new NghttpError(ret)); + } else if (onServerStreamFinishChannel.hasSubscribers) { + onServerStreamFinishChannel.publish({ + stream: this, + }); + } } // Initiate a response using an open FD. Note that there are fewer From ad787683948c43ef4236f610be4b729485d139e7 Mon Sep 17 00:00:00 2001 From: Santiago Gimeno Date: Tue, 21 Jan 2025 16:41:07 +0100 Subject: [PATCH 2/2] lib: add metrics support for http2 --- lib/internal/nsolid_diag.js | 49 +++ .../test-nsolid-http2-client-destroy.mjs | 311 ++++++++++++++++++ .../test-nsolid-http2-compat-errors.js | 42 +++ ...t-nsolid-http2-compat-serverrequest-end.js | 47 +++ .../test-nsolid-http2-server-rst-stream.js | 71 ++++ ...est-nsolid-http2-server-session-destroy.js | 29 ++ test/parallel/test-nsolid-http2.js | 50 +++ 7 files changed, 599 insertions(+) create mode 100644 test/parallel/test-nsolid-http2-client-destroy.mjs create mode 100644 test/parallel/test-nsolid-http2-compat-errors.js create mode 100644 test/parallel/test-nsolid-http2-compat-serverrequest-end.js create mode 100644 test/parallel/test-nsolid-http2-server-rst-stream.js create mode 100644 test/parallel/test-nsolid-http2-server-session-destroy.js create mode 100644 test/parallel/test-nsolid-http2.js diff --git a/lib/internal/nsolid_diag.js b/lib/internal/nsolid_diag.js index 77cd316acf..e2e77225a3 100644 --- a/lib/internal/nsolid_diag.js +++ b/lib/internal/nsolid_diag.js @@ -23,6 +23,8 @@ const dc = require('diagnostics_channel'); const { kHttpClientAbortCount, kHttpClientCount, + kHttpServerAbortCount, + kHttpServerCount, kSpanHttpClient, kSpanHttpMethod, kSpanHttpReqUrl, @@ -31,6 +33,9 @@ const { const undiciFetch = dc.tracingChannel('undici:fetch'); +// To lazy load the http2 constants +let http2Constants; + let tracingEnabled = false; const fetchSubscribeListener = (message, name) => {}; @@ -119,3 +124,47 @@ dc.subscribe('undici:request:error', ({ request, error }) => { } } }); + +dc.subscribe('http2.client.stream.created', ({ stream }) => { + stream[nsolid_tracer_s] = { + start: now(), + response: false, + }; +}); + +dc.subscribe('http2.client.stream.finish', ({ stream, flags }) => { + stream[nsolid_tracer_s].response = true; +}); + +dc.subscribe('http2.client.stream.close', ({ stream, code }) => { + http2Constants ||= require('internal/http2/core').constants; + const tracingInfo = stream[nsolid_tracer_s]; + if (code === http2Constants.NGHTTP2_NO_ERROR && tracingInfo.response) { + nsolid_counts[kHttpClientCount]++; + nsolidApi.pushClientBucket(now() - tracingInfo.start); + } else { + nsolid_counts[kHttpClientAbortCount]++; + } +}); + +dc.subscribe('http2.server.stream.start', ({ stream }) => { + stream[nsolid_tracer_s] = { + start: now(), + response: false, + }; +}); + +dc.subscribe('http2.server.stream.finish', ({ stream, flags }) => { + stream[nsolid_tracer_s].response = true; +}); + +dc.subscribe('http2.server.stream.close', ({ stream, code }) => { + http2Constants ||= require('internal/http2/core').constants; + const tracingInfo = stream[nsolid_tracer_s]; + if (code === http2Constants.NGHTTP2_NO_ERROR && tracingInfo.response) { + nsolid_counts[kHttpServerCount]++; + nsolidApi.pushServerBucket(now() - tracingInfo.start); + } else { + nsolid_counts[kHttpServerAbortCount]++; + } +}); diff --git a/test/parallel/test-nsolid-http2-client-destroy.mjs b/test/parallel/test-nsolid-http2-client-destroy.mjs new file mode 100644 index 0000000000..2cb03d2d08 --- /dev/null +++ b/test/parallel/test-nsolid-http2-client-destroy.mjs @@ -0,0 +1,311 @@ +// Flags: --expose-internals +import * as common from '../common/index.mjs'; +if (!common.hasCrypto) + common.skip('missing crypto'); +import assert from 'assert'; +import * as h2 from 'http2'; +import util from 'internal/http2/util'; +import { getEventListeners } from 'events'; +import nsolid from 'nsolid'; + +const kSocket = util.kSocket; + +let httpClientAbortCount = 0; +let httpServerAbortCount = 0; + +const tests = []; + +tests.push({ + name: 'Test destroy before client operations', + test: (done) => { + return new Promise((resolve) => { + const server = h2.createServer(); + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + const socket = client[kSocket]; + socket.on('close', common.mustCall(() => { + assert(socket.destroyed); + })); + + const req = client.request(); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_CANCEL', + name: 'Error', + message: 'The pending stream has been canceled' + })); + + client.destroy(); + + req.on('response', common.mustNotCall()); + + const sessionError = { + name: 'Error', + code: 'ERR_HTTP2_INVALID_SESSION', + message: 'The session has been destroyed' + }; + + assert.throws(() => client.setNextStreamID(), sessionError); + assert.throws(() => client.setLocalWindowSize(), sessionError); + assert.throws(() => client.ping(), sessionError); + assert.throws(() => client.settings({}), sessionError); + assert.throws(() => client.goaway(), sessionError); + assert.throws(() => client.request(), sessionError); + client.close(); // Should be a non-op at this point + + // Wait for setImmediate call from destroy() to complete + // so that state.destroyed is set to true + setImmediate(() => { + assert.throws(() => client.setNextStreamID(), sessionError); + assert.throws(() => client.setLocalWindowSize(), sessionError); + assert.throws(() => client.ping(), sessionError); + assert.throws(() => client.settings({}), sessionError); + assert.throws(() => client.goaway(), sessionError); + assert.throws(() => client.request(), sessionError); + client.close(); // Should be a non-op at this point + }); + + req.resume(); + req.on('end', common.mustNotCall()); + req.on('close', common.mustCall(() => { + server.close(); + httpClientAbortCount++; + resolve(); + })); + })); + }); + } +}); + +tests.push({ + name: 'Test destroy before goaway', + test: () => { + return new Promise((resolve) => { + const server = h2.createServer(); + server.on('stream', common.mustCall((stream) => { + stream.session.destroy(); + })); + + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + + client.on('close', () => { + server.close(); + // Calling destroy in here should not matter + client.destroy(); + httpClientAbortCount++; + httpServerAbortCount++; + resolve(); + }); + + client.request(); + })); + }); + } +}); + +tests.push({ + name: 'Test destroy before connect', + test: () => { + return new Promise((resolve) => { + const server = h2.createServer(); + server.on('stream', common.mustNotCall()); + + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + + server.on('connection', common.mustCall(() => { + server.close(); + client.close(); + httpClientAbortCount++; + resolve(); + })); + + const req = client.request(); + req.destroy(); + })); + }); + } +}); + +tests.push({ + name: 'Destroy with AbortSignal', + test: () => { + return new Promise((resolve) => { + const server = h2.createServer(); + const controller = new AbortController(); + + server.on('stream', common.mustNotCall()); + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + client.on('close', common.mustCall()); + + const { signal } = controller; + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + + client.on('error', common.mustCall(() => { + // After underlying stream dies, signal listener detached + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + })); + + const req = client.request({}, { signal }); + + req.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ABORT_ERR'); + assert.strictEqual(err.name, 'AbortError'); + })); + req.on('close', common.mustCall(() => { + server.close(); + httpClientAbortCount++; + resolve(); + })); + + assert.strictEqual(req.aborted, false); + assert.strictEqual(req.destroyed, false); + // Signal listener attached + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + + controller.abort(); + + assert.strictEqual(req.aborted, false); + assert.strictEqual(req.destroyed, true); + })); + }); + } +}); + +tests.push({ + name: 'Pass an already destroyed signal to abort immediately', + test: async () => { + return new Promise((resolve) => { + const server = h2.createServer(); + const controller = new AbortController(); + + server.on('stream', common.mustNotCall()); + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + client.on('close', common.mustCall()); + + const { signal } = controller; + controller.abort(); + + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + + client.on('error', common.mustCall(() => { + // After underlying stream dies, signal listener detached + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + })); + + const req = client.request({}, { signal }); + // Signal already aborted, so no event listener attached. + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); + + assert.strictEqual(req.aborted, false); + // Destroyed on same tick as request made + assert.strictEqual(req.destroyed, true); + + req.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ABORT_ERR'); + assert.strictEqual(err.name, 'AbortError'); + })); + req.on('close', common.mustCall(() => { + server.close(); + httpClientAbortCount++; + resolve(); + })); + })); + }); + } +}); + +tests.push({ + name: 'Destroy ClientHttpSession with AbortSignal', + test: async () => { + async function testH2ConnectAbort(secure) { + return new Promise((resolve) => { + const server = secure ? h2.createSecureServer() : h2.createServer(); + const controller = new AbortController(); + server.on('stream', common.mustNotCall()); + server.listen(0, common.mustCall(() => { + const { signal } = controller; + const protocol = secure ? 'https' : 'http'; + const client = h2.connect(`${protocol}://localhost:${server.address().port}`, { + signal, + }); + client.on('close', common.mustCall()); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + client.on('error', common.mustCall(common.mustCall((err) => { + assert.strictEqual(err.code, 'ABORT_ERR'); + assert.strictEqual(err.name, 'AbortError'); + }))); + const req = client.request({}, {}); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + req.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_CANCEL'); + assert.strictEqual(err.name, 'Error'); + assert.strictEqual(req.aborted, false); + assert.strictEqual(req.destroyed, true); + })); + req.on('close', common.mustCall(() => { + server.close(); + resolve(); + })); + assert.strictEqual(req.aborted, false); + assert.strictEqual(req.destroyed, false); + // Signal listener attached + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + controller.abort(); + })); + }); + } + await testH2ConnectAbort(false); + httpClientAbortCount++; + await testH2ConnectAbort(true); + httpClientAbortCount++; + } +}); + +tests.push({ + name: 'Destroy ClientHttp2Stream with AbortSignal', + test: async () => { + return new Promise((resolve) => { + const server = h2.createServer(); + const controller = new AbortController(); + + server.on('stream', common.mustCall((stream) => { + stream.on('error', common.mustNotCall()); + stream.on('close', common.mustCall(() => { + assert.strictEqual(stream.rstCode, h2.constants.NGHTTP2_CANCEL); + server.close(); + httpClientAbortCount++; + httpServerAbortCount++; + resolve(); + })); + controller.abort(); + })); + server.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + client.on('close', common.mustCall()); + + const { signal } = controller; + const req = client.request({}, { signal }); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + req.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ABORT_ERR'); + assert.strictEqual(err.name, 'AbortError'); + client.close(); + })); + req.on('close', common.mustCall()); + })); + }); + }, +}); + +for (const { name, test } of tests) { + console.log(`${name}`); + await test(); +} + +assert.strictEqual(nsolid.traceStats.httpClientCount, 0); +assert.strictEqual(nsolid.traceStats.httpClientAbortCount, httpClientAbortCount); +assert.strictEqual(nsolid.traceStats.httpServerCount, 0); +assert.strictEqual(nsolid.traceStats.httpServerAbortCount, httpServerAbortCount); diff --git a/test/parallel/test-nsolid-http2-compat-errors.js b/test/parallel/test-nsolid-http2-compat-errors.js new file mode 100644 index 0000000000..9914958bb3 --- /dev/null +++ b/test/parallel/test-nsolid-http2-compat-errors.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); +const nsolid = require('nsolid'); + +// Errors should not be reported both in Http2ServerRequest +// and Http2ServerResponse + +let expected = null; + +const server = h2.createServer(common.mustCall(function(req, res) { + res.stream.on('error', common.mustCall()); + req.on('error', common.mustNotCall()); + res.on('error', common.mustNotCall()); + req.on('aborted', common.mustCall()); + res.on('aborted', common.mustNotCall()); + + res.write('hello'); + + expected = new Error('kaboom'); + res.stream.destroy(expected); + server.close(common.mustCall(() => { + assert.strictEqual(nsolid.traceStats.httpClientCount, 1); + assert.strictEqual(nsolid.traceStats.httpClientAbortCount, 0); + assert.strictEqual(nsolid.traceStats.httpServerCount, 0); + assert.strictEqual(nsolid.traceStats.httpServerAbortCount, 1); + })); +})); + +server.listen(0, common.mustCall(function() { + const url = `http://localhost:${server.address().port}`; + const client = h2.connect(url, common.mustCall(() => { + const request = client.request(); + request.on('data', common.mustCall((chunk) => { + client.destroy(); + })); + })); +})); diff --git a/test/parallel/test-nsolid-http2-compat-serverrequest-end.js b/test/parallel/test-nsolid-http2-compat-serverrequest-end.js new file mode 100644 index 0000000000..23e5efa5ac --- /dev/null +++ b/test/parallel/test-nsolid-http2-compat-serverrequest-end.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); +const nsolid = require('nsolid'); + +// Http2ServerRequest should always end readable stream +// even on GET requests with no body + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + assert.strictEqual(request.complete, false); + request.on('data', () => {}); + request.on('end', common.mustCall(() => { + assert.strictEqual(request.complete, true); + response.on('finish', common.mustCall(function() { + // The following tests edge cases on request socket + // right after finished fires but before backing + // Http2Stream is destroyed + assert.strictEqual(request.socket.readable, request.stream.readable); + assert.strictEqual(request.socket.readable, false); + + server.close(); + })); + assert.strictEqual(response.end(), response); + })); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(() => { + const request = client.request(); + request.resume(); + request.on('end', common.mustCall(() => { + client.close(common.mustCall(() => { + assert.strictEqual(nsolid.traceStats.httpClientCount, 1); + assert.strictEqual(nsolid.traceStats.httpClientAbortCount, 0); + assert.strictEqual(nsolid.traceStats.httpServerCount, 1); + assert.strictEqual(nsolid.traceStats.httpServerAbortCount, 0); + })); + })); + })); +})); diff --git a/test/parallel/test-nsolid-http2-server-rst-stream.js b/test/parallel/test-nsolid-http2-server-rst-stream.js new file mode 100644 index 0000000000..da9690af03 --- /dev/null +++ b/test/parallel/test-nsolid-http2-server-rst-stream.js @@ -0,0 +1,71 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const Countdown = require('../common/countdown'); +const nsolid = require('nsolid'); + +const { + NGHTTP2_CANCEL, + NGHTTP2_NO_ERROR, + NGHTTP2_PROTOCOL_ERROR, + NGHTTP2_REFUSED_STREAM, + NGHTTP2_INTERNAL_ERROR +} = http2.constants; + +const tests = [ + [NGHTTP2_NO_ERROR, false], + [NGHTTP2_NO_ERROR, false], + [NGHTTP2_PROTOCOL_ERROR, true, 'NGHTTP2_PROTOCOL_ERROR'], + [NGHTTP2_CANCEL, false], + [NGHTTP2_REFUSED_STREAM, true, 'NGHTTP2_REFUSED_STREAM'], + [NGHTTP2_INTERNAL_ERROR, true, 'NGHTTP2_INTERNAL_ERROR'], +]; + +const server = http2.createServer(); +server.on('stream', (stream, headers) => { + const test = tests.find((t) => t[0] === Number(headers.rstcode)); + if (test[1]) { + stream.on('error', common.expectsError({ + name: 'Error', + code: 'ERR_HTTP2_STREAM_ERROR', + message: `Stream closed with error code ${test[2]}` + })); + } + stream.close(headers.rstcode | 0); + stream.on('close', () => { + console.log('stream closed'); + }); +}); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const countdown = new Countdown(tests.length, () => { + client.close(); + server.close(); + assert.strictEqual(nsolid.traceStats.httpClientCount, 0); + assert.strictEqual(nsolid.traceStats.httpClientAbortCount, 6); + assert.strictEqual(nsolid.traceStats.httpServerCount, 0); + assert.strictEqual(nsolid.traceStats.httpServerAbortCount, 6); + }); + + tests.forEach((test) => { + const req = client.request({ + ':method': 'POST', + 'rstcode': test[0] + }); + req.on('close', common.mustCall(() => { + assert.strictEqual(req.rstCode, test[0]); + countdown.dec(); + })); + req.on('aborted', common.mustCall()); + if (test[1]) + req.on('error', common.mustCall()); + else + req.on('error', common.mustNotCall()); + }); +})); diff --git a/test/parallel/test-nsolid-http2-server-session-destroy.js b/test/parallel/test-nsolid-http2-server-session-destroy.js new file mode 100644 index 0000000000..147f8ea573 --- /dev/null +++ b/test/parallel/test-nsolid-http2-server-session-destroy.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); +const nsolid = require('nsolid'); + +const server = h2.createServer(); +server.listen(0, common.localhostIPv4, common.mustCall(() => { + const afterConnect = common.mustCall((session) => { + session.request({ ':method': 'POST' }).end(common.mustCall(() => { + session.destroy(); + server.close(); + })); + }); + + const port = server.address().port; + const host = common.localhostIPv4; + h2.connect(`http://${host}:${port}`, afterConnect); +})); + +process.on('beforeExit', common.mustCall(() => { + assert.strictEqual(nsolid.traceStats.httpClientCount, 0); + assert.strictEqual(nsolid.traceStats.httpClientAbortCount, 1); + assert.strictEqual(nsolid.traceStats.httpServerCount, 0); + assert.strictEqual(nsolid.traceStats.httpServerAbortCount, 1); +})); diff --git a/test/parallel/test-nsolid-http2.js b/test/parallel/test-nsolid-http2.js new file mode 100644 index 0000000000..cd4b1a694d --- /dev/null +++ b/test/parallel/test-nsolid-http2.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const nsolid = require('nsolid'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream, headers, flags) => { + stream.respond({ 'content-type': 'text/html' }); + stream.end('test'); +})); + + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + })); + + let data = ''; + + req.setEncoding('utf8'); + req.on('data', common.mustCallAtLeast((d) => data += d)); + req.on('end', common.mustCall(() => { + server.close(); + client.close(common.mustCall(() => { + assert.strictEqual(data, 'test'); + assert.strictEqual(nsolid.traceStats.httpClientCount, 1); + assert.strictEqual(nsolid.traceStats.httpClientAbortCount, 0); + assert.strictEqual(nsolid.traceStats.httpServerCount, 1); + assert.strictEqual(nsolid.traceStats.httpServerAbortCount, 0); + })); + // Wait for more than 3secs for the percentiles to be updated + setTimeout(() => { + const metrics = nsolid.metrics(); + assert.ok(metrics.httpClientMedian > 0); + assert.ok(metrics.httpClient99Ptile > 0); + assert.ok(metrics.httpServerMedian > 0); + assert.ok(metrics.httpServer99Ptile > 0); + }, 3500); + })); +}));