├── .gitignore ├── .jshintrc ├── .npmignore ├── README.md ├── index.js ├── lib ├── cluster.js ├── pool.js └── resource-request.js ├── package.json └── test ├── cluster.test.js ├── pool.test.js └── resource-request.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxparams": 4, 3 | "maxdepth": 3, 4 | "maxstatements": 15, 5 | "maxcomplexity": 6, 6 | "node": true, 7 | "bitwise": true, 8 | "curly": true, 9 | "eqeqeq": true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "regexp": true, 16 | "undef": true, 17 | "unused": "vars", 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "eqnull": true, 22 | "predef": [ "-Promise" ] 23 | } 24 | 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .jshintrc 4 | .npmignore 5 | coverage 6 | node_modules 7 | test 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pool2 2 | 3 | A generic resource pool 4 | 5 | ## Usage 6 | 7 | The values below are the defaults 8 | 9 | var Pool = require('pool2'); 10 | var pool = new Pool({ 11 | acquire: function (cb) { cb(null, resource); }, 12 | acquireTimeout: 30*1000, // please see note below 13 | 14 | dispose: function (res, cb) { cb(); }, 15 | disposeTimeout: 30*1000, 16 | 17 | destroy: function (res) { }, 18 | 19 | ping: function (res, cb) { cb(); }, 20 | pingTimeout: 10*1000, 21 | 22 | capabilities: ['tags'], 23 | 24 | min: 0, 25 | max: 10, 26 | 27 | maxRequests: Infinity, 28 | requestTimeout: Infinity, 29 | 30 | idleTimeout: 60*1000, 31 | syncInterval: 10*1000, 32 | 33 | backoff: { }, 34 | bailAfter: 0 35 | }); 36 | 37 | pool.acquire(function (err, rsrc) { 38 | // do stuff 39 | pool.release(rsrc); 40 | }); 41 | 42 | pool.stats(); 43 | /* { 44 | min: 0, 45 | max: 10, 46 | allocated: 0, 47 | available: 0, 48 | queued: 0, 49 | maxRequests: Infinity 50 | } */ 51 | 52 | pool.remove(rsrc); 53 | pool.destroy(rsrc); 54 | 55 | pool.end(function (errs) { 56 | // errs is null or an array of errors from resources that were released 57 | }); 58 | 59 | pool._destroyPool(); 60 | 61 | ## Constructor options 62 | 63 | ### acquire 64 | Required. The function that acquires a resource (e.g. opens a database connection) on behalf of the pool. Accepts a node-style callback. 65 | 66 | ### acquireTimeout 67 | An integer, in milliseconds, to specify how long to wait for a call to `acquire` before failing. 68 | 69 | **NOTE**: `acquireTimeout` is the delay after which `pool2` will stop waiting for a resource and move on with its life. This means that it's possible for in-flight resources that have timed out to remain open and exceed the pool maximum. Resources that complete allocation that have timed out this way will have the disposer called on them. 70 | 71 | If what you want is for something, e.g. a database connection, or http request, to be *destroyed* after some timeout, you probably want something like this, instead: 72 | 73 | ```javascript 74 | acquire: function (cb) { 75 | openThing({ timeout: 2000 }) 76 | .on('timeout', function () { 77 | cb(new Error('Timed out')); 78 | }); 79 | } 80 | ``` 81 | 82 | This puts the burden of timing out and cleaning up on the library for the resource you are using pool2 to manage; you then *react* to the timeout event and notify pool2 of the failure to acquire the resource. 83 | 84 | ### dispose 85 | Required. The function that disposes of a resource (e.g. gracefully closes a database connection) on behalf of the pool. Accepts the resource to dispose of, which is the same object returned by the acquire function, and a node-style callback. 86 | 87 | ### disposeTimeout 88 | An integer, in milliseconds, to specify how long to wait for a call to `dispose` before failing. Resources that fail the `dispose` call will still be removed from the pool, but undefined behavior may occur: if a dispose call fails, it may leave dangling sockets or handles that prevent graceful exit of an application. 89 | 90 | ### destroy 91 | Optional. This function is called with a resource that is destroyed, either by a timeout or failed call to `dispose`, or an explicit call to `pool.destroy()`. There is no node-style callback: this function is a last resort, and is fire-and-forget. 92 | 93 | ### ping 94 | Optional. A function to check whether a resource is still alive (e.g. send `SELECT 1` on a database connection). Accepts the resource to test and a node-style callback. 95 | 96 | ### pingTimeout 97 | An integer, in milliseconds, to specify how long to wait for the `ping` function before giving up and disposing of the resource. 98 | 99 | ### capabilities 100 | An array of strings. This is used in conjunction with clustering to specify which pools in the cluster to select from. For example, you might have a pool of connections to a database master with read-write capability, and another pool of connections to a slave with read-only capability. One would be defined as `capabilities: ['read', 'write']`, and the other as `capabilities: ['read']`. When acquiring from the cluster, you could use `cluster.acquire('read', ...)` and be served a connection from either pool, but if you used `cluster.acquire('write', ...)`, you would only receive connections from the master (read-write) pool. 101 | 102 | ### min 103 | An integer greater than zero. The minimum number of resources to maintain in the pool. If the pool contains fewer resources than this, it will attempt to acquire more until it reaches this value. 104 | 105 | ### max 106 | An integer greater than or equal to `min`. The maximum number of resources the pool may contain. Requests for resources will not cause new resources to be allocated when this number of resources are currently held in the pool (whether checked out or not). If all resources are checked out, requests are queued until one becomes available. 107 | 108 | ### maxRequests 109 | An integer greater than 0 (`Infinity` is also valid), to specify the maximum number of requests that the pool instance will allow. If the request queue exceeds this number, calls to `acquire` will fail with the error `Pool is full`. 110 | 111 | ### requestTimeout 112 | An integer, in milliseconds (`Infinity` is also valid), to specify how long to wait for a successful resource request before failing. 113 | 114 | ### idleTimeout 115 | An integer, in milliseconds, to specify how long a resource must be idle before it may be disposed of. Resources are periodically checked (see `syncInterval` below) for idleness, and idle resources are disposed of unless they would bring the pool below the configured `min` value. 116 | 117 | ### syncInterval 118 | An integer, in milliseconds, to specify how often to dispose of idle resources and/or open new resources to fulfill the pool minimum. 119 | 120 | ### backoff 121 | An object, passed as-is to [simple-backoff](https://www.npmjs.com/package/simple-backoff), which governs the retry rate for failed allocations. `pool2` uses the Fibonacci strategy for backoff timing. Currently only an explicit failure or timeout on an allocation is retried in this fashion; all other errors and removals of resources that may cause the pool's resource collection to fall under the minimum are remedied on the `syncInterval` or an explicit allocation request when no resources are available. 122 | 123 | ### bailAfter 124 | An integer, in milliseconds (`Infinity` is also valid), to specify how long to wait for a successful allocation before failing. `pool2` has a concept of whether a pool is "live", defined by whether it has succeeded in allocating *any* resource yet. When a pool is initialized, its `live` property is set to `false`, and a timestamp is taken. When an allocation succeeds, the `live` property is set to true. When an allocation fails, if `live` is `false`, the difference between the current timestamp and the initial timestamp is taken: if this value is greater than `bailAfter`, the pool is destroyed and an error is emitted. 125 | 126 | The primary purpose of this functionality is to allow for applications to crash and notify the user when configuration is incorrect or some other problem occurs (database isn't started, network configuration has changed, etc.) 127 | 128 | The default for this value is 0, meaning that if the *very first* allocation request fails, pool2 will fail. `Infinity` is an acceptable value, allowing you to retry infinitely. Retries follow the backoff settings, if supplied, though an extra try or two may result from the `syncInterval` setting as well. 129 | 130 | ## Instance methods 131 | 132 | ### pool.acquire() 133 | Acquire a resource from the pool. Accepts a node-style callback, which is given either the resource or an error. Calls to acquire are queued and served in first in, first out order. Currently, acquire requests are queued indefinitely. Requests are subject to the `maxRequests` option; if the queue is full, a call to `acquire` will be rejected with the error `Pool is full`. 134 | 135 | ### pool.remove() 136 | Remove a resource from the pool gracefully. This method should be preferred over `destroy` (see below). This method is fire-and-forget: it does not take a callback, even though the underlying `dispose` function does. 137 | 138 | ### pool.destroy() 139 | Remove a resource from the pool "ungracefully". This immediately removes the resource without attempting to clean it up. Suitable for removing resources that encounter a fatal error and cannot otherwise be nicely dealt with. 140 | 141 | ### pool.stats() 142 | Returns some information about the current state of the pool: 143 | 144 | { 145 | min: 0, 146 | max: 10, 147 | allocated: 0, 148 | available: 0, 149 | queued: 0, 150 | maxRequests: Infinity 151 | } 152 | 153 | ### pool.end() 154 | Attempt to gracefully shut everything down. Calls to `acquire` after calling `end` will be rejected with the error `Pool is ending` (or `Pool is destroyed` once shutdown has completed). Pending resources will not be disposed of until they are released by whatever has checked them out. When all resources have been released back to the pool, calls the `dispose` function on each of them and collects any errors. These errors are passed along to the callback, if provided. 155 | 156 | Example: 157 | 158 | pool.end(function (errors) { 159 | if (!errors.length) { return; } 160 | console.error('Encountered some errors while shutting down:', errors); 161 | }); 162 | 163 | ### pool._destroyPool() 164 | This is an internal method used by pool2 itself primarily during testing. Rejects all pending requests and destroys all open resources; disables timers and sets the pool's status to destroyed. You shouldn't need to call this, but I'm documenting it here anyway. 165 | 166 | ## Clustering 167 | 168 | var pool1 = new Pool(opts1), 169 | pool2 = new Pool(opts2); 170 | 171 | var cluster = new Pool.Cluster([pool1, pool2]); 172 | 173 | cluster.acquire(function (err, rsrc) { 174 | // do stuff 175 | cluster.release(rsrc); 176 | }); 177 | 178 | cluster.acquire('read', function (err, rsrc) { 179 | // if you specify a capability, only pools tagged with that capability 180 | // will be used to serve the request 181 | }); 182 | 183 | cluster.addPool(new Pool(...)); 184 | var pool = cluster.pools[0]; 185 | cluster.removePool(pool); 186 | 187 | cluster.end(function (errs) { 188 | // errs is an array of errors returned from ending the pools 189 | }); 190 | 191 | ## Constructor options 192 | 193 | `Pool.Cluster` takes only one argument to its constructor: an array of `Pool` instances. 194 | 195 | ## Instance methods 196 | 197 | ### cluster.addPool() 198 | Add a pool to the cluster. 199 | 200 | ### cluster.removePool() 201 | Remove a pool from the cluster. 202 | 203 | ### cluster.pools 204 | An array of pools in the cluster. 205 | 206 | ### cluster.acquire(callback) 207 | Just like `pool.acquire`, except it draws a resource from any of the pools in the cluster. Resources are drawn from the pool with the most idle resources first; otherwise, order is undefined. 208 | 209 | ### cluster.acquire('capability', callback) 210 | Like `cluster.acquire`, except only pools that list `'capability'` in their `capabilities` array are considered. 211 | 212 | ### cluster.end() 213 | Calls `pool.end()` on all pools in this cluster, consolidates any errors, and calls back with them 214 | 215 | ## Debugging 216 | Pool2 makes use of the [debug](https://www.npmjs.com/package/debug) module. For a detailed look at what exactly the pool is doing, execute your program with `DEBUG=pool2` set. 217 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Pool = require('./lib/pool'), 4 | Cluster = require('./lib/cluster'); 5 | 6 | Pool.Cluster = Cluster; 7 | module.exports = Pool; 8 | -------------------------------------------------------------------------------- /lib/cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var HashMap = require('hashmap'), 4 | Pool = require('./pool'); 5 | 6 | var inherits = require('util').inherits, 7 | EventEmitter = require('events').EventEmitter; 8 | 9 | function Cluster(pools) { 10 | EventEmitter.call(this); 11 | 12 | if (!pools) { pools = [ ]; } 13 | else if (!Array.isArray(pools)) { pools = [ pools ]; } 14 | 15 | this.pools = [ ]; 16 | this.caps = { }; 17 | this.removeListeners = new HashMap(); 18 | this.sources = new HashMap(); 19 | 20 | this.ended = false; 21 | 22 | pools.forEach(this.addPool, this); 23 | } 24 | inherits(Cluster, EventEmitter); 25 | 26 | Cluster.prototype.addPool = function (pool) { 27 | if (this.ended) { 28 | throw new Error('Cluster.addPool(): Cluster is ended'); 29 | } 30 | if (!(pool instanceof Pool)) { 31 | throw new Error('Cluster.addPool(): Not a valid pool'); 32 | } 33 | if (this.pools.indexOf(pool) > -1) { 34 | throw new Error('Cluster.addPool(): Pool already in cluster'); 35 | } 36 | 37 | this.pools.push(pool); 38 | this._bindListeners(pool); 39 | this._addCapabilities(pool); 40 | }; 41 | Cluster.prototype.removePool = function (pool) { 42 | if (!(pool instanceof Pool)) { 43 | throw new Error('Cluster.removePool(): Not a valid pool'); 44 | } 45 | var idx = this.pools.indexOf(pool); 46 | if (idx === -1) { 47 | throw new Error('Cluster.removePool(): Pool not in cluster'); 48 | } 49 | 50 | this.pools.splice(idx, 1); 51 | this._unbindListeners(pool); 52 | this._removeCapabilities(pool); 53 | }; 54 | Cluster.prototype.acquire = function (cap, cb) { // jshint maxstatements: 20, maxcomplexity: 8 55 | if (typeof cap === 'function') { 56 | cb = cap; 57 | cap = void 0; 58 | } 59 | if (typeof cb !== 'function') { 60 | this.emit('error', new Error('Cluster.acquire(): Callback is required')); 61 | return; 62 | } 63 | if (this.ended) { 64 | cb(new Error('Cluster.acquire(): Cluster is ended')); 65 | return; 66 | } 67 | 68 | var sources = this.pools; 69 | if (cap) { 70 | if (!this.caps[cap] || !this.caps[cap].length) { 71 | cb(new Error('Cluster.acquire(): No pools can fulfil capability: ' + cap)); 72 | return; 73 | } 74 | sources = this.caps[cap]; 75 | } 76 | 77 | var pool = sources.filter(function (pool) { 78 | var stats = pool.stats(); 79 | return stats.queued < stats.maxRequests; 80 | }).sort(function (a, b) { 81 | var statsA = a.stats(), 82 | statsB = b.stats(); 83 | 84 | return (statsB.available - statsB.queued) - (statsA.available - statsA.queued); 85 | })[0]; 86 | 87 | if (!pool) { 88 | cb(new Error('Cluster.acquire(): No pools available')); 89 | return; 90 | } 91 | 92 | pool.acquire(function (err, res) { 93 | if (err) { cb(err); return; } 94 | this.sources.set(res, pool); 95 | process.nextTick(cb.bind(null, null, res)); 96 | }.bind(this)); 97 | }; 98 | Cluster.prototype.release = function (res) { 99 | if (!this.sources.has(res)) { 100 | var err = new Error('Cluster.release(): Unknown resource'); 101 | err.res = res; 102 | this.emit('error', err); 103 | return; 104 | } 105 | var pool = this.sources.get(res); 106 | this.sources.remove(res); 107 | pool.release(res); 108 | }; 109 | Cluster.prototype.end = function (cb) { 110 | if (this.ended) { 111 | if (typeof cb === 'function') { 112 | cb(new Error('Cluster.end(): Cluster is already ended')); 113 | } 114 | return; 115 | } 116 | 117 | this.ended = true; 118 | 119 | var count = this.pools.length, 120 | errs = [ ]; 121 | 122 | this.pools.forEach(function (pool) { 123 | pool.end(function (err, res) { 124 | this.removePool(pool); 125 | if (err) { errs.concat(err); } 126 | count--; 127 | if (count === 0 && typeof cb === 'function') { 128 | cb(errs.length ? errs : null); 129 | } 130 | }.bind(this)); 131 | }, this); 132 | }; 133 | 134 | Cluster.prototype._addCapabilities = function (pool) { 135 | if (!pool.capabilities || !Array.isArray(pool.capabilities)) { return; } 136 | pool.capabilities.forEach(function (cap) { 137 | if (typeof cap !== 'string') { return; } 138 | this.caps[cap] = this.caps[cap] || [ ]; 139 | this.caps[cap].push(pool); 140 | }, this); 141 | }; 142 | Cluster.prototype._removeCapabilities = function (pool) { 143 | if (!pool.capabilities || !Array.isArray(pool.capabilities)) { return; } 144 | pool.capabilities.forEach(function (cap) { 145 | if (typeof cap !== 'string' || !Array.isArray(this.caps[cap])) { return; } 146 | var idx = this.caps[cap].indexOf(pool); 147 | if (idx > -1) { this.caps[cap].splice(idx, 1); } 148 | }, this); 149 | }; 150 | Cluster.prototype._bindListeners = function (pool) { 151 | var onError, onWarn; 152 | 153 | onError = function (err) { 154 | err.source = pool; 155 | this.emit('error', err); 156 | }.bind(this); 157 | 158 | onWarn = function (err) { 159 | err.source = pool; 160 | this.emit('warn', err); 161 | }.bind(this); 162 | 163 | pool.on('error', onError); 164 | pool.on('warn', onWarn); 165 | 166 | this.removeListeners.set(pool, function () { 167 | pool.removeListener('error', onError); 168 | pool.removeListener('warn', onWarn); 169 | }); 170 | }; 171 | Cluster.prototype._unbindListeners = function (pool) { 172 | this.removeListeners.get(pool)(); 173 | this.removeListeners.remove(pool); 174 | }; 175 | 176 | 177 | module.exports = Cluster; 178 | -------------------------------------------------------------------------------- /lib/pool.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Deque = require('double-ended-queue'), 4 | HashMap = require('hashmap'); 5 | 6 | var ResourceRequest = require('./resource-request'); 7 | 8 | var inherits = require('util').inherits, 9 | EventEmitter = require('events').EventEmitter, 10 | debug = require('debug')('pool2'); 11 | 12 | var assert = require('assert'); 13 | 14 | var Backoff = require('simple-backoff').FibonacciBackoff; 15 | 16 | function deprecate(old, current) { 17 | if (process.env.NODE_ENV === 'testing') { return; } 18 | console.log('Pool2: ' + old + ' is deprecated, please use ' + current); 19 | } 20 | 21 | /* Object tagging for debugging. I don't want to modify objects when not debugging, so I only tag them 22 | * if debugging is enabled (setId); however, since I have to access the property of the object in the debug() 23 | * calls, and this may cause accesses on undefined properties of an object, which may cause deoptimization 24 | * of the object, I have to create a helper function to avoid this (getId) 25 | */ 26 | var SEQ = 0; 27 | 28 | function getId(res) { 29 | if (!debug.enabled) { return -1; } 30 | return res.__pool2__id; 31 | } 32 | function setId(res) { 33 | if (!debug.enabled) { return; } 34 | if (res && typeof res === 'object') { 35 | Object.defineProperty(res, '__pool2__id', { 36 | configurable: false, 37 | enumerable: false, 38 | value: SEQ++ 39 | }); 40 | } 41 | } 42 | 43 | function validNum(opts, val, standard, allowZero, allowInfinity) { // jshint ignore: line 44 | if (!opts || !opts.hasOwnProperty(val)) { 45 | return standard; 46 | } 47 | if (allowInfinity && opts[val] === Infinity) { 48 | return Infinity; 49 | } 50 | var num = parseInt(opts[val], 10); 51 | if (isNaN(num) || num !== +opts[val] || !isFinite(num) || num < 0) { 52 | throw new RangeError('Pool2: ' + val + ' must be a positive integer, ' + opts[val] + ' given.'); 53 | } 54 | if (!allowZero && num === 0) { 55 | throw new RangeError('Pool2: ' + val + ' cannot be 0.'); 56 | } 57 | return num; 58 | } 59 | function HOP(a, b) { return a && hasOwnProperty.call(a, b); } 60 | 61 | function Pool(opts) { // jshint maxcomplexity: 12, maxstatements: 45 62 | EventEmitter.call(this); 63 | 64 | opts = opts || { }; 65 | 66 | if (HOP(opts, 'release')) { 67 | deprecate('opts.release', 'opts.dispose'); 68 | opts.dispose = opts.release; 69 | } 70 | 71 | if (HOP(opts, 'releaseTimeout')) { 72 | deprecate('opts.releaseTimeout', 'opts.disposeTimeout'); 73 | opts.disposeTimeout = opts.releaseTimeout; 74 | } 75 | 76 | assert(HOP(opts, 'acquire'), 'new Pool(): opts.acquire is required'); 77 | assert(HOP(opts, 'dispose'), 'new Pool(): opts.dispose is required'); 78 | assert(typeof opts.acquire === 'function', 'new Pool(): opts.acquire must be a function'); 79 | assert(typeof opts.dispose === 'function', 'new Pool(): opts.dispose must be a function'); 80 | assert(!HOP(opts, 'destroy') || typeof opts.destroy === 'function', 'new Pool(): opts.destroy must be a function'); 81 | assert(!HOP(opts, 'ping') || typeof opts.ping === 'function', 'new Pool(): opts.ping must be a function'); 82 | 83 | this._acquire = opts.acquire; 84 | this._dispose = opts.dispose; 85 | this._destroy = opts.destroy || Pool.defaults.destroy; 86 | this._ping = opts.ping || Pool.defaults.ping; 87 | 88 | this.max = validNum(opts, 'max', Pool.defaults.max); 89 | this.min = validNum(opts, 'min', Pool.defaults.min, true); 90 | 91 | assert(this.max >= this.min, 'new Pool(): opts.min cannot be greater than opts.max'); 92 | 93 | this.maxRequests = validNum(opts, 'maxRequests', Pool.defaults.maxRequests, false, true); 94 | this.acquireTimeout = validNum(opts, 'acquireTimeout', Pool.defaults.acquireTimeout, true); 95 | this.disposeTimeout = validNum(opts, 'disposeTimeout', Pool.defaults.disposeTimeout, true); 96 | this.requestTimeout = validNum(opts, 'requestTimeout', Pool.defaults.requestTimeout, false, true); 97 | this.pingTimeout = validNum(opts, 'pingTimeout', Pool.defaults.pingTimeout); 98 | this.idleTimeout = validNum(opts, 'idleTimeout', Pool.defaults.idleTimeout); 99 | this.syncInterval = validNum(opts, 'syncInterval', Pool.defaults.syncInterval, true); 100 | this.bailAfter = validNum(opts, 'bailAfter', Pool.defaults.bailAfter, true, true); 101 | 102 | assert(this.syncInterval > 0 || !HOP(opts, 'idleTimeout'), 'new Pool(): Cannot specify opts.idleTimeout when opts.syncInterval is 0'); 103 | 104 | this.capabilities = Array.isArray(opts.capabilities) ? opts.capabilities.slice() : [ ]; 105 | 106 | if (this.syncInterval !== 0) { 107 | this.syncTimer = setInterval(this._sync.bind(this), this.syncInterval); 108 | } 109 | 110 | this.live = false; 111 | this.ending = false; 112 | this.destroyed = false; 113 | 114 | this.acquiring = 0; 115 | 116 | this.pool = new HashMap(); 117 | this.available = [ ]; 118 | this.requests = new Deque(); 119 | 120 | this.started = new Date(); 121 | this.backoff = new Backoff(opts.backoff); 122 | 123 | if (debug.enabled) { 124 | this._seq = 0; 125 | } 126 | 127 | setImmediate(this._ensureMinimum.bind(this)); 128 | } 129 | inherits(Pool, EventEmitter); 130 | 131 | Pool.defaults = { 132 | destroy: function () { }, 133 | ping: function (res, cb) { setImmediate(cb); }, 134 | min: 0, 135 | max: 10, 136 | acquireTimeout: 30 * 1000, 137 | disposeTimeout: 30 * 1000, 138 | requestTimeout: Infinity, 139 | pingTimeout: 10 * 1000, 140 | idleTimeout: 60 * 1000, 141 | syncInterval: 10 * 1000, 142 | bailAfter: 0, 143 | maxRequests: Infinity 144 | }; 145 | 146 | // return stats on the pool 147 | Pool.prototype.stats = function () { 148 | var allocated = this.pool.count(); 149 | return { 150 | min: this.min, 151 | max: this.max, 152 | allocated: allocated, 153 | available: this.max - (allocated - this.available.length), 154 | queued: this.requests.length, 155 | maxRequests: this.maxRequests 156 | }; 157 | }; 158 | 159 | // request a resource from the pool 160 | Pool.prototype.acquire = function (cb) { 161 | if (this.destroyed || this.ending) { 162 | cb(new Error('Pool is ' + (this.ending ? 'ending' : 'destroyed'))); 163 | return; 164 | } 165 | 166 | if (this.requests.length >= this.maxRequests) { 167 | cb(new Error('Pool is full')); 168 | return; 169 | } 170 | 171 | var req = new ResourceRequest(this.requestTimeout, cb); 172 | req.on('error', this.emit.bind(this, 'warn')); 173 | 174 | this.requests.push(req); 175 | this.emit('request', req); 176 | 177 | setImmediate(this._maybeAllocateResource.bind(this)); 178 | 179 | return req; 180 | }; 181 | 182 | // release the resource back into the pool 183 | Pool.prototype.release = function (res) { // jshint maxstatements: 17 184 | var err; 185 | 186 | if (!this.pool.has(res)) { 187 | err = new Error('Pool.release(): Resource not member of pool'); 188 | err.res = res; 189 | this.emit('error', err); 190 | return; 191 | } 192 | 193 | if (this.available.indexOf(res) > -1) { 194 | err = new Error('Pool.release(): Resource already released (id=' + getId(res) + ')'); 195 | err.res = res; 196 | this.emit('error', err); 197 | return; 198 | } 199 | 200 | this.pool.set(res, new Date()); 201 | this.available.unshift(res); 202 | 203 | if (this.requests.length === 0 && this.pool.count() === this.available.length) { 204 | this.emit('drain'); 205 | } 206 | 207 | this._maybeAllocateResource(); 208 | }; 209 | 210 | // destroy the resource -- should be called only on error conditions and the like 211 | Pool.prototype.destroy = function (res) { 212 | debug('Ungracefully destroying resource (id=%s)', getId(res)); 213 | // make sure resource is not in our available resources array 214 | var idx = this.available.indexOf(res); 215 | if (idx > -1) { this.available.splice(idx, 1); } 216 | 217 | // remove from pool if present 218 | if (this.pool.has(res)) { 219 | this.pool.remove(res); 220 | } 221 | 222 | // destroy is fire-and-forget 223 | try { this._destroy(res); } 224 | catch (e) { this.emit('warn', e); } 225 | 226 | this._ensureMinimum(); 227 | }; 228 | 229 | // attempt to tear down the resource nicely -- should be called when the resource is still valid 230 | // (that is, the dispose callback is expected to behave correctly) 231 | Pool.prototype.remove = function (res, cb) { // jshint maxcomplexity: 8, maxstatements: 20 232 | // called sometimes internally for the timeout logic, but don't want to emit an error in those cases 233 | var timer, skipError = false; 234 | if (typeof cb === 'boolean') { 235 | skipError = cb; 236 | cb = null; 237 | } 238 | 239 | // ensure resource is not in our available resources array 240 | var idx = this.available.indexOf(res); 241 | if (idx > -1) { this.available.splice(idx, 1); } 242 | 243 | if (this.pool.has(res)) { 244 | this.pool.remove(res); 245 | } else if (!skipError) { 246 | // object isn't in our pool -- emit an error 247 | this.emit('error', new Error('Pool.remove() called on non-member')); 248 | } 249 | 250 | // if we don't get a response from the dispose callback 251 | // within the timeout period, attempt to destroy the resource 252 | if (this.disposeTimeout !== 0) { 253 | timer = setTimeout(this.destroy.bind(this, res), this.disposeTimeout); 254 | } 255 | 256 | try { 257 | debug('Attempting to gracefully remove resource (id=%s)', getId(res)); 258 | this._dispose(res, function (e) { 259 | clearTimeout(timer); 260 | if (e) { this.emit('warn', e); } 261 | else { this._ensureMinimum(); } 262 | 263 | if (typeof cb === 'function') { cb(e); } 264 | }.bind(this)); 265 | } catch (e) { 266 | clearTimeout(timer); 267 | this.emit('warn', e); 268 | if (typeof cb === 'function') { cb(e); } 269 | } 270 | }; 271 | 272 | // attempt to gracefully close the pool 273 | Pool.prototype.end = function (cb) { 274 | cb = cb || function () { }; 275 | 276 | this.ending = true; 277 | 278 | var closeResources = function () { 279 | debug('Closing resources'); 280 | clearInterval(this.syncTimer); 281 | 282 | var count = this.pool.count(), 283 | errors = [ ]; 284 | 285 | if (count === 0) { 286 | cb(); 287 | return; 288 | } 289 | 290 | this.pool.forEach(function (value, key) { 291 | this.remove(key, function (err, res) { 292 | if (err) { errors.push(err); } 293 | 294 | count--; 295 | if (count === 0) { 296 | debug('Resources closed'); 297 | if (errors.length) { cb(errors); } 298 | else { cb(); } 299 | } 300 | }); 301 | }.bind(this)); 302 | }.bind(this); 303 | 304 | // begin now, or wait until there are no pending requests 305 | if (this.available.length === this.pool.count() && this.requests.length === 0 && this.acquiring === 0) { 306 | closeResources(); 307 | } else { 308 | debug('Waiting for active requests to conclude before closing resources'); 309 | this.once('drain', closeResources); 310 | } 311 | }; 312 | 313 | Pool.prototype._clearStaleRequests = function () { 314 | while (this.requests.length && this.requests.peekFront().fulfilled) { 315 | this.requests.shift(); 316 | } 317 | }; 318 | 319 | // try to put things in the correct state 320 | Pool.prototype._sync = function () { 321 | debug('sync'); 322 | this._clearStaleRequests(); 323 | 324 | // this is starting to get messy, but will take care of certain edge cases for now 325 | if (this.ending && this.requests.length === 0 && this.acquiring === 0) { 326 | this.emit('drain'); 327 | return; 328 | } 329 | 330 | this._ensureMinimum(); 331 | this._reap(); 332 | this._maybeAllocateResource(); 333 | }; 334 | 335 | // close idle resources 336 | Pool.prototype._reap = function () { 337 | var n = this.pool.count(), 338 | i, c = 0, res, idleTimestamp, 339 | idleThreshold = (new Date()) - this.idleTimeout; 340 | 341 | debug('reap (cur=%d, av=%d)', n, this.available.length); 342 | 343 | for (i = this.available.length; n > this.min && i >= 0; i--) { 344 | res = this.available[i]; 345 | idleTimestamp = this.pool.get(res); 346 | 347 | if (idleTimestamp < idleThreshold) { 348 | n--; c++; 349 | this.remove(res); 350 | } 351 | } 352 | 353 | if (c) { debug('Shrinking pool: destroying %d idle connections', c); } 354 | }; 355 | 356 | // attempt to acquire at least the minimum quantity of resources 357 | Pool.prototype._ensureMinimum = function () { 358 | if (this.ending || this.destroyed) { return; } 359 | 360 | var n = this.min - (this.pool.count() + this.acquiring); 361 | if (n <= 0) { return; } 362 | 363 | debug('Attempting to acquire minimum resources (cur=%d, min=%d)', this.pool.count(), this.min); 364 | while (n--) { this._allocateResource(); } 365 | }; 366 | 367 | // allocate a resource to a waiting request, if possible 368 | Pool.prototype._maybeAllocateResource = function () { // jshint maxstatements: 25, maxcomplexity: 8 369 | this._clearStaleRequests(); 370 | 371 | // do nothing if there are no requests to serve 372 | if (this.requests.length === 0) { return; } 373 | 374 | // call callback if there is a request and a resource to give it 375 | if (this.available.length) { 376 | var res = this.available.shift(), 377 | req = this.requests.shift(); 378 | 379 | debug('Reserving request for resource (id=%s, req=%s)', getId(res), req.id); 380 | 381 | var aborted = false, abort, timer; 382 | 383 | timer = setTimeout(function () { 384 | debug('Ping timeout, removing resource (id=%s)', getId(res)); 385 | abort(); 386 | }, this.pingTimeout); 387 | 388 | abort = function () { 389 | debug('Releasing request to request list (req=%s)', req.id); 390 | 391 | this.emit('requeue', req); 392 | 393 | aborted = true; 394 | clearTimeout(timer); 395 | 396 | this.requests.unshift(req); 397 | this.remove(res); 398 | this._maybeAllocateResource(); 399 | }.bind(this); 400 | 401 | try { 402 | debug('Pinging resource (id=%s)', getId(res)); 403 | 404 | this._ping(res, function (err) { 405 | if (aborted) { 406 | debug('Ping succeeded after timeout, doing nothing (id=%s, req=%s)', getId(res), req.id); 407 | return; 408 | } 409 | clearTimeout(timer); 410 | 411 | if (err) { 412 | debug('Ping errored, releasing resource (id=%s)', getId(res)); 413 | this.emit('warn', err); 414 | abort(); 415 | return; 416 | } 417 | 418 | if (!req.fulfilled) { 419 | debug('Allocating resource to request (id=%s, req=%s); waited %ds', getId(res), req.id, ((new Date()) - req.ts) / 1000); 420 | req.resolve(res); 421 | } else { 422 | debug('Request became fulfilled while pinging resource; discarding (id=%s, req=%s)', getId(res), req.id); 423 | // there's no request to serve, but we've still got a resource checked out -- release it 424 | this.release(res); 425 | } 426 | }.bind(this)); 427 | } catch (err) { 428 | debug('Synchronous throw attempting to ping resource (id=%s): %s', getId(res), err.message); 429 | this.emit('error', err); 430 | abort(); 431 | } 432 | 433 | return; 434 | } 435 | 436 | // allocate a new resource if there is a request but no resource to give it 437 | // and there's room in the pool 438 | var pending = this.requests.length, 439 | toBeAvailable = this.available.length + this.acquiring, 440 | toBeTotal = this.pool.count() + this.acquiring; 441 | 442 | if (pending > toBeAvailable && toBeTotal < this.max) { 443 | debug('Growing pool: no resource to serve request (p=%d, tba=%d, tbt=%d, max=%d)', pending, toBeAvailable, toBeTotal, this.max); 444 | this._allocateResource(); 445 | } else { 446 | debug('Not growing pool: pending=%d, to be available=%d', pending, toBeAvailable); 447 | } 448 | }; 449 | 450 | // create a new resource 451 | Pool.prototype._allocateResource = function () { 452 | if (this.destroyed) { 453 | debug('Not allocating resource: destroyed'); 454 | return; 455 | } 456 | 457 | debug('Attempting to acquire resource (cur=%d, ac=%d)', this.pool.count(), this.acquiring); 458 | 459 | // acquiring is asynchronous, don't over-allocate due to in-progress resource allocation 460 | this.acquiring++; 461 | 462 | var onError, timer, destroyFn; 463 | 464 | onError = function (err) { 465 | clearTimeout(timer); 466 | 467 | debug('Couldn\'t allocate new resource: %s', err.message); 468 | 469 | // if the acquire function returned a destructor, call it 470 | if (typeof destroyFn === 'function') { 471 | try { 472 | destroyFn(); 473 | } catch (e) { 474 | this.emit('warn', e); 475 | } 476 | } 477 | 478 | // throw an error if we haven't successfully allocated a resource within 479 | // the alloted time 480 | var now = new Date(); 481 | if (this.live === false && now - this.started >= this.bailAfter) { 482 | debug('Destroying pool: unable to aquire a resource within %ds', this.bailAfter/1000); 483 | this._destroyPool(); 484 | this.emit('error', err); 485 | return; 486 | } 487 | 488 | // timed out allocations are dropped from the pool. this could leave us 489 | // below the minimum threshold (try to acquire new resources), or without 490 | // anything attempting to service a pending request (min=0) 491 | // try to bring us up to the minimum and/or service requests, but don't spam 492 | setTimeout(this._sync.bind(this), this.backoff.next()); 493 | }.bind(this); 494 | 495 | if (this.acquireTimeout !== 0) { 496 | timer = setTimeout(function () { 497 | debug('Timed out acquiring resource'); 498 | timer = null; 499 | this.acquiring--; 500 | 501 | onError(new Error('Timed out acquiring resource')); 502 | }.bind(this), this.acquireTimeout); 503 | } 504 | 505 | try { 506 | destroyFn = this._acquire(function (err, res) { // jshint maxstatements: 25 507 | 508 | if (err == null && res == null) { // null OR undefined 509 | onError(new Error('Acquire callback gave no error and no resource -- check your Pool instance\'s acquire function')); 510 | return; 511 | } 512 | 513 | setId(res); 514 | 515 | if (timer) { 516 | clearTimeout(timer); 517 | timer = null; 518 | this.acquiring--; 519 | } else if (!err) { 520 | debug('Attempting to gracefully clean up late-arrived resource (id=%s)', getId(res)); 521 | this.remove(res, true); 522 | return; 523 | } 524 | 525 | if (err) { 526 | onError(err); 527 | return; 528 | } 529 | 530 | this.live = true; 531 | 532 | debug('Successfully allocated new resource (cur=%d, ac=%d, id=%s)', this.pool.count(), this.acquiring, getId(res)); 533 | 534 | this.pool.set(res, new Date()); 535 | this.available.unshift(res); 536 | 537 | // normally 'drain' is emitted when the pending requests queue is empty; pending requests 538 | // are the primary source of acquiring new resources. the pool minimum can cause resources 539 | // to be acquired with no pending requests, however. if pool.end() is called while resources 540 | // are being acquired to fill the minimum, the 'drain' event will never get triggered because 541 | // there were no requests pending. in this case, we want to trigger the cleanup routine that 542 | // normally binds to 'drain' 543 | if (this.ending && this.requests.length === 0 && this.acquiring === 0) { 544 | this.emit('drain'); 545 | return; 546 | } 547 | 548 | // we've successfully acquired a resource, and we only get 549 | // here if something wants it, so... do that 550 | this._maybeAllocateResource(); 551 | }.bind(this)); 552 | } catch (e) { 553 | onError(e); 554 | } 555 | }; 556 | 557 | // destroy the pool itself 558 | Pool.prototype._destroyPool = function () { 559 | this.destroyed = true; 560 | clearInterval(this.syncTimer); 561 | this.pool.forEach(function (value, key) { 562 | this.destroy(key); 563 | }.bind(this)); 564 | this.pool.clear(); 565 | 566 | // requests is a deque, no forEach 567 | var req; 568 | while (( req = this.requests.shift() )) { 569 | req.reject(new Error('Pool was destroyed')); 570 | } 571 | 572 | this.acquiring = 0; 573 | this.available.length = 0; 574 | }; 575 | 576 | Pool._validNum = validNum; 577 | 578 | module.exports = Pool; 579 | -------------------------------------------------------------------------------- /lib/resource-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var inherits = require('util').inherits, 4 | EventEmitter = require('events').EventEmitter, 5 | debug = require('debug')('pool2'); 6 | 7 | var _id = 0; 8 | 9 | // this has promisey semantics but can't really be replaced with a simple promise 10 | function ResourceRequest(timeout, callback) { 11 | if (typeof timeout === 'function') { 12 | callback = timeout; 13 | timeout = Infinity; 14 | } 15 | if (typeof callback !== 'function') { 16 | throw new Error('new ResourceRequest(): callback is required'); 17 | } 18 | 19 | EventEmitter.call(this); 20 | 21 | this.id = _id++; 22 | this.ts = new Date(); 23 | this.cb = callback; 24 | this.fulfilled = false; 25 | this.timer = null; 26 | 27 | debug('New ResourceRequest (id=%s, timeout=%s)', this.id, timeout); 28 | 29 | if (timeout !== Infinity) { this.setTimeout(timeout); } 30 | } 31 | inherits(ResourceRequest, EventEmitter); 32 | 33 | ResourceRequest.prototype.setTimeout = function (_duration) { 34 | if (_duration === Infinity) { 35 | debug('ResourceRequest: setTimeout called with Infinity: clearing timeout'); 36 | this.clearTimeout(); 37 | return; 38 | } 39 | 40 | var duration = parseInt(_duration, 10); 41 | 42 | if (isNaN(duration) || duration <= 0) { 43 | throw new Error('ResourceRequest.setTimeout(): invalid duration: ' + duration); 44 | } 45 | 46 | var now = new Date(), 47 | elapsed = now - this.ts; 48 | 49 | if (elapsed > duration) { 50 | setImmediate(this._rejectTimeout.bind(this)); 51 | } else { 52 | this.timer = setTimeout(this._rejectTimeout.bind(this), duration - elapsed); 53 | } 54 | }; 55 | ResourceRequest.prototype.clearTimeout = function () { 56 | debug('ResourceRequest: clearing timeout (id=%s)', this.id); 57 | clearTimeout(this.timer); 58 | this.timer = null; 59 | }; 60 | ResourceRequest.prototype.resolve = function (res) { 61 | debug('ResourceRequest: resolve (id=%s)', this.id); 62 | this._fulfill(null, res); 63 | }; 64 | ResourceRequest.prototype.reject = function (err) { 65 | debug('ResourceRequest: reject (id=%s)', this.id); 66 | this._fulfill(err); 67 | }; 68 | ResourceRequest.prototype.abort = function (msg) { 69 | msg = msg || 'No reason given'; 70 | 71 | debug('ResourceRequest: abort (id=%s)', this.id); 72 | this.reject(new Error('ResourceRequest aborted: ' + msg)); 73 | }; 74 | ResourceRequest.prototype._rejectTimeout = function () { 75 | debug('ResourceRequest: rejectTimeout (id=%s)', this.id); 76 | this.reject(new Error('ResourceRequest timed out')); 77 | }; 78 | ResourceRequest.prototype._fulfill = function (err, res) { 79 | if (err !== null) { 80 | debug('ResourceRequest: fulfilling with error: %s (id=%s)', err.message, this.id); 81 | // ensure any error gets emitted 82 | this.emit('error', err); 83 | } else { 84 | debug('ResourceRequest: fulfilling with resource (id=%s)', this.id); 85 | } 86 | 87 | // if we've already fulfilled this request, don't try to do it again 88 | if (this.fulfilled) { 89 | debug('ResourceRequest: redundant fulfill, not calling callback (id=%s)', this.id); 90 | // but make sure somebody knows about it 91 | this.emit('error', new Error('ResourceRequest already fulfilled')); 92 | return; 93 | } 94 | 95 | this.fulfilled = true; 96 | this.clearTimeout(); 97 | this.cb.apply(this, arguments); 98 | }; 99 | 100 | module.exports = ResourceRequest; 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pool2", 3 | "version": "1.4.1", 4 | "repository": { 5 | "type": "git", 6 | "url": "http://github.com/myndzi/pool2" 7 | }, 8 | "description": "Resource pool built with database drivers in mind", 9 | "keywords": [ 10 | "database", 11 | "pool", 12 | "cluster" 13 | ], 14 | "main": "index.js", 15 | "scripts": { 16 | "bamp": "bamp", 17 | "test": "NODE_ENV=testing mocha -u bdd -R spec --bail test/*.test.js;jshint lib/*.js", 18 | "cov": "NODE_ENV=testing istanbul cover node_modules/.bin/_mocha -- -u bdd -R spec --bail test/*.test.js;jshint lib/*.js" 19 | }, 20 | "author": "Kris Reeves", 21 | "license": "ISC", 22 | "dependencies": { 23 | "debug": "^2.1.3", 24 | "double-ended-queue": "^2.1.0-0", 25 | "hashmap": "^2.0.1", 26 | "simple-backoff": "^1.0.0" 27 | }, 28 | "devDependencies": { 29 | "bamp": "^1.0.11", 30 | "istanbul": "^0.3.6", 31 | "jshint": "^2.6.0", 32 | "mocha": "^2.1.0", 33 | "should": "^5.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/cluster.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | 5 | var Pool = require('..'), 6 | Cluster = Pool.Cluster; 7 | 8 | describe('Cluster', function () { 9 | var _seq = 0; 10 | function acquireFn(tag) { 11 | return function seqAcquire(cb) { cb(null, { seq: _seq++, tag: tag }); } 12 | } 13 | function noop(cb) { cb(); } 14 | 15 | var cluster; 16 | afterEach(function () { cluster.end(); }); 17 | 18 | it('Should instantiate with no arguments', function () { 19 | cluster = new Cluster(); 20 | }); 21 | it('Should emit an error when no callback is given', function () { 22 | cluster = new Cluster(); 23 | cluster.acquire.bind(cluster).should.throw(/Callback is required/); 24 | }); 25 | it('Should call back with an error when no pools are available', function (done) { 26 | cluster = new Cluster(); 27 | cluster.acquire(function (err) { 28 | err.should.match(/No pools available/); 29 | done(); 30 | }); 31 | }); 32 | it('Should throw with non-Pool arguments (singular)', function () { 33 | (function () { 34 | cluster = new Cluster('foo'); 35 | }).should.throw(/Not a valid pool/); 36 | }); 37 | it('Should emit an error with non-Pool arguments (array)', function () { 38 | (function () { 39 | cluster = new Cluster(['foo', 'bar']); 40 | }).should.throw(/Not a valid pool/); 41 | }); 42 | it('Should instantiate with a singular argument', function (done) { 43 | var pool1 = new Pool({ 44 | acquire: acquireFn('pool1'), 45 | dispose: noop 46 | }); 47 | 48 | cluster = new Cluster(pool1); 49 | cluster.acquire(done); 50 | }); 51 | it('Should instantiate with an array', function (done) { 52 | var pool1 = new Pool({ 53 | acquire: acquireFn('pool1'), 54 | dispose: noop 55 | }), pool2 = new Pool({ 56 | acquire: acquireFn('pool2'), 57 | dispose: noop 58 | }); 59 | cluster = new Cluster([pool1, pool2]); 60 | 61 | cluster.acquire(done); 62 | }); 63 | it('Should error on releasing an invalid resource', function (done) { 64 | var pool1 = new Pool({ 65 | acquire: acquireFn('pool1'), 66 | dispose: noop 67 | }); 68 | cluster = new Cluster(pool1); 69 | cluster.on('error', done.bind(null, null)); 70 | cluster.release('foo'); 71 | }); 72 | it('Should return from the pool with the most available / fewest queued requests', function (done) { 73 | var pool1 = new Pool({ 74 | acquire: acquireFn('pool1'), 75 | dispose: noop 76 | }), pool2 = new Pool({ 77 | acquire: acquireFn('pool2'), 78 | dispose: noop 79 | }); 80 | cluster = new Cluster([pool1, pool2]); 81 | 82 | cluster.acquire(function (err, res1) { 83 | var tag = res1.tag; 84 | 85 | cluster.acquire(function (err, res2) { 86 | res2.tag.should.not.equal(res1.tag); 87 | 88 | cluster.release(res1); 89 | cluster.release(res2); 90 | 91 | done(); 92 | }); 93 | }); 94 | }); 95 | it('Should error when requested capability is unavailable', function (done) { 96 | var pool1 = new Pool({ 97 | acquire: acquireFn('pool1'), 98 | dispose: noop, 99 | capabilities: ['read'] 100 | }); 101 | cluster = new Cluster(pool1); 102 | 103 | cluster.acquire('write', function (err) { 104 | err.should.match(/No pools can fulfil capability/); 105 | done(); 106 | }); 107 | }); 108 | it('Should return only pools that match requested capabilities (subset)', function (done) { 109 | var pool1 = new Pool({ 110 | acquire: acquireFn('pool1'), 111 | dispose: noop, 112 | capabilities: ['read'] 113 | }), pool2 = new Pool({ 114 | acquire: acquireFn('pool2'), 115 | dispose: noop, 116 | capabilities: ['read', 'write'] 117 | }); 118 | cluster = new Cluster([pool1, pool2]); 119 | 120 | cluster.acquire('write', function (err, res1) { 121 | var tag = res1.tag; 122 | 123 | cluster.acquire('write', function (err, res2) { 124 | res2.tag.should.equal(res1.tag); 125 | 126 | cluster.release(res1); 127 | cluster.release(res2); 128 | 129 | done(); 130 | }); 131 | }); 132 | }); 133 | it('Should return only pools that match requested capabilities (superset)', function (done) { 134 | var pool1 = new Pool({ 135 | acquire: acquireFn('pool1'), 136 | dispose: noop, 137 | capabilities: ['read'] 138 | }), pool2 = new Pool({ 139 | acquire: acquireFn('pool2'), 140 | dispose: noop, 141 | capabilities: ['read', 'write'] 142 | }); 143 | cluster = new Cluster([pool1, pool2]); 144 | 145 | cluster.acquire('read', function (err, res1) { 146 | var tag = res1.tag; 147 | 148 | cluster.acquire('read', function (err, res2) { 149 | res2.tag.should.not.equal(res1.tag); 150 | 151 | cluster.release(res1); 152 | cluster.release(res2); 153 | 154 | done(); 155 | }); 156 | }); 157 | }); 158 | it('Should error if all pools are full', function (done) { 159 | var pool1 = new Pool({ 160 | acquire: acquireFn('pool1'), 161 | dispose: noop, 162 | max: 1 163 | }), pool2 = new Pool({ 164 | acquire: acquireFn('pool2'), 165 | dispose: noop, 166 | max: 1 167 | }); 168 | cluster.acquire(function () { }); 169 | cluster.acquire(function () { }); 170 | cluster.acquire(function (err) { 171 | err.should.match(/No pools available/); 172 | done(); 173 | }); 174 | }); 175 | it('Should wait and end cleanly', function (done) { 176 | var pool1 = new Pool({ 177 | acquire: acquireFn('pool1'), 178 | dispose: noop 179 | }), pool2 = new Pool({ 180 | acquire: acquireFn('pool2'), 181 | dispose: noop 182 | }); 183 | cluster = new Cluster([pool1, pool2]); 184 | 185 | cluster.acquire(function (err, res) { 186 | setTimeout(function () { cluster.release(res); }, 100); 187 | }); 188 | cluster.end(done.bind(null, null)); 189 | }); 190 | it('Should error on acquire when ended', function (done) { 191 | var pool1 = new Pool({ 192 | acquire: acquireFn('pool1'), 193 | dispose: noop 194 | }); 195 | cluster = new Cluster(pool1); 196 | 197 | cluster.acquire('write', function (err) { 198 | err.should.match(/Cluster is ended/); 199 | done(); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /test/pool.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | 5 | var Pool = require('..'); 6 | 7 | describe('validNum', function () { 8 | it('should return the default if opts doesn\'t exist', function () { 9 | Pool._validNum(void 0, 'foo', 33).should.equal(33); 10 | }); 11 | it('should return the default if opts has no own property \'val\'', function () { 12 | Pool._validNum({ }, 'foo', 33).should.equal(33); 13 | Pool._validNum(Object.create({ foo: 22 }), 'foo', 33).should.equal(33); 14 | }); 15 | it('should return the specified key if it exists', function () { 16 | Pool._validNum({ foo: 22 }, 'foo', 33).should.equal(22); 17 | }); 18 | it('should throw if the value is not a positive integer', function () { 19 | [1.2, -3, Infinity, new Date(), [ ], { }, 'keke', null, void 0] 20 | .forEach(function (v) { 21 | (function () { 22 | Pool._validNum({ foo: v }, 'foo', 123); 23 | }).should.throw(/must be a positive integer/); 24 | }); 25 | }); 26 | it('should throw if the value is 0 unless allowZero is true', function () { 27 | (function () { 28 | Pool._validNum({ foo: 0 }, 'foo', 123); 29 | }).should.throw(/cannot be 0/); 30 | Pool._validNum({ foo: 0 }, 'foo', 123, true).should.equal(0); 31 | }); 32 | }); 33 | describe('Pool', function () { 34 | var _seq = 0; 35 | function seqAcquire(cb) { cb(null, { seq: _seq++ }); } 36 | function disposeStub(res, cb) { cb(); } 37 | function noop() { } 38 | 39 | var pool; 40 | afterEach(function () { pool._destroyPool(); }); 41 | 42 | var TYPES = { 43 | undefined: void 0, 44 | null: null, 45 | positiveInteger: 1, 46 | positiveDecimal: 1.5, 47 | positiveInfinity: Infinity, 48 | zero: 0, 49 | negativeInteger: -1, 50 | negativeDecimal: -1.5, 51 | negativeInfinity: -Infinity, 52 | object: { }, 53 | array: [ ], 54 | date: new Date(), 55 | string: 'foo', 56 | booleanTrue: true, 57 | booleanFalse: false, 58 | function: function () { } 59 | }; 60 | 61 | var OPTS = { 62 | acquire: { 63 | required: true, 64 | valids: [ TYPES.function ] 65 | }, 66 | acquireTimeout: { 67 | valids: [ TYPES.zero, TYPES.positiveInteger ] 68 | }, 69 | dispose: { 70 | required: true, 71 | valids: [ TYPES.function ] 72 | }, 73 | disposeTimeout: { 74 | valids: [ TYPES.zero, TYPES.positiveInteger ] 75 | }, 76 | destroy: { 77 | valids: [ TYPES.function ] 78 | }, 79 | ping: { 80 | valids: [ TYPES.function ] 81 | }, 82 | pingTimeout: { 83 | valids: [ TYPES.positiveInteger ] 84 | },/* capabilities currently isn't checked very strictly 85 | capabilities: { 86 | valids: [ TYPES.array ] 87 | },*/ 88 | min: { 89 | valids: [ TYPES.zero, TYPES.positiveInteger ] 90 | }, 91 | max: { 92 | valids: [ TYPES.positiveInteger ] 93 | }, 94 | maxRequests: { 95 | valids: [ TYPES.positiveInteger, TYPES.positiveInfinity ] 96 | }, 97 | requestTimeout: { 98 | valids: [ TYPES.positiveInteger, TYPES.positiveInfinity ] 99 | }, 100 | idleTimeout: { 101 | valids: [ TYPES.positiveInteger ] 102 | }, 103 | syncInterval: { 104 | valids: [ TYPES.zero, TYPES.positiveInteger ] 105 | },/* backoff is passed to the simple-backoff constructor as-is 106 | backoff: { 107 | valids: [ TYPES.object ] 108 | },*/ 109 | bailAfter: { 110 | valids: [ TYPES.zero, TYPES.positiveInteger, TYPES.positiveInfinity ] 111 | } 112 | }; 113 | 114 | describe('constructor', function () { 115 | // generate options of all types and ensure they behave as expected according to config above 116 | 117 | var opts; 118 | beforeEach(function () { 119 | opts = { }; 120 | Object.keys(OPTS).forEach(function (k) { 121 | if (OPTS[k].required) { 122 | opts[k] = OPTS[k].valids[0]; 123 | } 124 | }); 125 | }); 126 | 127 | Object.keys(OPTS).forEach(function (k) { 128 | var cfg = OPTS[k]; 129 | describe(k, function () { 130 | if (cfg.required) { 131 | it('should throw if '+k+' is missing', function () { 132 | delete opts[k]; 133 | (function () { 134 | new Pool(opts); 135 | }).should.throw(/required/); 136 | }); 137 | } 138 | Object.keys(TYPES).forEach(function (t) { 139 | var isValid = cfg.valids.indexOf(TYPES[t]) > -1; 140 | if (isValid) { 141 | it('should allow '+k+' to be '+t, function () { 142 | opts[k] = TYPES[t]; 143 | new Pool(opts); 144 | }); 145 | if (!cfg.required) { 146 | it('should have a default value assigned', function () { 147 | Pool.defaults.should.have.property(k); 148 | }); 149 | it('should allow '+t+' as a default value', function () { 150 | var dflt = Pool.defaults[k]; 151 | after(function () { Pool.defaults[k] = dflt; }); 152 | 153 | Pool.defaults[k] = TYPES[t]; 154 | new Pool(opts); 155 | }); 156 | } 157 | } else { 158 | it('should throw if '+k+' is '+t, function () { 159 | opts[k] = TYPES[t]; 160 | (function () { 161 | new Pool(opts); 162 | }).should.throw(/must be|cannot be/); 163 | }); 164 | } 165 | }); 166 | }); 167 | }); 168 | 169 | // a couple extra sanity checks 170 | it('should throw if min is greater than max', function () { 171 | (function () { 172 | new Pool({ 173 | acquire: noop, 174 | dispose: noop, 175 | min: 3, 176 | max: 2 177 | }); 178 | }).should.throw(/opts\.min cannot be greater than opts\.max/); 179 | }); 180 | it('should throw if idleTimeout is specified when syncInterval is 0', function () { 181 | (function () { 182 | new Pool({ 183 | acquire: noop, 184 | dispose: noop, 185 | syncInterval: 0, 186 | idleTimeout: 3 187 | }); 188 | }).should.throw(/Cannot specify opts\.idleTimeout when opts\.syncInterval is 0/); 189 | }); 190 | }); 191 | 192 | it('should honor resource limit', function (done) { 193 | pool = new Pool({ 194 | acquire: seqAcquire, 195 | dispose: disposeStub, 196 | max: 1 197 | }); 198 | 199 | var waited = false; 200 | pool.acquire(function (err, res) { 201 | pool.acquire(function (err, res) { 202 | pool.release(res); 203 | waited.should.equal(true); 204 | done(); 205 | }); 206 | setTimeout(function () { 207 | waited = true; 208 | pool.release(res); 209 | }, 100); 210 | }); 211 | }); 212 | 213 | it('should allocate the minimum number of resources', function (done) { 214 | pool = new Pool({ 215 | acquire: seqAcquire, 216 | dispose: disposeStub, 217 | min: 1 218 | }); 219 | setTimeout(function () { 220 | pool.stats().allocated.should.equal(1); 221 | done(); 222 | }, 100); 223 | }); 224 | 225 | it('should emit an error if no initial resource can be acquired', function (done) { 226 | pool = new Pool({ 227 | acquire: function (cb) { cb(new Error('fail')); }, 228 | dispose: disposeStub, 229 | min: 1 230 | }); 231 | pool.on('error', done.bind(null, null)); 232 | }); 233 | 234 | it('should emit an error if no initial resource can be acquired (timeout)', function (done) { 235 | pool = new Pool({ 236 | acquire: function () { }, 237 | acquireTimeout: 10, 238 | dispose: disposeStub, 239 | min: 1 240 | }); 241 | pool.on('error', done.bind(null, null)); 242 | }); 243 | 244 | it('should retry initial connections until bailAfter is exceeded', function (done) { 245 | var retries = 0; 246 | pool = new Pool({ 247 | acquire: function (cb) { 248 | retries++; 249 | cb(new Error('fail')); 250 | }, 251 | dispose: disposeStub, 252 | bailAfter: 100, 253 | min: 1 254 | }); 255 | pool.on('error', function () { 256 | retries.should.be.above(1); 257 | done(); 258 | }); 259 | }); 260 | 261 | it('should pass along backoff options', function (done) { 262 | var retries = 0; 263 | pool = new Pool({ 264 | acquire: function (cb) { 265 | retries++; 266 | cb(new Error('fail')); 267 | }, 268 | dispose: disposeStub, 269 | bailAfter: 100, 270 | min: 1, 271 | backoff: { 272 | min: 150 273 | } 274 | }); 275 | pool.on('error', function () { 276 | // one for the initial attempt 277 | // one for the attempt that causes the error to be emitted 278 | retries.should.equal(2); 279 | done(); 280 | }); 281 | }); 282 | 283 | it('should allow Infinity for bailAfter', function () { 284 | pool = new Pool({ 285 | acquire: seqAcquire, 286 | dispose: disposeStub, 287 | bailAfter: Infinity 288 | }); 289 | }); 290 | 291 | it('should emit an error on releasing an invalid resource', function (done) { 292 | pool = new Pool({ 293 | acquire: seqAcquire, 294 | dispose: disposeStub 295 | }); 296 | pool.on('error', done.bind(null, null)); 297 | pool.release('foo'); 298 | }); 299 | 300 | it('should emit an error on releasing an idle resource', function (done) { 301 | pool = new Pool({ 302 | acquire: seqAcquire, 303 | dispose: disposeStub 304 | }); 305 | pool.on('error', done.bind(null, null)); 306 | pool.acquire(function (err, res) { 307 | pool.release(res); 308 | pool.release(res); 309 | }); 310 | }); 311 | 312 | it('should emit an error on removing a non-member', function (done) { 313 | pool = new Pool({ 314 | acquire: seqAcquire, 315 | dispose: disposeStub 316 | }); 317 | pool.on('error', done.bind(null, null)); 318 | pool.remove('foo'); 319 | }); 320 | 321 | it('should allow .remove on an allocated resource', function (done) { 322 | pool = new Pool({ 323 | acquire: seqAcquire, 324 | dispose: done.bind(null, null), 325 | min: 1, 326 | max: 1 327 | }); 328 | pool.acquire(function (err, res) { 329 | pool.remove(res); 330 | pool.stats().allocated.should.equal(0); 331 | }); 332 | }); 333 | 334 | it('should allow .remove on an idle resource', function (done) { 335 | pool = new Pool({ 336 | acquire: seqAcquire, 337 | dispose: done.bind(null, null), 338 | min: 1, 339 | max: 1 340 | }); 341 | 342 | pool.acquire(function (err, res) { 343 | pool.release(res); 344 | pool.remove(res); 345 | pool.stats().allocated.should.equal(0); 346 | }); 347 | }); 348 | 349 | it('should allow .destroy on an allocated resource', function (done) { 350 | pool = new Pool({ 351 | acquire: seqAcquire, 352 | dispose: disposeStub, 353 | destroy: done.bind(null, null) 354 | }); 355 | pool.acquire(function (err, res) { 356 | pool.destroy(res); 357 | }); 358 | }); 359 | 360 | it('should allow .destroy on an idle resource', function (done) { 361 | pool = new Pool({ 362 | acquire: seqAcquire, 363 | dispose: disposeStub, 364 | destroy: done.bind(null, null) 365 | }); 366 | 367 | pool.acquire(function (err, res) { 368 | pool.release(res); 369 | pool.destroy(res); 370 | }); 371 | }); 372 | 373 | it('should call .destroy if .remove times out', function (done) { 374 | pool = new Pool({ 375 | acquire: seqAcquire, 376 | dispose: function () { }, 377 | disposeTimeout: 50, 378 | destroy: done.bind(null, null) 379 | }); 380 | pool.acquire(function (err, res) { 381 | pool.remove(res); 382 | }); 383 | }); 384 | 385 | it('should remove idle resources down to the minimum', function (done) { 386 | pool = new Pool({ 387 | acquire: seqAcquire, 388 | dispose: disposeStub, 389 | syncInterval: 10, 390 | idleTimeout: 10, 391 | min: 1 392 | }); 393 | pool.acquire(function (err, res) { 394 | pool.acquire(function (err, res2) { 395 | pool.release(res); 396 | pool.release(res2); 397 | setTimeout(function () { 398 | pool.stats().allocated.should.equal(1); 399 | done(); 400 | }, 100); 401 | }); 402 | }); 403 | }); 404 | 405 | it('should refill resources up to the minimum', function (done) { 406 | pool = new Pool({ 407 | acquire: seqAcquire, 408 | dispose: disposeStub, 409 | syncInterval: 10, 410 | idleTimeout: 10, 411 | min: 1 412 | }); 413 | pool.acquire(function (err, res) { 414 | pool.remove(res); 415 | 416 | setTimeout(function () { 417 | pool.stats().allocated.should.equal(1); 418 | done(); 419 | }, 100); 420 | }); 421 | }); 422 | 423 | it('should ping resources before use', function (done) { 424 | var pings = 0; 425 | pool = new Pool({ 426 | acquire: seqAcquire, 427 | dispose: disposeStub, 428 | ping: function (res, cb) { pings++; cb(); }, 429 | min: 1, 430 | max: 1 431 | }); 432 | pool.acquire(function (err, res) { 433 | pings.should.equal(1); 434 | pool.release(res); 435 | done(); 436 | }); 437 | }); 438 | 439 | it('should execute requests in order', function (done) { 440 | var count = 0; 441 | pool = new Pool({ 442 | acquire: seqAcquire, 443 | dispose: disposeStub, 444 | min: 1, 445 | max: 1 446 | }); 447 | pool.acquire(function (err, res) { 448 | count.should.equal(0); 449 | count++; 450 | pool.release(res); 451 | }); 452 | pool.acquire(function (err, res) { 453 | count.should.equal(1); 454 | count++; 455 | pool.release(res); 456 | done(); 457 | }); 458 | }); 459 | 460 | it('should acquire a new resource if ping fails', function (done) { 461 | var pings = 0, num; 462 | pool = new Pool({ 463 | acquire: seqAcquire, 464 | dispose: disposeStub, 465 | ping: function (res, cb) { 466 | pings++; 467 | if (pings === 3) { cb(new Error('foo')); } 468 | else { cb(); } 469 | }, 470 | min: 1, 471 | max: 1 472 | }); 473 | pool.acquire(function (err, res) { 474 | num = res; 475 | pool.release(res); 476 | }); 477 | pool.acquire(function (err, res) { 478 | num.should.equal(res); 479 | pool.release(res); 480 | }); 481 | pool.acquire(function (err, res) { 482 | num.should.not.equal(res); 483 | pool.release(res); 484 | done(); 485 | }); 486 | }); 487 | 488 | it('should fail acquire when pool is full', function (done) { 489 | pool = new Pool({ 490 | acquire: seqAcquire, 491 | dispose: disposeStub, 492 | min: 1, 493 | max: 1, 494 | maxRequests: 1 495 | }); 496 | pool.acquire(function (err, res) { 497 | pool.acquire(function (err, res2) { 498 | pool.release(res2); 499 | }); 500 | pool.acquire(function (err) { 501 | err.message.should.match(/Pool is full/); 502 | pool.release(res); 503 | done(); 504 | }); 505 | }); 506 | }); 507 | 508 | it('should end gracefully (no resources)', function (done) { 509 | pool = new Pool({ 510 | acquire: seqAcquire, 511 | dispose: disposeStub 512 | }); 513 | 514 | pool.end(done); 515 | }); 516 | 517 | it('should end gracefully (idle resources)', function (done) { 518 | pool = new Pool({ 519 | acquire: seqAcquire, 520 | dispose: function (res, cb) { cb(); }, 521 | min: 1 522 | }); 523 | setTimeout(function () { 524 | pool.end(done); 525 | }, 100); 526 | }); 527 | 528 | it('should end gracefully (allocated resources)', function (done) { 529 | pool = new Pool({ 530 | acquire: seqAcquire, 531 | dispose: function (res, cb) { cb(); }, 532 | min: 1 533 | }); 534 | 535 | pool.acquire(function (err, res) { 536 | setTimeout(function () { 537 | pool.release(res); 538 | }, 100); 539 | }); 540 | pool.end(done); 541 | }); 542 | 543 | it('should end gracefully (resources in allocation)', function (done) { 544 | pool = new Pool({ 545 | acquire: function (cb) { setTimeout(cb.bind(null, null, { }), 100); }, 546 | dispose: disposeStub 547 | }); 548 | 549 | pool.acquire(function (err, res) { 550 | pool.release(res); 551 | }); 552 | 553 | setTimeout(pool.end.bind(pool, done), 50); 554 | }); 555 | 556 | it('should end gracefully (min-fill with no pending requests)', function (done) { 557 | pool = new Pool({ 558 | acquire: function (cb) { setTimeout(cb.bind(null, null, { }), 100); }, 559 | dispose: disposeStub, 560 | min: 1 561 | }); 562 | 563 | setTimeout(pool.end.bind(pool, done), 50); 564 | }); 565 | 566 | it('should end gracefully (min-fill with no pending requests, min > 1)', function (done) { 567 | var dly = 66, num = 0; 568 | pool = new Pool({ 569 | acquire: function (cb) { 570 | num++; 571 | setTimeout(cb.bind(null, null, { }), dly); 572 | dly += 33; 573 | }, 574 | dispose: function (res, cb) { 575 | num--; 576 | cb(); 577 | }, 578 | min: 2 579 | }); 580 | 581 | setTimeout(function () { 582 | pool.end(function () { 583 | num.should.equal(0); 584 | done(); 585 | }); 586 | }, 33); 587 | }); 588 | 589 | it('should fail acquire when ending', function (done) { 590 | pool = new Pool({ 591 | acquire: seqAcquire, 592 | dispose: function (res, cb) { cb(); }, 593 | min: 1 594 | }); 595 | 596 | pool.end(); 597 | pool.acquire(function (err, res) { 598 | err.message.should.match(/ending/); 599 | done(); 600 | }); 601 | }); 602 | 603 | it('should fail acquire when destroyed', function (done) { 604 | pool = new Pool({ 605 | acquire: seqAcquire, 606 | dispose: function (res, cb) { cb(); }, 607 | min: 1 608 | }); 609 | 610 | pool._destroyPool(); 611 | pool.acquire(function (err, res) { 612 | err.message.should.match(/destroyed/); 613 | done(); 614 | }); 615 | }); 616 | 617 | it('should attempt to nicely release resources that arrived late', function (done) { 618 | var count = 0; 619 | pool = new Pool({ 620 | acquire: function (cb) { 621 | if (count === 1) { setTimeout(cb.bind(null, null, { val: 'foo' }), 100); } 622 | else { cb(null, { val: 'bar' }); } 623 | count++; 624 | }, 625 | dispose: function (res, cb) { 626 | try { 627 | res.should.eql({ val: 'foo' }); 628 | done(); 629 | } catch (e) { 630 | done(e); 631 | } 632 | }, 633 | acquireTimeout: 10, 634 | }); 635 | 636 | pool.acquire(function (err, res) { }); 637 | pool.acquire(function (err, res) { }); 638 | }); 639 | 640 | it('should wait for resources to be released back to the pool before ending', function (done) { 641 | var released = false; 642 | pool = new Pool({ 643 | acquire: seqAcquire, 644 | dispose: function (res, cb) { 645 | released = true; 646 | cb(); 647 | } 648 | }); 649 | var count = 3; 650 | var doDone = function(err) { 651 | if (--count) { return; } 652 | done(err); 653 | }; 654 | pool.acquire(function (err, res1) { 655 | pool.acquire(function (err, res2) { 656 | pool.end(function (err) { 657 | released.should.equal(true); 658 | doDone(err); 659 | }); 660 | setTimeout(function () { 661 | released.should.equal(false); 662 | pool.release(res1); 663 | doDone(); 664 | }, 50); 665 | setTimeout(function () { 666 | released.should.equal(false); 667 | pool.release(res2); 668 | doDone(); 669 | }, 100); 670 | }); 671 | }); 672 | }); 673 | 674 | it('should still support release', function(done) { 675 | var called = false; 676 | pool = new Pool({ 677 | acquire: function (cb) { cb(null, 'bar'); }, 678 | release: function (res, cb) { called = true; cb(); }, 679 | min: 1 680 | }); 681 | 682 | setTimeout(function () { 683 | pool.end(function (err, res) { 684 | (!err).should.be.ok; 685 | called.should.equal(true); 686 | done(); 687 | }); 688 | }, 50); 689 | }); 690 | 691 | it('should allow disabling of syncInterval', function () { 692 | pool = new Pool({ 693 | acquire: seqAcquire, 694 | dispose: disposeStub, 695 | syncInterval: 0 696 | }); 697 | // this is usually testing bad behavior, but in this case it's the only simple way to get the job done 698 | pool.should.not.have.property('syncTimer'); 699 | }); 700 | 701 | it('should fallback to destroy if disposeTimeout > 1', function (done) { 702 | pool = new Pool({ 703 | acquire: seqAcquire, 704 | dispose: function (res, cb) { 705 | // time out 706 | }, 707 | destroy: function (res) { 708 | done(); 709 | }, 710 | disposeTimeout: 1 711 | }); 712 | pool.acquire(function (err, res) { 713 | pool.remove(res); 714 | }); 715 | }); 716 | 717 | it('should not fallback to destroy if disposeTimeout = 0', function (done) { 718 | var d = Pool.defaults.disposeTimeout, 719 | destroyed = false; 720 | 721 | before(function () { 722 | Pool.defaults.disposeTimeout = 1; 723 | }); 724 | after(function () { 725 | Pool.defaults.disposeTimeout = d; 726 | }); 727 | 728 | pool = new Pool({ 729 | acquire: seqAcquire, 730 | dispose: function () { 731 | // time out 732 | }, 733 | destroy: function () { 734 | destroyed = true; 735 | }, 736 | disposeTimeout: 0 737 | }); 738 | pool.acquire(function (err, res) { 739 | pool.remove(res); 740 | 741 | setTimeout(function () { 742 | destroyed.should.equal(false); 743 | done(); 744 | }, 50); 745 | }); 746 | }); 747 | 748 | it('should emit an error if a resource cannot be acquired within acquireTimeout ms', function (done) { 749 | pool = new Pool({ 750 | acquire: function () { 751 | // time out 752 | }, 753 | dispose: function () { 754 | }, 755 | acquireTimeout: 1 756 | }); 757 | pool.once('error', function (err) { 758 | err.message.should.match(/Timed out acquiring resource/); 759 | done(); 760 | }); 761 | pool.acquire(function () { }); 762 | }); 763 | 764 | it('should not time out resource acquisition if acquireTimeout = 0', function (done) { 765 | var a = Pool.defaults.acquireTimeout, 766 | timedOut = false; 767 | 768 | before(function () { 769 | Pool.defaults.acquireTimeout = 1; 770 | }); 771 | after(function () { 772 | Pool.defaults.acquireTimeout = a; 773 | }); 774 | 775 | pool = new Pool({ 776 | acquire: function () { 777 | // time out 778 | }, 779 | dispose: function () { 780 | }, 781 | acquireTimeout: 0 782 | }); 783 | pool.once('error', function (err) { 784 | if (/Timed out acquiring resource/.test(err.message)) { timedOut = true; } 785 | else { throw err; } 786 | }); 787 | pool.acquire(function () { }); 788 | 789 | setTimeout(function () { 790 | timedOut.should.equal(false); 791 | done(); 792 | }, 50); 793 | }); 794 | 795 | it('should reject pending resource requests when the pool is destroyed', function (done) { 796 | pool = new Pool({ 797 | acquire: function () { }, 798 | dispose: function () { }, 799 | acquireTimeout: 0 800 | }); 801 | 802 | pool.acquire(function (err) { 803 | err.should.match(/Pool was destroyed/); 804 | done(); 805 | }); 806 | setTimeout(function () { 807 | pool._destroyPool(); 808 | }, 50); 809 | }); 810 | 811 | it('should not overallocate resources while waiting on a ping (#10)', function (done) { 812 | var acquires = 0; 813 | pool = new Pool({ 814 | acquire: function (cb) { 815 | acquires++; 816 | setTimeout(seqAcquire.bind(null, cb), 50); 817 | }, 818 | dispose: disposeStub, 819 | min: 1 820 | }); 821 | pool.acquire(function (err, res) { 822 | acquires.should.equal(1); 823 | done(); 824 | }); 825 | }); 826 | 827 | it('should not call back the allocation request if a ping times out (#14)', function (done) { 828 | var pinged = false, acquires = 0; 829 | pool = new Pool({ 830 | acquire: function (cb) { 831 | acquires++; 832 | seqAcquire(cb); 833 | }, 834 | pingTimeout: 20, 835 | ping: function (res, cb) { 836 | if (pinged) { 837 | // we've already timed out a ping, succeed the rest immediately 838 | cb(); 839 | 840 | // also set a timer to finish up the test later 841 | setTimeout(function () { 842 | acquires.should.equal(2); 843 | done(); 844 | }, 100); 845 | } else { 846 | // force a successful callback after the ping timeout 847 | setTimeout(cb, 50); 848 | } 849 | pinged = true; 850 | }, 851 | dispose: disposeStub, 852 | min: 1 853 | }); 854 | pool.once('error', done); 855 | pool.acquire(function (err, res) { 856 | pool.release(res); 857 | }); 858 | }); 859 | 860 | it('should emit warnings for resource request errors', function (done) { 861 | pool = new Pool({ 862 | acquire: noop, 863 | dispose: noop 864 | }); 865 | 866 | pool.once('warn', function (err) { 867 | err.should.match(/foo/); 868 | }); 869 | 870 | var res = pool.acquire(function (err) { 871 | err.should.match(/aborted/); 872 | done(); 873 | }); 874 | res.abort('foo'); 875 | }); 876 | 877 | it('should correctly create requests with timeout durations when configured', function (done) { 878 | pool = new Pool({ 879 | acquire: noop, 880 | dispose: noop, 881 | requestTimeout: 10 882 | }); 883 | pool.acquire(function (err) { 884 | err.should.match(/timed out/); 885 | done(); 886 | }); 887 | }); 888 | 889 | it('should create requests without timeout durations when not configured', function () { 890 | pool = new Pool({ 891 | acquire: noop, 892 | dispose: noop 893 | }); 894 | var res = pool.acquire(noop); 895 | (res.timer === null).should.be.ok; 896 | }); 897 | 898 | it('should emit a \'request\' event when creating a new resource request', function (done) { 899 | pool = new Pool({ 900 | acquire: noop, 901 | dispose: noop 902 | }); 903 | pool.once('request', done.bind(null, null)); 904 | pool.acquire(noop); 905 | }); 906 | 907 | it('should emit a \'requeue\' event with the request when the request is requeued', function (done) { 908 | pool = new Pool({ 909 | acquire: seqAcquire, 910 | dispose: noop, 911 | ping: function (res, cb) { cb(new Error('foo')); } 912 | }); 913 | var req = pool.acquire(noop); 914 | pool.once('requeue', function (_req) { 915 | _req.should.equal(req); 916 | req.abort(); 917 | done(); 918 | }); 919 | }); 920 | 921 | it('should not allocate resource if there\'s no request to fill', function (done) { 922 | var num = 0; 923 | pool = new Pool({ 924 | acquire: function (cb) { 925 | num++; 926 | // acquire needs to take longer than the request to time out 927 | setTimeout(function () { 928 | cb(null, { }); 929 | }, 50); 930 | }, 931 | dispose: function () { 932 | num--; 933 | }, 934 | requestTimeout: 1, 935 | max: 1 936 | }); 937 | 938 | // generate a request so a resource will be allocated 939 | pool.acquire(function (err) { 940 | err.should.match(/timed out/); 941 | 942 | // the request has timed out but the resource allocation is still pending 943 | // we need to wait for the resource allocation to succeed before checking 944 | // that things are resolved correctly 945 | setTimeout(function () { 946 | // the number of resources in the pool should equal the tally we kept via acquire/dispose 947 | pool.available.length.should.equal(num); 948 | done(); 949 | }, 75); 950 | }); 951 | }); 952 | 953 | it('should correctly release resource after successful ping when request has been fulfilled', function (done) { 954 | var num = 0; 955 | pool = new Pool({ 956 | acquire: function (cb) { 957 | num++; 958 | cb(null, { }); 959 | }, 960 | dispose: function () { 961 | num--; 962 | }, 963 | ping: function (res, cb) { 964 | // ping needs to take longer than the request to time out 965 | setTimeout(function () { 966 | cb(); 967 | }, 50); 968 | }, 969 | requestTimeout: 1, 970 | max: 1 971 | }); 972 | 973 | // generate a request so a resource will be allocated 974 | pool.acquire(function (err) { 975 | err.should.match(/timed out/); 976 | 977 | // the request has timed out but the resource allocation is still pending 978 | // we need to wait for the resource allocation to succeed before checking 979 | // that things are resolved correctly 980 | setTimeout(function () { 981 | // the number of resources in the pool should equal the tally we kept via acquire/dispose 982 | pool.available.length.should.equal(num); 983 | done(); 984 | }, 75); 985 | }); 986 | }); 987 | 988 | it('should not spin idle with pending requests due to min 0 (issue #25)', function (done) { 989 | var num = 0; 990 | pool = new Pool({ 991 | acquire: function (cb) { 992 | if (num++ === 1) { return; } 993 | cb(null, num); 994 | }, 995 | dispose: disposeStub, 996 | acquireTimeout: 50, 997 | min: 0, 998 | max: 1 999 | }); 1000 | 1001 | pool.acquire(function (err, res) { 1002 | // succeed first, to set the pool 'live' 1003 | pool.destroy(res); // ensure we flush the pool to 0 resources 1004 | 1005 | // expect a successful acquisition 1006 | pool.acquire(done); 1007 | }); 1008 | }); 1009 | 1010 | it('should end gracefully with pending requests that time out', function (done) { 1011 | var num = 0; 1012 | pool = new Pool({ 1013 | acquire: function (cb) { 1014 | if (num++ >= 1) { return; } 1015 | cb(null, num); 1016 | }, 1017 | dispose: disposeStub, 1018 | acquireTimeout: 50, 1019 | requestTimeout: 100, 1020 | min: 0 1021 | }); 1022 | 1023 | pool.acquire(function (err, res) { 1024 | (err === null).should.be.ok; 1025 | pool.acquire(function (err, res) { 1026 | err.should.match(/Pool was destroyed/); 1027 | }); 1028 | pool.end(done); 1029 | }); 1030 | }); 1031 | 1032 | it('should not leak resources when acquire times out', function (done) { 1033 | var open = 0; 1034 | 1035 | pool = new Pool({ 1036 | acquire: function (cb) { 1037 | open++; 1038 | return function () { 1039 | open--; 1040 | }; 1041 | }, 1042 | dispose: disposeStub, 1043 | acquireTimeout: 20 1044 | }); 1045 | 1046 | 1047 | pool.acquire(function (err, res) { 1048 | err.should.match(/timed out/); 1049 | open.should.equal(0); 1050 | done(); 1051 | }); 1052 | }); 1053 | 1054 | it('should emit warnings if the acquire disposer fails', function (done) { 1055 | pool = new Pool({ 1056 | acquire: function (cb) { 1057 | return function () { 1058 | throw 'foo'; 1059 | }; 1060 | }, 1061 | dispose: disposeStub, 1062 | acquireTimeout: 20 1063 | }); 1064 | 1065 | pool.once('warn', function (e) { 1066 | e.should.equal('foo'); 1067 | }); 1068 | pool.once('error', function (e) { 1069 | e.should.match(/Timed out/); 1070 | done(); 1071 | }); 1072 | 1073 | pool.acquire(function (err, res) { 1074 | err.should.match(/timed out/); 1075 | }); 1076 | }); 1077 | }); 1078 | -------------------------------------------------------------------------------- /test/resource-request.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | 5 | var ResourceRequest = require('../lib/resource-request'); 6 | 7 | describe('ResourceRequest', function () { 8 | it('should throw without a callback', function () { 9 | (function () { 10 | new ResourceRequest(); 11 | }).should.throw(/callback is required/); 12 | }); 13 | it('should instantiate with a callback', function () { 14 | (function () { 15 | new ResourceRequest(function () { }); 16 | }).should.not.throw(); 17 | }); 18 | it('should accept an optional timeout', function () { 19 | (function () { 20 | var res = new ResourceRequest(123, function () { }); 21 | res.clearTimeout(); 22 | }).should.not.throw(); 23 | }); 24 | it('should not accept a zero timeout', function () { 25 | (function () { 26 | new ResourceRequest(0, function () { }); 27 | }).should.throw(/invalid duration/); 28 | }); 29 | it('should not accept a negative timeout', function () { 30 | (function () { 31 | new ResourceRequest(-123, function () { }); 32 | }).should.throw(/invalid duration/); 33 | }); 34 | it('should accept an Infinity timeout', function () { 35 | (function () { 36 | new ResourceRequest(Infinity, function () { }); 37 | }).should.not.throw(); 38 | }); 39 | it('should time out when given a timeout in the constructor', function (done) { 40 | var res = new ResourceRequest(10, function (err, res) { 41 | err.should.match(/timed out/); 42 | done(); 43 | }); 44 | res.on('error', function (err) { 45 | err.should.match(/timed out/); 46 | }); 47 | }); 48 | it('should time out when using setTimeout', function (done) { 49 | var res = new ResourceRequest(function (err, res) { 50 | err.should.match(/timed out/); 51 | done(); 52 | }); 53 | res.on('error', function (err) { 54 | err.should.match(/timed out/); 55 | }); 56 | setImmediate(function () { 57 | res.setTimeout(10); 58 | }); 59 | }); 60 | it('should time out immediately when using setTimeout with a shorter duration than the elapsed time', function (done) { 61 | var res = new ResourceRequest(function (err, res) { 62 | err.should.match(/timed out/); 63 | done(); 64 | }); 65 | res.on('error', function (err) { 66 | err.should.match(/timed out/); 67 | }); 68 | setTimeout(function () { 69 | res.setTimeout(1); 70 | (res.timer === null).should.be.ok; 71 | }, 50); 72 | }); 73 | it('should clear the timeout if setTimeout is called with Infinity', function () { 74 | var res = new ResourceRequest(10, function (err, res) { 75 | err.should.match(/timed out/); 76 | done(); 77 | }); 78 | res.on('error', function (err) { 79 | err.should.match(/timed out/); 80 | }); 81 | res.setTimeout(Infinity); 82 | (res.timer === null).should.be.ok; 83 | }); 84 | it('should not time out when the timeout has been cleared', function (done) { 85 | var res = new ResourceRequest(50, function (err, res) { 86 | done(); 87 | }); 88 | setImmediate(function () { 89 | res.clearTimeout(); 90 | setTimeout(done, 100); 91 | }); 92 | }); 93 | it('should reject and emit when using abort', function (done) { 94 | var res = new ResourceRequest(function (err) { 95 | err.should.match(/aborted: No reason given/); 96 | done(); 97 | }); 98 | res.on('error', function (err) { 99 | err.should.match(/aborted/); 100 | }); 101 | setImmediate(function () { 102 | res.abort(); 103 | }); 104 | }); 105 | it('should pass along an abort message', function (done) { 106 | var res = new ResourceRequest(function (err) { 107 | err.should.match(/aborted: foo/); 108 | done(); 109 | }); 110 | res.on('error', function (err) { 111 | err.should.match(/aborted/); 112 | }); 113 | setImmediate(function () { 114 | res.abort('foo'); 115 | }); 116 | }); 117 | it('should emit an error when being fulfilled twice, but only call the callback once', function (done) { 118 | var counter = 0; 119 | var res = new ResourceRequest(function () { 120 | counter++; 121 | counter.should.equal(1); 122 | }); 123 | res.on('error', function (err) { 124 | err.should.match(/redundant fulfill/); 125 | done(); 126 | }); 127 | res.resolve(1); 128 | res.resolve(1); 129 | }); 130 | it('should emit an error and call back with an error when rejected', function (done) { 131 | var res = new ResourceRequest(function (err) { 132 | err.should.match(/bar/); 133 | done(); 134 | }); 135 | res.on('error', function (err) { 136 | err.should.match(/bar/); 137 | }); 138 | res.reject(new Error('bar')); 139 | }); 140 | it('should not throw synchronously when setting a timeout that has already expired', function (done) { 141 | var res = new ResourceRequest(1000, function (err) { 142 | err.should.match(/timed out/); 143 | }); 144 | setTimeout(function () { 145 | try { 146 | res.setTimeout(1); 147 | } catch (e) { 148 | done(new Error('Caught synchronous throw from ResourceRequest.setTimeout')); 149 | } 150 | res.on('error', function () { 151 | done(); 152 | }); 153 | }, 25); 154 | }); 155 | }); --------------------------------------------------------------------------------