Skip to content

Commit 9fc9b87

Browse files
committed
http: Proper KeepAlive behavior
Instead of destroying sockets when there are no pending requests, put them in a freeSockets list, and unref() them so that they do not keep the event loop open. Also, set the default max sockets to Infinity, to prevent the awful surprising deadlocks that happen when more connections are made.
1 parent 6176e49 commit 9fc9b87

File tree

3 files changed

+149
-8
lines changed

3 files changed

+149
-8
lines changed

lib/_http_agent.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,20 @@ var EventEmitter = require('events').EventEmitter;
3636
// concerned with managing a connection pool.
3737

3838
function Agent(options) {
39+
if (!(this instanceof Agent))
40+
return new Agent(options);
41+
3942
EventEmitter.call(this);
4043

4144
var self = this;
4245
self.options = options || {};
4346
self.requests = {};
4447
self.sockets = {};
48+
self.freeSockets = {};
49+
self.keepAliveMsecs = self.options.keepAliveMsecs || 1000;
50+
self.keepAlive = self.options.keepAlive || false;
4551
self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets;
52+
4653
self.on('free', function(socket, host, port, localAddress) {
4754
var name = host + ':' + port;
4855
if (localAddress) {
@@ -57,11 +64,31 @@ function Agent(options) {
5764
delete self.requests[name];
5865
}
5966
} else {
60-
// If there are no pending requests just destroy the
61-
// socket and it will get removed from the pool. This
62-
// gets us out of timeout issues and allows us to
63-
// default to Connection:keep-alive.
64-
socket.destroy();
67+
// If there are no pending requests, then put it in
68+
// the freeSockets pool, but only if we're allowed to do so.
69+
var req = socket._httpMessage;
70+
if (req &&
71+
req.shouldKeepAlive &&
72+
!socket.destroyed &&
73+
self.options.keepAlive) {
74+
var freeSockets = self.freeSockets[name];
75+
var count = freeSockets ? freeSockets.length : 0;
76+
if (self.sockets[name])
77+
count += self.sockets[name].length;
78+
79+
if (count > self.maxSockets) {
80+
socket.destroy();
81+
} else {
82+
freeSockets = freeSockets || [];
83+
self.freeSockets[name] = freeSockets;
84+
socket.setKeepAlive(true, self.keepAliveMsecs);
85+
socket.unref();
86+
socket._httpMessage = null;
87+
freeSockets.push(socket);
88+
}
89+
} else {
90+
socket.destroy();
91+
}
6592
}
6693
});
6794
self.createConnection = net.createConnection;
@@ -70,7 +97,7 @@ function Agent(options) {
7097
util.inherits(Agent, EventEmitter);
7198
exports.Agent = Agent;
7299

73-
Agent.defaultMaxSockets = 5;
100+
Agent.defaultMaxSockets = Infinity;
74101

75102
Agent.prototype.defaultPort = 80;
76103
Agent.prototype.addRequest = function(req, host, port, localAddress) {
@@ -81,7 +108,18 @@ Agent.prototype.addRequest = function(req, host, port, localAddress) {
81108
if (!this.sockets[name]) {
82109
this.sockets[name] = [];
83110
}
84-
if (this.sockets[name].length < this.maxSockets) {
111+
112+
if (this.freeSockets[name] && this.freeSockets[name].length) {
113+
// we have a free socket, so use that.
114+
var socket = this.freeSockets[name].shift();
115+
116+
// don't leak
117+
if (!this.freeSockets[name].length)
118+
delete this.freeSockets[name];
119+
120+
socket.ref();
121+
req.onSocket(socket);
122+
} else if (this.sockets[name].length < this.maxSockets) {
85123
// If we are under maxSockets create a new one.
86124
req.onSocket(this.createSocket(name, host, port, localAddress, req));
87125
} else {
@@ -158,5 +196,16 @@ Agent.prototype.removeSocket = function(s, name, host, port, localAddress) {
158196
}
159197
};
160198

199+
Agent.prototype.destroy = function() {
200+
var sets = [this.freeSockets, this.sockets];
201+
sets.forEach(function(set) {
202+
Object.keys(set).forEach(function(name) {
203+
set[name].forEach(function(socket) {
204+
socket.destroy();
205+
});
206+
});
207+
});
208+
};
209+
161210
var globalAgent = new Agent();
162211
exports.globalAgent = globalAgent;

test/simple/test-http-client-abort.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ var server = http.Server(function(req, res) {
5050
});
5151

5252
var responses = 0;
53-
var N = http.Agent.defaultMaxSockets - 1;
53+
var N = 16;
5454
var requests = [];
5555

5656
server.listen(common.PORT, function() {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
var common = require('../common');
23+
var assert = require('assert');
24+
25+
var http = require('http');
26+
27+
28+
var serverSocket = null;
29+
var server = http.createServer(function(req, res) {
30+
// They should all come in on the same server socket.
31+
if (serverSocket) {
32+
assert.equal(req.socket, serverSocket);
33+
} else {
34+
serverSocket = req.socket;
35+
}
36+
37+
res.end(req.url);
38+
});
39+
server.listen(common.PORT);
40+
41+
var agent = http.Agent({ keepAlive: true });
42+
43+
44+
var clientSocket = null;
45+
var expectRequests = 10;
46+
var actualRequests = 0;
47+
48+
49+
makeRequest(expectRequests);
50+
function makeRequest(n) {
51+
if (n === 0) {
52+
server.close();
53+
agent.destroy();
54+
return;
55+
}
56+
57+
var req = http.request({
58+
port: common.PORT,
59+
agent: agent,
60+
path: '/' + n
61+
});
62+
63+
req.end();
64+
65+
req.on('socket', function(sock) {
66+
if (clientSocket) {
67+
assert.equal(sock, clientSocket);
68+
} else {
69+
clientSocket = sock;
70+
}
71+
});
72+
73+
req.on('response', function(res) {
74+
var data = '';
75+
res.setEncoding('utf8');
76+
res.on('data', function(c) {
77+
data += c;
78+
});
79+
res.on('end', function() {
80+
assert.equal(data, '/' + n);
81+
setTimeout(function() {
82+
actualRequests++;
83+
makeRequest(n - 1);
84+
}, 1);
85+
});
86+
});
87+
}
88+
89+
process.on('exit', function() {
90+
assert.equal(actualRequests, expectRequests)
91+
console.log('ok');
92+
});

0 commit comments

Comments
 (0)