UNPKG

jackpot

Version:

Jackpot, TCP connection pooling for Node.js

394 lines (327 loc) 10.9 kB
'use strict'; var retry = require('retry') , ms = require('./ms'); /** * A net.Stream connection pool. * * @constructor * @param {Number} limit Size of the connection pool. * @param {Function} builder Stream factory. * @param {Object} options Configuration options. * @api public */ function Manager(limit, builder, options) { options = options || {}; if ('object' === typeof builder) { options = builder; builder = null; } this.limit = +limit || 20; // Defaults to 20 connections max. this.pool = []; // Contains the active connections. this.pending = 0; // Connecting connections. this.generator = null; // Stream generator. this.retries = options.retries || 5; // Max retries for each #pull. this.factor = options.factor || 3; // Exponential backoff factor. this.minTimeout = ms(options.min || '1 seconds'); // Minimum delay. this.maxTimeout = ms(options.max || '60 seconds');// Maximum delay. this.randomize = 'randomize' in options // Randomize the retry timeout. ? options.randomize : true; // Some stats that can be used for metrics. this.metrics = { allocations: 0 , releases: 0 }; if (builder) this.factory(builder); } Manager.prototype.__proto__ = require('events').EventEmitter.prototype; /** * Add a stream generator so we can generate streams for the pool. * * @param {Function} builder Function that pre-configures a stream. * @api public */ Manager.prototype.factory = function factory(builder) { if (typeof builder !== 'function') { throw new Error('The #factory requires a function.'); } this.generator = builder; return this; }; /** * Start listening to events that could influence the state of the connection. * * @param {net.Connection} net The connection. * @api private */ Manager.prototype.listen = function listen(net) { if (!net) return this; var self = this; /** * Simple helper function that allows us to automatically remove the * connection from the pool when we are unable to connect using it. * * @param {Error} err Optional error. * @api private */ function regenerate(err) { net.destroySoon(); self.remove(net); net.removeListener('timeout', regenerate); net.removeListener('error', regenerate); net.removeListener('end', regenerate); if (err) self.emit('error', err); } // Listen for events that would mess up the connection. net.once('timeout', regenerate) .once('error', regenerate) .once('end', regenerate); return this; }; /** * A fault tolerant connection allocation wrapper. * * @param {Function} fn Callback that receives a working connection. * @api public */ Manager.prototype.pull = function pull(fn) { var operation = retry.operation({ retries: this.retries , factor: this.factor , minTimeout: this.minTimeout , maxTimeout: this.maxTimeout , randomize: this.randomize }) , self = this; /** * Small wrapper around pulling a connection. * * @param {Error} err Optional error argument. * @param {Socket} connection A working connection. * @api private */ function allocate(err, connection) { if (operation.retry(err)) return; fn(err, connection); } operation.attempt(function attempt() { self.allocate(allocate); }); return this; }; /** * Allocate a new connection from the connection pool, this can be done async * that's why we use a error first callback pattern. * * @param {Function} fn * @api public */ Manager.prototype.allocate = function allocate(fn) { if (!this.generator) { fn(new Error('Specify a stream #factory')); return this; } /** * Two helper functions that allows us to correctly call the callback with * the correct arguments when we generate a new connection as the connection * should be emitting 'connect' before we can use it. But it can also emit * error if it fails to connect, or times in so doing. * * @param {Error} err Optional error argument. * @api private */ function either(err) { this.removeListener('error', either); this.removeListener('connect', either); this.removeListener('timeout', timeout); // Add to the pool if (!err) self.pool.push(this); self.pending--; fn(err, this); } /** * Listen to timeout events. * * @api private */ function timeout() { this.removeListener('timeout', timeout); self.pending--; fn(new Error('Timed out while trying to establish connection'), this); } var probabilities = [] , self = this , total, i, probability, connection; i = total = this.pool.length; // increase the allocation metric this.metrics.allocations++; // check the current pool if we already have a few connections available, so // we don't have to generate a new connection while (i--) { connection = this.pool[i]; probability = this.isAvailable(connection); // we are sure this connection works if (probability === 100) { fn(undefined, connection); return this; } // no accurate match, add it to the queue as we can get the most likely // available connection probabilities.push({ probability: probability , connection: connection }); } // we didn't find a confident match, see if we are allowed to generate a fresh // connection if ((this.pool.length + this.pending) < this.limit) { // determine if the function expects a callback or not, this can be done by // checking the length of the given function, as the amount of args accepted // equals the length.. if (this.generator.length === 0) { connection = this.generator(); if (connection) { this.pending++; this.listen(connection); connection.on('error', either) .on('connect', either) .on('timeout', timeout); return this; } } else { return this.generator(function generate(err, connection) { if (err) return fn(err); if (!connection) return fn(new Error('The #factory failed to generate a stream')); self.pending++; self.listen(connection); return connection.on('error', either) .on('connect', either) .on('timeout', timeout); }); } } // o, dear, we got issues.. we didn't find a valid connection and we cannot // create more.. so we are going to check if we might have semi valid // connection by sorting the probabilities array and see if it has // a probability above 60 probability = probabilities.sort(function sort(a, b) { return a.probability - b.probability; }).pop(); if (probability && probability.probability >= 60) { fn(undefined, probability.connection); return this; } // well, that didn't work out, so assume failure fn(new Error('The connection pool is full')); return this; }; /** * Check if a connection is available for writing. * * @param {net.Connection} net The connection. * @param {Boolean} ignore Ignore closed or dead connections. * @returns {Number} Probability that his connection is available or will be. * @api private */ Manager.prototype.isAvailable = function isAvailable(net, ignore) { var readyState = net.readyState , writable = readyState === 'open' || readyState === 'writeOnly' , writePending = net._pendingWriteReqs || 0 , writeQueue = net._writeQueue || [] , writes = writeQueue.length || writePending; // If the stream is writable and we don't have anything pending we are 100% // sure that this stream is available for writing. if (writable && writes === 0) return 100; // The connection is already closed or has been destroyed, why on earth are we // getting it then, remove it from the pool and return 0. if (readyState === 'closed' || net.destroyed) { this.remove(net); return 0; } // If the stream isn't writable we aren't that sure.. if (!writable) return 0; // The connection is still opening, so we can write to it in the future. if (readyState === 'opening') return 70; // We have some writes, so we are going to subtract that amount from our 100. if (writes < 100) return 100 - writes; // We didn't find any reliable states of the stream, so we are going to // assume something random, because we have no clue, so generate a random // number between 0 - 70. return Math.floor(Math.random() * 70); }; /** * Release the connection from the connection pool. * * @param {Stream} net The connection. * @param {Boolean} hard Switch between destroySoon or destroy. * @returns {Boolean} Was the removal successful * @api private */ Manager.prototype.release = function release(net, hard) { var index = this.pool.indexOf(net); if (index === -1) return false; // Check if the stream is still open. if (net) { if (!hard) net.destroySoon(); else net.destroy(); // Remove it from the pool. this.pool.splice(index, 1); // Increase the releases metric. this.metrics.releases++; } return true; }; // Alias remove to release. Manager.prototype.remove = Manager.prototype.release; /** * Free dead connections from the pool. * * @param {Number} keep the amount of connection to keep open * @param {Boolean} hard destroy all connections instead of destroySoon * @api public */ Manager.prototype.free = function free(keep, hard) { // Default to 0 if no arguments are supplied. keep = +keep || 0; // Create a back-up of the pool as we will be removing items from the array // and this could cause memory / socket leaks as we are unable to close some // connections in the array as the index has moved. var pool = this.pool.slice(0) , saved = 0; for (var i = 0, length = pool.length; i < length; i++) { var connection = pool[i] , probability = this.isAvailable(connection); // This is still a healthy connection, so try we probably just want to keep it. if (keep && saved < keep && probability === 100) { saved++; continue; } this.release(connection, hard); } // Clear the back-up pool.length = 0; // See how much connections are still available. return this.emit('free', saved, this.pool.length); }; /** * Iterate over the different connection. * * @param {Function} callback * @param {Mixed} context * @api public */ Manager.prototype.forEach = function forEach(callback, context) { this.pool.forEach(callback, context); return this; }; /** * Close the connection pool. * * @param {Boolean} hard destroy all connections * @api public */ Manager.prototype.end = function end(hard) { this.free(0, hard); return this.emit('end'); }; module.exports = Manager;