UNPKG

generic-pool-redux

Version:

Generic resource pooling in Javascript

332 lines (289 loc) 10.7 kB
// Generic Pool Redux // // Fork of https://github.com/coopernurse/node-pool // with prototypes, api changes, and support for the client. // License: MIT // ------------------------------------------------ (function(define) { "use strict"; define(function(require, exports, module) { // Initialize arrays to hold queue elements. var PriorityQueue = function(size) { this.slots = []; this.queueSize = Math.max(+size | 0, 1); for (var i = 0; i < this.queueSize; i += 1) { this.slots.push([]); } }; PriorityQueue.prototype = { total: null, // Calculates the size of the queue, and sets // the value to total. size: function() { if (this.total === null) { this.total = 0; for (var i = 0; i < this.queueSize; i += 1) { this.total += this.slots[i].length; } } return this.total; }, // Clears the cache for total and adds an // object to the queue, based on an optional priority. enqueue: function(obj, priority) { priority = priority && +priority | 0 || 0; this.total = null; if (priority) { var priorityOrig = priority; if (priority < 0 || priority >= this.queueSize) { priority = (this.size - 1); } } this.slots[priority].push(obj); }, // Clears the cache for total and removes an object // from the queue. dequeue: function() { var obj = null, i, sl = this.slots.length; this.total = null; for (i = 0; i < sl; i += 1) { if (this.slots[i].length) { obj = this.slots[i].shift(); break; } } return obj; } }; // Constructor for a new pool. var Pool = function(options) { if (!(this instanceof Pool)) return new Pool(options); this.idleTimeoutMillis = options.idleTimeoutMillis || 30000; this.reapInterval = options.reapIntervalMillis || 1000; this.destroyHandler = options.destroy || function() {}; this.refreshIdle = ('refreshIdle' in options) ? options.refreshIdle : true; this.availableObjects = []; this.waitingClients = new PriorityQueue(options.priorityRange || 1); this.create = options.create || (function() { throw new Error('A create method must be defined for the connection pool.'); })(); // If a validate method is provided, use that instead of the default. if (options.validate) this.validate = options.validate; // Set the max & min's on the options. var max = parseInt(options.max, 10); var min = parseInt(options.min, 10); this.max = Math.max(isNaN(max) ? 1 : max, 1); this.min = Math.min(isNaN(min) ? 0 : min, this.max - 1); // Ensure the minimum is created. this.ensureMinimum(); }; Pool.prototype = { count: 0, draining: false, removeIdleTimer: null, removeIdleScheduled: false, // Default validate. validate: function() { return true; }, // Request a new client. The callback will be called, // when a new client will be availabe, passing the client to it. // Optionally, yoy may specify a priority of the caller if there are no // available resources. Lower numbers mean higher priority. acquire: function(callback, priority) { if (this.draining) return callback(new Error("Pool is draining and cannot accept work")); this.waitingClients.enqueue(callback, priority); this.dispense(); return (this.count < this.max); }, // Return the client to the pool, in case it is no longer required. release: function(obj, callback) { // Check to see if this object has already been released (i.e., is back in the pool of availableObjects) if (this.availableObjects.some(function(objWithTimeout) { return (objWithTimeout.obj === obj); })) { if (callback) callback(new Error('Release called multiple times on the same object')); return; } var objWithTimeout = { obj: obj, timeout: (new Date().getTime() + this.idleTimeoutMillis) }; this.availableObjects.push(objWithTimeout); this.dispense(); this.scheduleRemoveIdle(); if (callback) callback(null); }, // Try to get a new client to work, and clean up pool unused (idle) items. // // - If there are available clients waiting, shift the first one out (LIFO), // and call its callback. // - If there are no waiting clients, try to create one if it won't exceed // the maximum number of clients. // - If creating a new client would exceed the maximum, add the client to // the wait list. dispense: function() { var obj = null, objWithTimeout = null, err = null, clientCb = null, waitingCount = this.waitingClients.size(); if (waitingCount > 0) { while (this.availableObjects.length > 0) { objWithTimeout = this.availableObjects[0]; if (!this.validate(objWithTimeout.obj)) { this.destroy(objWithTimeout.obj); continue; } this.availableObjects.shift(); clientCb = this.waitingClients.dequeue(); return clientCb(err, objWithTimeout.obj); } if (this.count < this.max) { this.createResource(); } } }, // Disallow any new requests and let the request backlog dissapate, // Setting the `draining` flag so as to let any additional work on the queue // dissapate. drain: function(callback) { this.draining = true; var pool = this; var checking = function() { if (pool.waitingClients.size() > 0 || pool.availableObjects.length != pool.count) { setTimeout(checking, 100); } else { if (callback) callback(); } }; checking(); }, // Forcibly destroys all clients regardless of timeout. Intended to be // invoked as part of a drain. Does not prevent the creation of new // clients as a result of subsequent calls to acquire. // // Note that if this.min > 0, the pool will destroy all idle resources // in the pool, but replace them with newly created resources up to the // specified this.min value. If this is not desired, set this.min // to zero before calling destroyAllNow() destroyAllNow: function(callback) { var willDie = this.availableObjects; this.availableObjects = []; var obj = willDie.shift(); while (obj !== null && obj !== undefined) { this.destroy(obj.obj); obj = willDie.shift(); } this.removeIdleScheduled = false; clearTimeout(this.removeIdleTimer); if (callback) callback(); }, // Decorates a function to use a acquired client from the object pool when called. pooled: function(decorated, priority) { var pool = this; return function() { var callerArgs = arguments; var callerCallback = callerArgs[callerArgs.length - 1]; var callerHasCallback = typeof callerCallback === 'function'; pool.acquire(function(err, client) { if (err) { if (callerHasCallback) callerCallback(err, null); return; } var args = [client].concat(slice.call(callerArgs, 0, callerHasCallback ? -1 : undefined)); args.push(function() { pool.release.call(pool, client); if (callerHasCallback) callerCallback.apply(null, arguments); }); decorated.apply(null, args); }, priority); }; }, // Request the client to be destroyed. The factory's destroy handler // will also be called. This should be called within an acquire() // block as an alternative to release(). destroy: function(obj) { this.count -= 1; this.availableObjects = this.availableObjects.filter(function(objWithTimeout) { return (objWithTimeout.obj !== obj); }); this.destroyHandler(obj); this.ensureMinimum(); }, // Checks and removes the available (idle) clients that have timed out. removeIdle: function() { var toRemove = [], now = new Date().getTime(), i, availableLength, tr, timeout; this.removeIdleScheduled = false; // Go through the available (idle) items, // check if they have timed out for (i = 0, availableLength = this.availableObjects.length; i < availableLength && (this.refreshIdle || (this.count - this.min > toRemove.length)); i += 1) { timeout = this.availableObjects[i].timeout; if (now >= timeout) { // Client timed out, so destroy it. toRemove.push(this.availableObjects[i].obj); } } for (i = 0, tr = toRemove.length; i < tr; i += 1) { this.destroy(toRemove[i]); } // Replace the available items with the ones to keep. availableLength = this.availableObjects.length; if (availableLength > 0) { this.scheduleRemoveIdle(); } }, // Schedule removal of idle items in the pool. // More schedules cannot run concurrently. scheduleRemoveIdle: function() { if (!this.removeIdleScheduled) { this.removeIdleScheduled = true; var pool = this; this.removeIdleTimer = setTimeout(function() { pool.removeIdle.call(pool); }, this.reapInterval); } }, // Creates a new resource, adding an object to the pool createResource: function() { var pool = this; this.count += 1; this.create(function(err, obj) { var clientCb = pool.waitingClients.dequeue(); if (err) { pool.count -= 1; if (clientCb) clientCb(err, null); setTimeout(function() { pool.dispense.call(pool); }, 0); } else { if (clientCb) return clientCb(null, obj); pool.release(obj); } }); }, // If the client isn't in the process of draining, this ensures // that the minimum number of resources are always around. ensureMinimum: function() { var i, diff; if (!this.draining && (this.count < this.min)) { diff = this.min - this.count; for (i = 0; i < diff; i++) { this.createResource(); } } } }; var slice = Array.prototype.slice; module.exports = { // Export the `Pool` constructor. Pool: Pool, // Export the PriorityQueue constructor, in case anyone wants to fiddle with that. PriorityQueue: PriorityQueue }; }); })( typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports, module); } );