Skip to content

Commit 571e2d9

Browse files
committed
http: remove duplicate async_hooks init for http parser
Each time a new parser was created, AsyncReset was being called via the C++ Parser class constructor (super constructor AsyncWrap) and also via Parser::Reinitialize. This also adds a missing async_hooks destroy event before AsyncReset is called for the parser reuse case, otherwise the old async_id never gets destroyed. Refs: nodejs#19859
1 parent 70e6a88 commit 571e2d9

File tree

10 files changed

+113
-31
lines changed

10 files changed

+113
-31
lines changed

benchmark/http/bench-parser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function main({ len, n }) {
2525
bench.start();
2626
for (var i = 0; i < n; i++) {
2727
parser.execute(header, 0, header.length);
28-
parser.reinitialize(REQUEST);
28+
parser.reinitialize(REQUEST, i > 0);
2929
}
3030
bench.end(n);
3131
}

lib/_http_client.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ const {
3636
const { OutgoingMessage } = require('_http_outgoing');
3737
const Agent = require('_http_agent');
3838
const { Buffer } = require('buffer');
39-
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
39+
const {
40+
defaultTriggerAsyncIdScope,
41+
destroyHooksExist,
42+
emitDestroy
43+
} = require('internal/async_hooks');
4044
const { URL, urlToOptions, searchParamsSymbol } = require('internal/url');
4145
const { outHeadersKey, ondrain } = require('internal/http');
4246
const {
@@ -631,7 +635,10 @@ function tickOnSocket(req, socket) {
631635
var parser = parsers.alloc();
632636
req.socket = socket;
633637
req.connection = socket;
634-
parser.reinitialize(HTTPParser.RESPONSE);
638+
if (destroyHooksExist() && parser.needsAsyncReset && parser.getAsyncId()) {
639+
emitDestroy(parser.getAsyncId());
640+
}
641+
parser.reinitialize(HTTPParser.RESPONSE, parser.needsAsyncReset);
635642
parser.socket = socket;
636643
parser.outgoing = req;
637644
req.parser = parser;

lib/_http_server.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const { OutgoingMessage } = require('_http_outgoing');
4040
const { outHeadersKey, ondrain } = require('internal/http');
4141
const {
4242
defaultTriggerAsyncIdScope,
43+
destroyHooksExist,
44+
emitDestroy,
4345
getOrSetAsyncId
4446
} = require('internal/async_hooks');
4547
const { IncomingMessage } = require('_http_incoming');
@@ -338,7 +340,10 @@ function connectionListenerInternal(server, socket) {
338340
socket.on('timeout', socketOnTimeout);
339341

340342
var parser = parsers.alloc();
341-
parser.reinitialize(HTTPParser.REQUEST);
343+
if (destroyHooksExist() && parser.needsAsyncReset && parser.getAsyncId()) {
344+
emitDestroy(parser.getAsyncId());
345+
}
346+
parser.reinitialize(HTTPParser.REQUEST, parser.needsAsyncReset);
342347
parser.socket = socket;
343348
socket.parser = parser;
344349

lib/internal/freelist.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class FreeList {
1010

1111
alloc() {
1212
return this.list.length ?
13-
this.list.pop() :
14-
this.ctor.apply(this, arguments);
13+
needsToCallAsyncReset(this.list.pop()) :
14+
mustNotCallAsyncReset(this.ctor.apply(this, arguments));
1515
}
1616

1717
free(obj) {
@@ -23,4 +23,14 @@ class FreeList {
2323
}
2424
}
2525

26+
function needsToCallAsyncReset(item) {
27+
item.needsAsyncReset = true;
28+
return item;
29+
}
30+
31+
function mustNotCallAsyncReset(item) {
32+
item.needsAsyncReset = false;
33+
return item;
34+
}
35+
2636
module.exports = FreeList;

src/node_http_parser.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ class Parser : public AsyncWrap, public StreamListener {
465465
Environment* env = Environment::GetCurrent(args);
466466

467467
CHECK(args[0]->IsInt32());
468+
CHECK(args[1]->IsBoolean());
469+
bool needsAsyncReset = args[1]->IsTrue();
468470
http_parser_type type =
469471
static_cast<http_parser_type>(args[0].As<Int32>()->Value());
470472

@@ -473,8 +475,12 @@ class Parser : public AsyncWrap, public StreamListener {
473475
ASSIGN_OR_RETURN_UNWRAP(&parser, args.Holder());
474476
// Should always be called from the same context.
475477
CHECK_EQ(env, parser->env());
476-
// The parser is being reused. Reset the async id and call init() callbacks.
477-
parser->AsyncReset();
478+
// This parser has either just been created or it is being reused.
479+
// We must only call AsyncReset for the latter case, because AsyncReset has
480+
// already been called via the constructor for the former case.
481+
if (needsAsyncReset) {
482+
parser->AsyncReset();
483+
}
478484
parser->Init(type);
479485
}
480486

test/async-hooks/test-graph.http.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,14 @@ process.on('exit', function() {
3838
{ type: 'HTTPPARSER',
3939
id: 'httpparser:1',
4040
triggerAsyncId: 'tcpserver:1' },
41-
{ type: 'HTTPPARSER',
42-
id: 'httpparser:2',
43-
triggerAsyncId: 'tcpserver:1' },
4441
{ type: 'TCPWRAP', id: 'tcp:2', triggerAsyncId: 'tcpserver:1' },
4542
{ type: 'Timeout', id: 'timeout:1', triggerAsyncId: 'tcp:2' },
4643
{ type: 'HTTPPARSER',
47-
id: 'httpparser:3',
48-
triggerAsyncId: 'tcp:2' },
49-
{ type: 'HTTPPARSER',
50-
id: 'httpparser:4',
44+
id: 'httpparser:2',
5145
triggerAsyncId: 'tcp:2' },
5246
{ type: 'Timeout',
5347
id: 'timeout:2',
54-
triggerAsyncId: 'httpparser:4' },
48+
triggerAsyncId: 'httpparser:2' },
5549
{ type: 'SHUTDOWNWRAP',
5650
id: 'shutdown:1',
5751
triggerAsyncId: 'tcp:2' } ]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use strict';
2+
const common = require('../common');
3+
const Countdown = require('../common/countdown');
4+
const assert = require('assert');
5+
const async_hooks = require('async_hooks');
6+
const http = require('http');
7+
8+
// Regression test for https://github.com/nodejs/node/issues/19859.
9+
// Checks that matching destroys are emitted when creating new/reusing old http
10+
// parser instances.
11+
12+
const N = 50;
13+
const KEEP_ALIVE = 100;
14+
15+
const createdIds = [];
16+
const destroyedIds = [];
17+
async_hooks.createHook({
18+
init: common.mustCallAtLeast((asyncId, type) => {
19+
if (type === 'HTTPPARSER') {
20+
createdIds.push(asyncId);
21+
}
22+
}, N),
23+
destroy: (asyncId) => {
24+
destroyedIds.push(asyncId);
25+
}
26+
}).enable();
27+
28+
const server = http.createServer(function(req, res) {
29+
res.end('Hello');
30+
});
31+
32+
const keepAliveAgent = new http.Agent({
33+
keepAlive: true,
34+
keepAliveMsecs: KEEP_ALIVE,
35+
});
36+
37+
const countdown = new Countdown(N, () => {
38+
server.close(() => {
39+
// give the server sockets time to close (which will also free their
40+
// associated parser objects) after the server has been closed.
41+
setTimeout(() => {
42+
createdIds.forEach((createdAsyncId) => {
43+
assert.ok(destroyedIds.indexOf(createdAsyncId) >= 0);
44+
});
45+
}, KEEP_ALIVE * 2);
46+
});
47+
});
48+
49+
server.listen(0, function() {
50+
for (let i = 0; i < N; ++i) {
51+
(function makeRequest() {
52+
http.get({
53+
port: server.address().port,
54+
agent: keepAliveAgent
55+
}, function(res) {
56+
countdown.dec();
57+
res.resume();
58+
});
59+
})();
60+
}
61+
});

test/parallel/test-freelist.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,23 @@ const FreeList = require('internal/freelist');
88

99
assert.strictEqual(typeof FreeList, 'function');
1010

11-
const flist1 = new FreeList('flist1', 3, String);
11+
const flist1 = new FreeList('flist1', 3, Object);
1212

1313
// Allocating when empty, should not change the list size
14-
const result = flist1.alloc('test');
15-
assert.strictEqual(typeof result, 'string');
16-
assert.strictEqual(result, 'test');
14+
const result = flist1.alloc();
15+
assert.strictEqual(typeof result, 'object');
1716
assert.strictEqual(flist1.list.length, 0);
1817

1918
// Exhaust the free list
20-
assert(flist1.free('test1'));
21-
assert(flist1.free('test2'));
22-
assert(flist1.free('test3'));
19+
assert(flist1.free({ id: 'test1' }));
20+
assert(flist1.free({ id: 'test2' }));
21+
assert(flist1.free({ id: 'test3' }));
2322

2423
// Now it should not return 'true', as max length is exceeded
25-
assert.strictEqual(flist1.free('test4'), false);
26-
assert.strictEqual(flist1.free('test5'), false);
24+
assert.strictEqual(flist1.free({ id: 'test4' }), false);
25+
assert.strictEqual(flist1.free({ id: 'test5' }), false);
2726

2827
// At this point 'alloc' should just return the stored values
29-
assert.strictEqual(flist1.alloc(), 'test3');
30-
assert.strictEqual(flist1.alloc(), 'test2');
31-
assert.strictEqual(flist1.alloc(), 'test1');
28+
assert.strictEqual(flist1.alloc().id, 'test3');
29+
assert.strictEqual(flist1.alloc().id, 'test2');
30+
assert.strictEqual(flist1.alloc().id, 'test1');

test/parallel/test-http-parser.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function expectBody(expected) {
9898
throw new Error('hello world');
9999
};
100100

101-
parser.reinitialize(HTTPParser.REQUEST);
101+
parser.reinitialize(HTTPParser.REQUEST, false);
102102

103103
assert.throws(
104104
() => { parser.execute(request, 0, request.length); },
@@ -558,7 +558,7 @@ function expectBody(expected) {
558558
parser[kOnBody] = expectBody('ping');
559559
parser.execute(req1, 0, req1.length);
560560

561-
parser.reinitialize(REQUEST);
561+
parser.reinitialize(REQUEST, false);
562562
parser[kOnBody] = expectBody('pong');
563563
parser[kOnHeadersComplete] = onHeadersComplete2;
564564
parser.execute(req2, 0, req2.length);

test/sequential/test-http-regr-gh-2928.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function execAndClose() {
2525
process.stdout.write('.');
2626

2727
const parser = parsers.pop();
28-
parser.reinitialize(HTTPParser.RESPONSE);
28+
parser.reinitialize(HTTPParser.RESPONSE, parser.needsAsyncReset);
2929

3030
const socket = net.connect(common.PORT);
3131
socket.on('error', (e) => {

0 commit comments

Comments
 (0)