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

3 files changed

Lines changed: 134 additions & 4 deletions

File tree

‎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-
}elseif(req.finished&&!this.aborted){
740+
}elseif(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-
functionrequestOnPrefinish(){
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+
functionrequestOnFinish(){
759765
constreq=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
constsocket=this.socket;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
'use strict';
2+
constcommon=require('../common');
3+
constassert=require('assert');
4+
consthttp=require('http');
5+
constnet=require('net');
6+
7+
constagent=newhttp.Agent({
8+
keepAlive: true,
9+
maxFreeSockets: Infinity,
10+
maxSockets: Infinity,
11+
maxTotalSockets: Infinity,
12+
});
13+
14+
constserver=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+
functionsendFstReq(serverPort){
27+
constreq=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+
constdataLargerThanTCPSendBuf=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+
functionsendSecReq(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+
constreq=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+
functionsendThrReq(serverPort,secReqCliPort){
109+
// Make the third request, the agent should reuse the second socket we just made
110+
constreq=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+
functionnoop(){}

0 commit comments

Comments
 (0)