UNPKG

yakaa

Version:

Yet Another Keep-Alive Agent

312 lines (260 loc) 9.61 kB
'use strict'; // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. var net = require('net'); var EventEmitter = require('events').EventEmitter; var util = require('util'); var debug = require('./lib/debuglog.js')('httpagent'); // New Agent code. // The largest departure from the previous implementation is that // an Agent instance holds connections for a variable number of host:ports. // Surprisingly, this is still API compatible as far as third parties are // concerned. The only code that really notices the difference is the // request object. // Another departure is that all code related to HTTP parsing is in // ClientRequest.onSocket(). The Agent is now *strictly* // concerned with managing a connection pool. function Agent(options) { if (!(this instanceof Agent)) return new Agent(options); EventEmitter.call(this); this.defaultPort = 80; this.protocol = 'http:'; this.options = util._extend({}, options); // don't confuse net and make it think that we're connecting to a pipe this.options.path = null; this.requests = {}; this.sockets = {}; this.freeSockets = {}; this.keepAliveMsecs = this.options.keepAliveMsecs || 1000; this.keepAlive = this.options.keepAlive || false; this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets; this.maxFreeSockets = this.options.maxFreeSockets || 256; var self = this; this.on('free', function(socket, options) { var name = self.getName(options); debug('agent.on(free)', name); if (!self.isSocketDestroyed(socket) && self.requests[name] && self.requests[name].length) { self.requests[name].shift().onSocket(socket); if (self.requests[name].length === 0) { // don't leak delete self.requests[name]; } } else { // If there are no pending requests, then put it in // the freeSockets pool, but only if we're allowed to do so. var req = socket._httpMessage; if (req && req.shouldKeepAlive && !self.isSocketDestroyed(socket) && self.options.keepAlive) { var freeSockets = self.freeSockets[name]; var freeLen = freeSockets ? freeSockets.length : 0; var count = freeLen; if (self.sockets[name]) count += self.sockets[name].length; debug('potentially pooling', freeLen, count); if (count > self.maxSockets || freeLen >= self.maxFreeSockets) { debug('destroying socket', name); self.removeSocket(socket, options); socket.destroy(); } else { debug('pooling socket', name); freeSockets = freeSockets || []; self.freeSockets[name] = freeSockets; socket.setKeepAlive(true, self.keepAliveMsecs); if (socket.unref) { socket.unref(); } else if (socket.socket && socket.socket._handle && socket.socket._handle.unref) { socket.socket._handle.unref(); } socket._httpMessage = null; self.removeSocket(socket, options); freeSockets.push(socket); // set timeout on idle sockets if (options.keepAliveTimeoutMsecs !== undefined) { socket._yakaa_timeout = setTimeout(function () { self.emit('yakaa_destroy'); socket.destroySoon(); }, options.keepAliveTimeoutMsecs); } } } else { self.removeSocket(socket, options); socket.destroy(); } } }); } util.inherits(Agent, EventEmitter); exports.Agent = Agent; Agent.defaultMaxSockets = Infinity; Agent.prototype.createConnection = net.createConnection; Agent.prototype.isSocketDestroyed = function(socket) { // Different Node versions have different names for the property return socket.destroyed || socket._destroyed; }; // Get the key for a given set of request options Agent.prototype.getName = function(options) { var name = ''; if (options.host) name += options.host; else name += 'localhost'; name += ':'; if (options.port) name += options.port; name += ':'; if (options.localAddress) name += options.localAddress; name += ':'; return name; }; Agent.prototype.addRequest = function(req, options) { debug('addRequest'); // Legacy API: addRequest(req, host, port, path) if (typeof options === 'string') { options = { host: options, port: arguments[2], path: arguments[3] }; } var name = this.getName(options); if (!this.sockets[name]) { this.sockets[name] = []; } var freeLen = this.freeSockets[name] ? this.freeSockets[name].length : 0; var sockLen = freeLen + this.sockets[name].length; if (freeLen) { // we have a free socket, so use that. var socket = this.freeSockets[name].shift(); debug('have free socket'); // mark socket as non-idle so it doesn't get destroyed mid-request if (socket._yakaa_timeout) { clearTimeout(socket._yakaa_timeout); delete socket._yakaa_timeout; } // don't leak if (!this.freeSockets[name].length) delete this.freeSockets[name]; if (socket.ref) { socket.ref(); } else if (socket.socket && socket.socket._handle && socket.socket._handle.ref) { socket.socket._handle.ref(); } req.onSocket(socket); this.sockets[name].push(socket); } else if (sockLen < this.maxSockets) { debug('call onSocket', name, sockLen, freeLen); // If we are under maxSockets create a new one. req.onSocket(this.createSocket(req, options)); } else { debug('wait for socket'); // We are over limit so we'll add it to the queue. if (!this.requests[name]) { this.requests[name] = []; } this.requests[name].push(req); } }; Agent.prototype.createSocket = function(req, options) { debug('createSocket'); var self = this; options = util._extend({}, options); options = util._extend(options, self.options); options.servername = options.host; if (req) { var hostHeader = req.getHeader('host'); if (hostHeader) { options.servername = hostHeader.replace(/:.*$/, ''); } } var name = self.getName(options); debug('createConnection', name, options); options.encoding = null; var s = self.createConnection(options); if (!self.sockets[name]) { self.sockets[name] = []; } this.sockets[name].push(s); debug('sockets', name, this.sockets[name].length); function onFree() { self.emit('free', s, options); } s.on('free', onFree); function onClose() { debug('CLIENT socket onClose'); // This is the only place where sockets get removed from the Agent. // If you want to remove a socket from the pool, just close it. // All socket errors end in a close event anyway. self.removeSocket(s, options); } s.on('close', onClose); function onRemove() { // We need this function for cases like HTTP 'upgrade' // (defined by WebSockets) where we need to remove a socket from the // pool because it'll be locked up indefinitely debug('CLIENT socket onRemove'); self.removeSocket(s, options); s.removeListener('close', onClose); s.removeListener('free', onFree); s.removeListener('agentRemove', onRemove); } s.on('agentRemove', onRemove); return s; }; Agent.prototype.removeSocket = function(s, options) { var name = this.getName(options); debug('removeSocket', name, 'destroyed:', this.isSocketDestroyed(s)); var sets = [this.sockets]; this.emit('yakaa_remove', s); // If the socket was destroyed, remove it from the free buffers too. if (this.isSocketDestroyed(s)) sets.push(this.freeSockets); sets.forEach(function(sockets) { if (sockets[name]) { var index = sockets[name].indexOf(s); if (index !== -1) { sockets[name].splice(index, 1); // Don't leak if (sockets[name].length === 0) delete sockets[name]; } } }); if (this.requests[name] && this.requests[name].length) { debug('removeSocket, have a request, make a socket'); var req = this.requests[name][0]; // If we have pending requests and a socket gets closed make a new one this.createSocket(req, options).emit('free'); } }; Agent.prototype.destroy = function() { var sets = [this.freeSockets, this.sockets]; sets.forEach(function(set) { Object.keys(set).forEach(function(name) { set[name].forEach(function(socket) { socket.destroy(); }); }); }); }; module.exports = Agent;