Skip to content

Commit cd8704d

Browse files
ywave620danielleadams
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 <matteo.collina@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Robert Nagy <ronagy@icloud.com>
1 parent e2a3bf3 commit cd8704d

File tree

3 files changed

+134
-4
lines changed

3 files changed

+134
-4
lines changed

lib/_http_client.js

Lines changed: 9 additions & 3 deletions
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

Lines changed: 3 additions & 1 deletion
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;
Lines changed: 122 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy