Skip to content

Commit 5dc39a1

Browse files
ywave620ruyadorno
authored andcommitted
http: reuse socket only when it is drained
Ensuring every request is assigned to a drained socket or nothing. Because is has no benifit for a request to be attached to a non drained socket and it prevents the request from being assigned to a drained one, which might occur soon or already in the free pool We achieve this by claiming a socket as free only when the socket is drained. PR-URL: #43902 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Paolo Insogna <[email protected]> Reviewed-By: Robert Nagy <[email protected]>
1 parent 04fdc3e commit 5dc39a1

File tree

3 files changed

+134
-4
lines changed

3 files changed

+134
-4
lines changed

lib/_http_client.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ function parserOnIncomingClient(res, shouldKeepAlive) {
665665

666666
// Add our listener first, so that we guarantee socket cleanup
667667
res.on('end', responseOnEnd);
668-
req.on('prefinish', requestOnPrefinish);
668+
req.on('finish', requestOnFinish);
669669
socket.on('timeout', responseOnTimeout);
670670

671671
// If the user did not listen for the 'response' event, then they
@@ -737,12 +737,16 @@ function responseOnEnd() {
737737
socket.end();
738738
}
739739
assert(!socket.writable);
740-
} else if (req.finished && !this.aborted) {
740+
} else if (req.writableFinished && !this.aborted) {
741+
assert(req.finished);
741742
// We can assume `req.finished` means all data has been written since:
742743
// - `'responseOnEnd'` means we have been assigned a socket.
743744
// - when we have a socket we write directly to it without buffering.
744745
// - `req.finished` means `end()` has been called and no further data.
745746
// can be written
747+
// In addition, `req.writableFinished` means all data written has been
748+
// accepted by the kernel. (i.e. the `req.socket` is drained).Without
749+
// this constraint, we may assign a non drained socket to a request.
746750
responseKeepAlive(req);
747751
}
748752
}
@@ -755,7 +759,9 @@ function responseOnTimeout() {
755759
res.emit('timeout');
756760
}
757761

758-
function requestOnPrefinish() {
762+
// This function is necessary in the case where we receive the entire reponse
763+
// from server before we finish sending out the request
764+
function requestOnFinish() {
759765
const req = this;
760766

761767
if (req.shouldKeepAlive && req._ended)

lib/_http_outgoing.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,8 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
985985
};
986986

987987

988+
// This function is called once all user data are flushed to the socket.
989+
// Note that it has a chance that the socket is not drained.
988990
OutgoingMessage.prototype._finish = function _finish() {
989991
assert(this.socket);
990992
this.emit('prefinish');
@@ -1008,7 +1010,7 @@ OutgoingMessage.prototype._finish = function _finish() {
10081010
// the socket yet. Thus the outgoing messages need to be prepared to queue
10091011
// up data internally before sending it on further to the socket's queue.
10101012
//
1011-
// This function, outgoingFlush(), is called by both the Server and Client
1013+
// This function, _flush(), is called by both the Server and Client
10121014
// to attempt to flush any pending messages out to the socket.
10131015
OutgoingMessage.prototype._flush = function _flush() {
10141016
const socket = this.socket;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
const net = require('net');
6+
7+
const agent = new http.Agent({
8+
keepAlive: true,
9+
maxFreeSockets: Infinity,
10+
maxSockets: Infinity,
11+
maxTotalSockets: Infinity,
12+
});
13+
14+
const server = net.createServer({
15+
pauseOnConnect: true,
16+
}, (sock) => {
17+
// Do not read anything from `sock`
18+
sock.pause();
19+
sock.write('HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: Keep-Alive\r\n\r\n');
20+
});
21+
22+
server.listen(0, common.mustCall(() => {
23+
sendFstReq(server.address().port);
24+
}));
25+
26+
function sendFstReq(serverPort) {
27+
const req = http.request({
28+
agent,
29+
host: '127.0.0.1',
30+
port: serverPort,
31+
}, (res) => {
32+
res.on('data', noop);
33+
res.on('end', common.mustCall(() => {
34+
// Agent's socket reusing code is registered to process.nextTick(),
35+
// and will be run after this function, make sure it take effect.
36+
setImmediate(sendSecReq, serverPort, req.socket.localPort);
37+
}));
38+
});
39+
40+
// Make the `req.socket` non drained, i.e. has some data queued to write to
41+
// and accept by the kernel. In Linux and Mac, we only need to call `req.end(aLargeBuffer)`.
42+
// However, in Windows, the mechanism of acceptance is loose, the following code is a workaround
43+
// for Windows.
44+
45+
/**
46+
* https://docs.microsoft.com/en-US/troubleshoot/windows/win32/data-segment-tcp-winsock says
47+
*
48+
* Winsock uses the following rules to indicate a send completion to the application
49+
* (depending on how the send is invoked, the completion notification could be the
50+
* function returning from a blocking call, signaling an event, or calling a notification
51+
* function, and so forth):
52+
* - If the socket is still within SO_SNDBUF quota, Winsock copies the data from the application
53+
* send and indicates the send completion to the application.
54+
* - If the socket is beyond SO_SNDBUF quota and there's only one previously buffered send still
55+
* in the stack kernel buffer, Winsock copies the data from the application send and indicates
56+
* the send completion to the application.
57+
* - If the socket is beyond SO_SNDBUF quota and there's more than one previously buffered send
58+
* in the stack kernel buffer, Winsock copies the data from the application send. Winsock doesn't
59+
* indicate the send completion to the application until the stack completes enough sends to put
60+
* back the socket within SO_SNDBUF quota or only one outstanding send condition.
61+
*/
62+
63+
req.on('socket', () => {
64+
req.socket.on('connect', () => {
65+
// Print tcp send buffer information
66+
console.log(process.report.getReport().libuv.filter((handle) => handle.type === 'tcp'));
67+
68+
const dataLargerThanTCPSendBuf = Buffer.alloc(1024 * 1024 * 64, 0);
69+
70+
req.write(dataLargerThanTCPSendBuf);
71+
req.uncork();
72+
if (process.platform === 'win32') {
73+
assert.ok(req.socket.writableLength === 0);
74+
}
75+
76+
req.write(dataLargerThanTCPSendBuf);
77+
req.uncork();
78+
if (process.platform === 'win32') {
79+
assert.ok(req.socket.writableLength === 0);
80+
}
81+
82+
req.end(dataLargerThanTCPSendBuf);
83+
assert.ok(req.socket.writableLength > 0);
84+
});
85+
});
86+
}
87+
88+
function sendSecReq(serverPort, fstReqCliPort) {
89+
// Make the second request, which should be sent on a new socket
90+
// because the first socket is not drained and hence can not be reused
91+
const req = http.request({
92+
agent,
93+
host: '127.0.0.1',
94+
port: serverPort,
95+
}, (res) => {
96+
res.on('data', noop);
97+
res.on('end', common.mustCall(() => {
98+
setImmediate(sendThrReq, serverPort, req.socket.localPort);
99+
}));
100+
});
101+
102+
req.on('socket', common.mustCall((sock) => {
103+
assert.notStrictEqual(sock.localPort, fstReqCliPort);
104+
}));
105+
req.end();
106+
}
107+
108+
function sendThrReq(serverPort, secReqCliPort) {
109+
// Make the third request, the agent should reuse the second socket we just made
110+
const req = http.request({
111+
agent,
112+
host: '127.0.0.1',
113+
port: serverPort,
114+
}, noop);
115+
116+
req.on('socket', common.mustCall((sock) => {
117+
assert.strictEqual(sock.localPort, secReqCliPort);
118+
process.exit(0);
119+
}));
120+
}
121+
122+
function noop() { }

0 commit comments

Comments
 (0)