Skip to content

Commit 8b7b524

Browse files
committed
lib: add metrics support for http2
1 parent c5d98da commit 8b7b524

7 files changed

+599
-0
lines changed

lib/internal/nsolid_diag.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const dc = require('diagnostics_channel');
2323
const {
2424
kHttpClientAbortCount,
2525
kHttpClientCount,
26+
kHttpServerAbortCount,
27+
kHttpServerCount,
2628
kSpanHttpClient,
2729
kSpanHttpMethod,
2830
kSpanHttpReqUrl,
@@ -31,6 +33,9 @@ const {
3133

3234
const undiciFetch = dc.tracingChannel('undici:fetch');
3335

36+
// To lazy load the http2 constants
37+
let http2Constants;
38+
3439
let tracingEnabled = false;
3540

3641
const fetchSubscribeListener = (message, name) => {};
@@ -119,3 +124,47 @@ dc.subscribe('undici:request:error', ({ request, error }) => {
119124
}
120125
}
121126
});
127+
128+
dc.subscribe('http2.client.stream.created', ({ stream }) => {
129+
stream[nsolid_tracer_s] = {
130+
start: now(),
131+
response: false,
132+
};
133+
});
134+
135+
dc.subscribe('http2.client.stream.finish', ({ stream, flags }) => {
136+
stream[nsolid_tracer_s].response = true;
137+
});
138+
139+
dc.subscribe('http2.client.stream.close', ({ stream, code }) => {
140+
http2Constants ||= require('internal/http2/core').constants;
141+
const tracingInfo = stream[nsolid_tracer_s];
142+
if (code === http2Constants.NGHTTP2_NO_ERROR && tracingInfo.response) {
143+
nsolid_counts[kHttpClientCount]++;
144+
nsolidApi.pushClientBucket(now() - tracingInfo.start);
145+
} else {
146+
nsolid_counts[kHttpClientAbortCount]++;
147+
}
148+
});
149+
150+
dc.subscribe('http2.server.stream.start', ({ stream }) => {
151+
stream[nsolid_tracer_s] = {
152+
start: now(),
153+
response: false,
154+
};
155+
});
156+
157+
dc.subscribe('http2.server.stream.finish', ({ stream, flags }) => {
158+
stream[nsolid_tracer_s].response = true;
159+
});
160+
161+
dc.subscribe('http2.server.stream.close', ({ stream, code }) => {
162+
http2Constants ||= require('internal/http2/core').constants;
163+
const tracingInfo = stream[nsolid_tracer_s];
164+
if (code === http2Constants.NGHTTP2_NO_ERROR && tracingInfo.response) {
165+
nsolid_counts[kHttpServerCount]++;
166+
nsolidApi.pushServerBucket(now() - tracingInfo.start);
167+
} else {
168+
nsolid_counts[kHttpServerAbortCount]++;
169+
}
170+
});
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
// Flags: --expose-internals
2+
import * as common from '../common/index.mjs';
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
import assert from 'assert';
6+
import * as h2 from 'http2';
7+
import util from 'internal/http2/util';
8+
import { getEventListeners } from 'events';
9+
import nsolid from 'nsolid';
10+
11+
const kSocket = util.kSocket;
12+
13+
let httpClientAbortCount = 0;
14+
let httpServerAbortCount = 0;
15+
16+
const tests = [];
17+
18+
tests.push({
19+
name: 'Test destroy before client operations',
20+
test: (done) => {
21+
return new Promise((resolve) => {
22+
const server = h2.createServer();
23+
server.listen(0, common.mustCall(() => {
24+
const client = h2.connect(`http://localhost:${server.address().port}`);
25+
const socket = client[kSocket];
26+
socket.on('close', common.mustCall(() => {
27+
assert(socket.destroyed);
28+
}));
29+
30+
const req = client.request();
31+
req.on('error', common.expectsError({
32+
code: 'ERR_HTTP2_STREAM_CANCEL',
33+
name: 'Error',
34+
message: 'The pending stream has been canceled'
35+
}));
36+
37+
client.destroy();
38+
39+
req.on('response', common.mustNotCall());
40+
41+
const sessionError = {
42+
name: 'Error',
43+
code: 'ERR_HTTP2_INVALID_SESSION',
44+
message: 'The session has been destroyed'
45+
};
46+
47+
assert.throws(() => client.setNextStreamID(), sessionError);
48+
assert.throws(() => client.setLocalWindowSize(), sessionError);
49+
assert.throws(() => client.ping(), sessionError);
50+
assert.throws(() => client.settings({}), sessionError);
51+
assert.throws(() => client.goaway(), sessionError);
52+
assert.throws(() => client.request(), sessionError);
53+
client.close(); // Should be a non-op at this point
54+
55+
// Wait for setImmediate call from destroy() to complete
56+
// so that state.destroyed is set to true
57+
setImmediate(() => {
58+
assert.throws(() => client.setNextStreamID(), sessionError);
59+
assert.throws(() => client.setLocalWindowSize(), sessionError);
60+
assert.throws(() => client.ping(), sessionError);
61+
assert.throws(() => client.settings({}), sessionError);
62+
assert.throws(() => client.goaway(), sessionError);
63+
assert.throws(() => client.request(), sessionError);
64+
client.close(); // Should be a non-op at this point
65+
});
66+
67+
req.resume();
68+
req.on('end', common.mustNotCall());
69+
req.on('close', common.mustCall(() => {
70+
server.close();
71+
httpClientAbortCount++;
72+
resolve();
73+
}));
74+
}));
75+
});
76+
}
77+
});
78+
79+
tests.push({
80+
name: 'Test destroy before goaway',
81+
test: () => {
82+
return new Promise((resolve) => {
83+
const server = h2.createServer();
84+
server.on('stream', common.mustCall((stream) => {
85+
stream.session.destroy();
86+
}));
87+
88+
server.listen(0, common.mustCall(() => {
89+
const client = h2.connect(`http://localhost:${server.address().port}`);
90+
91+
client.on('close', () => {
92+
server.close();
93+
// Calling destroy in here should not matter
94+
client.destroy();
95+
httpClientAbortCount++;
96+
httpServerAbortCount++;
97+
resolve();
98+
});
99+
100+
client.request();
101+
}));
102+
});
103+
}
104+
});
105+
106+
tests.push({
107+
name: 'Test destroy before connect',
108+
test: () => {
109+
return new Promise((resolve) => {
110+
const server = h2.createServer();
111+
server.on('stream', common.mustNotCall());
112+
113+
server.listen(0, common.mustCall(() => {
114+
const client = h2.connect(`http://localhost:${server.address().port}`);
115+
116+
server.on('connection', common.mustCall(() => {
117+
server.close();
118+
client.close();
119+
httpClientAbortCount++;
120+
resolve();
121+
}));
122+
123+
const req = client.request();
124+
req.destroy();
125+
}));
126+
});
127+
}
128+
});
129+
130+
tests.push({
131+
name: 'Destroy with AbortSignal',
132+
test: () => {
133+
return new Promise((resolve) => {
134+
const server = h2.createServer();
135+
const controller = new AbortController();
136+
137+
server.on('stream', common.mustNotCall());
138+
server.listen(0, common.mustCall(() => {
139+
const client = h2.connect(`http://localhost:${server.address().port}`);
140+
client.on('close', common.mustCall());
141+
142+
const { signal } = controller;
143+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
144+
145+
client.on('error', common.mustCall(() => {
146+
// After underlying stream dies, signal listener detached
147+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
148+
}));
149+
150+
const req = client.request({}, { signal });
151+
152+
req.on('error', common.mustCall((err) => {
153+
assert.strictEqual(err.code, 'ABORT_ERR');
154+
assert.strictEqual(err.name, 'AbortError');
155+
}));
156+
req.on('close', common.mustCall(() => {
157+
server.close();
158+
httpClientAbortCount++;
159+
resolve();
160+
}));
161+
162+
assert.strictEqual(req.aborted, false);
163+
assert.strictEqual(req.destroyed, false);
164+
// Signal listener attached
165+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
166+
167+
controller.abort();
168+
169+
assert.strictEqual(req.aborted, false);
170+
assert.strictEqual(req.destroyed, true);
171+
}));
172+
});
173+
}
174+
});
175+
176+
tests.push({
177+
name: 'Pass an already destroyed signal to abort immediately',
178+
test: async () => {
179+
return new Promise((resolve) => {
180+
const server = h2.createServer();
181+
const controller = new AbortController();
182+
183+
server.on('stream', common.mustNotCall());
184+
server.listen(0, common.mustCall(() => {
185+
const client = h2.connect(`http://localhost:${server.address().port}`);
186+
client.on('close', common.mustCall());
187+
188+
const { signal } = controller;
189+
controller.abort();
190+
191+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
192+
193+
client.on('error', common.mustCall(() => {
194+
// After underlying stream dies, signal listener detached
195+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
196+
}));
197+
198+
const req = client.request({}, { signal });
199+
// Signal already aborted, so no event listener attached.
200+
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
201+
202+
assert.strictEqual(req.aborted, false);
203+
// Destroyed on same tick as request made
204+
assert.strictEqual(req.destroyed, true);
205+
206+
req.on('error', common.mustCall((err) => {
207+
assert.strictEqual(err.code, 'ABORT_ERR');
208+
assert.strictEqual(err.name, 'AbortError');
209+
}));
210+
req.on('close', common.mustCall(() => {
211+
server.close();
212+
httpClientAbortCount++;
213+
resolve();
214+
}));
215+
}));
216+
});
217+
}
218+
});
219+
220+
tests.push({
221+
name: 'Destroy ClientHttpSession with AbortSignal',
222+
test: async () => {
223+
async function testH2ConnectAbort(secure) {
224+
return new Promise((resolve) => {
225+
const server = secure ? h2.createSecureServer() : h2.createServer();
226+
const controller = new AbortController();
227+
server.on('stream', common.mustNotCall());
228+
server.listen(0, common.mustCall(() => {
229+
const { signal } = controller;
230+
const protocol = secure ? 'https' : 'http';
231+
const client = h2.connect(`${protocol}://localhost:${server.address().port}`, {
232+
signal,
233+
});
234+
client.on('close', common.mustCall());
235+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
236+
client.on('error', common.mustCall(common.mustCall((err) => {
237+
assert.strictEqual(err.code, 'ABORT_ERR');
238+
assert.strictEqual(err.name, 'AbortError');
239+
})));
240+
const req = client.request({}, {});
241+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
242+
req.on('error', common.mustCall((err) => {
243+
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_CANCEL');
244+
assert.strictEqual(err.name, 'Error');
245+
assert.strictEqual(req.aborted, false);
246+
assert.strictEqual(req.destroyed, true);
247+
}));
248+
req.on('close', common.mustCall(() => {
249+
server.close();
250+
resolve();
251+
}));
252+
assert.strictEqual(req.aborted, false);
253+
assert.strictEqual(req.destroyed, false);
254+
// Signal listener attached
255+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
256+
controller.abort();
257+
}));
258+
});
259+
}
260+
await testH2ConnectAbort(false);
261+
httpClientAbortCount++;
262+
await testH2ConnectAbort(true);
263+
httpClientAbortCount++;
264+
}
265+
});
266+
267+
tests.push({
268+
name: 'Destroy ClientHttp2Stream with AbortSignal',
269+
test: async () => {
270+
return new Promise((resolve) => {
271+
const server = h2.createServer();
272+
const controller = new AbortController();
273+
274+
server.on('stream', common.mustCall((stream) => {
275+
stream.on('error', common.mustNotCall());
276+
stream.on('close', common.mustCall(() => {
277+
assert.strictEqual(stream.rstCode, h2.constants.NGHTTP2_CANCEL);
278+
server.close();
279+
httpClientAbortCount++;
280+
httpServerAbortCount++;
281+
resolve();
282+
}));
283+
controller.abort();
284+
}));
285+
server.listen(0, common.mustCall(() => {
286+
const client = h2.connect(`http://localhost:${server.address().port}`);
287+
client.on('close', common.mustCall());
288+
289+
const { signal } = controller;
290+
const req = client.request({}, { signal });
291+
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
292+
req.on('error', common.mustCall((err) => {
293+
assert.strictEqual(err.code, 'ABORT_ERR');
294+
assert.strictEqual(err.name, 'AbortError');
295+
client.close();
296+
}));
297+
req.on('close', common.mustCall());
298+
}));
299+
});
300+
},
301+
});
302+
303+
for (const { name, test } of tests) {
304+
console.log(`${name}`);
305+
await test();
306+
}
307+
308+
assert.strictEqual(nsolid.traceStats.httpClientCount, 0);
309+
assert.strictEqual(nsolid.traceStats.httpClientAbortCount, httpClientAbortCount);
310+
assert.strictEqual(nsolid.traceStats.httpServerCount, 0);
311+
assert.strictEqual(nsolid.traceStats.httpServerAbortCount, httpServerAbortCount);

0 commit comments

Comments
 (0)