diff --git a/lib/generic-pool.js b/lib/generic-pool.js index eb9460c2..b89174ab 100644 --- a/lib/generic-pool.js +++ b/lib/generic-pool.js @@ -365,25 +365,76 @@ Pool.prototype._ensureMinimum = function _ensureMinimum () { } } +Pool.prototype._isFull = function () { + return this._count >= this._factory.max && this._availableObjects.length === 0 +} + /** * Request a new client. The callback will be called, - * when a new client will be availabe, passing the client to it. + * when a new client will be available, passing the client to it. * * @param {Function} callback * Callback function to be called after the acquire is successful. * The function will receive the acquired item as the first parameter. * * @param {Number} priority - * Optional. Integer between 0 and (priorityRange - 1). Specifies the priority - * of the caller if there are no available resources. Lower numbers mean higher - * priority. + * Optional. Integer between 0 and (priorityRange - 1). Specifies the + * priority of the caller if there are no available resources. Lower numbers + * mean higher priority. + * + * @param {Object} options + * Optional. If `options.timeout` is set to a non-negative number, throw an + * error if we cannot acquire a client in `options.timeout` ms. If + * `options.timeout` is 0, we will hit the callback in the next tick if no + * clients are available. If no clients are available in the specified time, + * generic-pool will return Pool.full. + * + * Omit the timeout or set it to `null` to wait indefinitely. * * @returns {boolean} `true` if the pool is not fully utilized, `false` otherwise. */ -Pool.prototype.acquire = function acquire (callback, priority) { +Pool.prototype.acquire = function acquire (callback, priority, options) { if (this._draining) { throw new Error('pool is draining and cannot accept work') } + var that = this + if (options !== null && typeof options !== 'undefined') { + if (typeof options.timeout === 'number') { + if (options.timeout < 0 || isNaN(options.timeout)) { + callback(new Error('Timeout set to negative or invalid value: ' + options.timeout)) + return false + } else if (options.timeout === 0) { + if (this._isFull()) { + process.nextTick(function () { + callback(fullError) + }) + return false + } + } else { + var fired = false + var canceled = false + // This would break if Node ever allowed multiple threads, since + // there's a race between checking (fired) and hitting the callback. + var cbCopy = callback + callback = function (err, obj) { + if (canceled) { + that.release(obj) + return + } + fired = true + cbCopy(err, obj) + } + setTimeout(function () { + if (fired) { + return + } else { + canceled = true + cbCopy(fullError) + } + }, options.timeout) + } + } + } this._waitingClients.enqueue(callback, priority) this._dispense() return (this._count < this._factory.max) @@ -562,4 +613,7 @@ Pool.prototype.getMinPoolSize = function getMinPoolSize () { return this._factory.min } +var fullError = new Error('Cannot acquire resource because the pool is full') + +exports.full = fullError exports.Pool = Pool diff --git a/test/generic-pool.test.js b/test/generic-pool.test.js index abecf156..5a2eed93 100644 --- a/test/generic-pool.test.js +++ b/test/generic-pool.test.js @@ -461,6 +461,282 @@ module.exports = { }) }, + 'timeout:0 returns error if pool full': function (beforeExit) { + var assertion_count = 0 + var pool = poolModule.Pool({ + name: 'block-false', + create: function (callback) { callback({id: Math.floor(Math.random() * 1000)}) }, + destroy: function (client) {}, + max: 2, + idleTimeoutMillis: 100 + }) + pool.acquire(function (err, obj1) { + if (err) { throw err } + pool.acquire(function (err, obj2) { + if (err) { throw err } + pool.acquire(function (err) { + assert.equal(err.message, 'Cannot acquire resource because the pool is full') + assertion_count += 1 + assert.equal(err, poolModule.full) + assertion_count += 1 + }, 0, {timeout: 0}) + }, 0, {timeout: 0}) + }) + + beforeExit(function () { + assert.equal(assertion_count, 2) + }) + }, + + 'timeout:5 times out if pool full': function (beforeExit) { + var released = false + var assertion_count = 0 + var createCount = 0 + var pool = poolModule.Pool({ + name: 'timeout5', + create: function (callback) { + callback(null, { count: ++createCount }) + }, + destroy: function (client) {}, + max: 2, + idleTimeoutMillis: 100 + }) + pool.acquire(function (err, obj1) { + if (err) { throw err } + pool.acquire(function (err, obj2) { + if (err) { throw err } + setTimeout(function () { + pool.release(obj1) + released = true + }, 10) + pool.acquire(function (err, obj3) { + assert.equal(err, poolModule.full) + assertion_count += 1 + }, 0, {timeout: 5}) + }, 0, {timeout: 0}) + }) + + beforeExit(function () { + assert(released) + assert.equal(assertion_count, 1) + }) + }, + + // 0. Set poolSize to 2 + // 1. Acquire an object + // 2. Acquire 2nd object (resource created) + // 3. Release one object (all resources created, only one live) + // 4. Acquire an object (should succeed despite resources being created) + 'timeout:0 returns error if all objects created, pool not full': function (beforeExit) { + var assertion_count = 0 + var pool = poolModule.Pool({ + name: 'test-object-created', + create: function (callback) { callback({id: Math.floor(Math.random() * 1000)}) }, + destroy: function (client) {}, + max: 2, + idleTimeoutMillis: 100 + }) + pool.acquire(function (err, obj1) { + if (err) { throw err } + pool.acquire(function (err, obj2) { + if (err) { throw err } + pool.release(obj1) + pool.acquire(function (err, obj3) { + assert.equal(err, null) + assertion_count += 1 + }, 0, {timeout: 0}) + }, 0, {timeout: 0}) + }) + + beforeExit(function () { + assert.equal(assertion_count, 1) + }) + }, + + 'timeout:5 times out at the timeout time, not the release time': function (beforeExit) { + var released = false + var assertion_count = 0 + var createCount = 0 + var pool = poolModule.Pool({ + name: 'timeout5', + create: function (callback) { + callback(null, { count: ++createCount }) + }, + destroy: function (client) {}, + max: 2, + idleTimeoutMillis: 100 + }) + pool.acquire(function (err, obj1) { + if (err) { throw err } + pool.acquire(function (err, obj2) { + if (err) { throw err } + setTimeout(function () { + pool.release(obj1) + released = true + }, 50) + var start = Date.now() + pool.acquire(function (err, obj3) { + // time out times aren't exact, but let's check this is closer to + // 5 than 50 + var diff = Date.now() - start + assert(diff < 25, 'expected time difference to be < 25, was ' + diff) + assertion_count += 1 + assert.equal(err, poolModule.full) + assertion_count += 1 + }, 0, {timeout: 5}) + }, 0, {timeout: 0}) + }) + + beforeExit(function () { + assert(released) + assert.equal(assertion_count, 2) + }) + }, + + 'timeout:5 lets you acquire more resources afterwards': function (beforeExit) { + var released = false + var assertion_count = 0 + var createCount = 0 + var pool = poolModule.Pool({ + name: 'timeout5-release', + create: function (callback) { + callback(null, { count: ++createCount }) + }, + destroy: function (client) {}, + max: 2, + idleTimeoutMillis: 100 + }) + pool.acquire(function (err, obj1) { + if (err) { throw err } + pool.acquire(function (err, obj2) { + if (err) { throw err } + setTimeout(function () { + pool.release(obj1) + released = true + }, 30) + pool.acquire(function (err, obj3) { + assert.equal(err, poolModule.full) + assertion_count += 1 + pool.acquire(function (err, obj4) { + assert.equal(err, null) + assertion_count += 1 + assert.equal(obj4.count, 1) + assertion_count += 1 + }) + }, 0, {timeout: 5}) + }, 0, {timeout: 0}) + }) + + beforeExit(function () { + assert(released) + assert.equal(assertion_count, 3) + }) + }, + + 'timeout: NaN returns an error': function (beforeExit) { + var assertion_count = 0 + var createCount = 0 + var pool = poolModule.Pool({ + name: 'timeout5', + create: function (callback) { + callback(null, { count: ++createCount }) + }, + destroy: function (client) {}, + max: 2 + }) + pool.acquire(function (err, obj1) { + assert(err.message, 'Timeout set to immediate or negative value: NaN') + assertion_count++ + }, 0, {timeout: NaN}) + beforeExit(function () { + assert.equal(assertion_count, 1) + }) + }, + + 'timeout: -1 returns an error': function (beforeExit) { + var assertion_count = 0 + var createCount = 0 + var pool = poolModule.Pool({ + name: 'timeout5', + create: function (callback) { + callback(null, { count: ++createCount }) + }, + destroy: function (client) {}, + max: 2 + }) + pool.acquire(function (err, obj1) { + assert(err.message, 'Timeout set to immediate or negative value: -1') + assertion_count++ + }, 0, {timeout: -1}) + beforeExit(function () { + assert.equal(assertion_count, 1) + }) + }, + + 'timeout:5 returns resource if pool isnt full': function (beforeExit) { + var assertion_count = 0 + var createCount = 0 + var pool = poolModule.Pool({ + name: 'timeout5', + create: function (callback) { + callback(null, { count: ++createCount }) + }, + destroy: function (client) {}, + max: 2, + idleTimeoutMillis: 100 + }) + pool.acquire(function (err, obj1) { + if (err) { throw err } + pool.acquire(function (err, obj2) { + if (err) { throw err } + assert.equal(err, null) + assertion_count += 1 + assert.equal(obj2.count, 2) + assertion_count += 1 + }, 0, {timeout: 5}) + }) + + beforeExit(function () { + assert.equal(assertion_count, 2) + }) + }, + + 'timeout:5 returns resource if it becomes available before timeout': function (beforeExit) { + var released = false + var assertion_count = 0 + var createCount = 0 + var pool = poolModule.Pool({ + name: 'timeout5', + create: function (callback) { + callback(null, { count: ++createCount }) + }, + destroy: function (client) {}, + max: 2, + idleTimeoutMillis: 100 + }) + pool.acquire(function (err, obj1) { + if (err) { throw err } + pool.acquire(function (err, obj2) { + if (err) { throw err } + setTimeout(function () { + pool.release(obj1) + released = true + }, 10) + pool.acquire(function (err, obj3) { + assert.equal(err, null) + assertion_count += 1 + assert.equal(obj3.count, 1) + assertion_count += 1 + }, 0, {timeout: 500}) + }, 0, {timeout: null}) + }) + + beforeExit(function () { + assert(released) + assert.equal(assertion_count, 2) + }) + }, + 'availableObjectsCount': function (beforeExit) { var assertion_count = 0 var pool = poolModule.Pool({