diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..18f75c21 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +.eslintignore +.eslintrc.js +.travis.yml +Makefile +tsconfig.json +test/* diff --git a/.travis.yml b/.travis.yml index db01c40f..afea0147 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ language: node_js +arch: + - amd64 + - ppc64le node_js: - - "4" - - "5" - "6" - - "7" - "8" + - "10" + - "11" install: - make install diff --git a/CHANGELOG.md b/CHANGELOG.md index e61be5ae..500ba7f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,43 @@ -# Change Log +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## [3.9.0](https://github.com/coopernurse/node-pool/compare/v3.8.2...v3.9.0) (2022-09-10) + + +### Bug Fixes + +* add ready function to index.d.ts ([0a5ef1d](https://github.com/coopernurse/node-pool/commit/0a5ef1dd9124ea7d56626c7cc295e181c15ad4e2)) +* unref setTimeout in pool ([e94fd37](https://github.com/coopernurse/node-pool/commit/e94fd374266acb1531b986050ba7486b03b18250)) + +## [3.7.1] - March 28 2019 +- fix #257 - `pool.use` now destroys resources on rejection, matching the docs. + +## [3.6.1] - Febuary 6 2019 +- fix #251 - silence bluebird warning about side-effect only handler (@sandfox) +- Pool.clear no longer resolves to pointless array on undefineds (@restjohn) + +## [3.6.0] - Febuary 4 2019 +- update docs (@chdh) +- fix #159 - pool.clear can no longer "skip" clearing up resources when called early in pool lifecycle (@sandfox) + +## [3.5.0] - January 17 2019 +- update nodejs versions tested by travis +- eviction iterator no longer stops when reaching end of object list #243 (@DiegoRBaquero) +- fix #192 softIdleTimeoutMillis = -1 no longer triggers evictor to run #242 (@DiegoRBaquero) +- fix #234 maxWaitingClients = 0 is no longer ignored #247 (@anupbaldawa) + +## [3.4.2] - Febuary 16 2018 +- fix `pool.use` to resolve after user supplied function has finished. (@asannes) + +## [3.4.1] - Febuary 1 2018 +- prevent timed-out resource requests from being issued resources (@rebareba) + +## [3.4.0] - December 27 2017 +- #218 fix numerous docblock annotations and minor errors in internal classes (@geovanisouza92) + +## [3.3.0] - December 27 2017 +- add `use` method to simplify basic pool usage (@san00) ## [3.2.0] - October 15 2017 - add `isBorrowedResource` method to check if objects are currently on loan from pool (@C-h-e-r-r-y) @@ -161,7 +200,16 @@ - First NPM release ======= -[unreleased]: https://github.com/coopernurse/node-pool/compare/v3.2.0...HEAD + +[unreleased]: https://github.com/coopernurse/node-pool/compare/v3.7.1...HEAD +[3.7.1]: https://github.com/coopernurse/node-pool/compare/v3.6.1...v3.7.1 +[3.6.1]: https://github.com/coopernurse/node-pool/compare/v3.6.0...v3.6.1 +[3.6.0]: https://github.com/coopernurse/node-pool/compare/v3.5.0...v3.6.0 +[3.5.0]: https://github.com/coopernurse/node-pool/compare/v3.4.2...v3.5.0 +[3.4.2]: https://github.com/coopernurse/node-pool/compare/v3.4.1...v3.4.2 +[3.4.1]: https://github.com/coopernurse/node-pool/compare/v3.4.0...v3.4.1 +[3.4.0]: https://github.com/coopernurse/node-pool/compare/v3.3.0...v3.4.0 +[3.3.0]: https://github.com/coopernurse/node-pool/compare/v3.2.0...v3.3.0 [3.2.0]: https://github.com/coopernurse/node-pool/compare/v3.1.8...v3.2.0 [3.1.8]: https://github.com/coopernurse/node-pool/compare/v3.1.7...v3.1.8 [3.1.7]: https://github.com/coopernurse/node-pool/compare/v3.1.6...v3.1.7 diff --git a/README.md b/README.md index 5911d6f8..9a3850d9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ **V3 upgrade warning** -Version 3 contains many breaking changes. The differences are mostly minor and I hope easy to accomodate. There is a very rough and basic [upgrade guide](https://gist.github.com/sandfox/5ca20648b60a0cb959638c0cd6fcd02d) I've written, improvements and other attempts most welcome. +Version 3 contains many breaking changes. The differences are mostly minor and I hope easy to accommodate. There is a very rough and basic [upgrade guide](https://gist.github.com/sandfox/5ca20648b60a0cb959638c0cd6fcd02d) I've written, improvements and other attempts most welcome. If you are after the older version 2 of this library you should look at the [current github branch](https://github.com/coopernurse/node-pool/tree/v2.5) for it. @@ -32,37 +32,27 @@ $ npm install generic-pool [--save] Here is an example using a fictional generic database driver that doesn't implement any pooling whatsoever itself. ```js -var genericPool = require('generic-pool'); -var DbDriver = require('some-db-driver'); +const genericPool = require("generic-pool"); +const DbDriver = require("some-db-driver"); /** * Step 1 - Create pool using a factory object */ const factory = { - create: function(){ - return new Promise(function(resolve, reject){ - var client = DbDriver.createClient() - client.on('connected', function(){ - resolve(client) - }) - }) - }, - destroy: function(client){ - return new Promise(function(resolve){ - client.on('end', function(){ - resolve() - }) - client.disconnect() - }) - } -} + create: function() { + return DbDriver.createClient(); + }, + destroy: function(client) { + client.disconnect(); + } +}; -var opts = { - max: 10, // maximum size of the pool - min: 2 // minimum size of the pool -} +const opts = { + max: 10, // maximum size of the pool + min: 2 // minimum size of the pool +}; -var myPool = genericPool.createPool(factory, opts) +const myPool = genericPool.createPool(factory, opts); /** * Step 2 - Use pool in your code to acquire/release resources @@ -70,18 +60,19 @@ var myPool = genericPool.createPool(factory, opts) // acquire connection - Promise is resolved // once a resource becomes available -const resourcePromise = myPool.acquire() - -resourcePromise.then(function(client) { - client.query("select * from foo", [], function() { - // return object back to pool - myPool.release(client); - }); -}) -.catch(function(err){ - // handle error - this is generally a timeout or maxWaitingClients - // error -}); +const resourcePromise = myPool.acquire(); + +resourcePromise + .then(function(client) { + client.query("select * from foo", [], function() { + // return object back to pool + myPool.release(client); + }); + }) + .catch(function(err) { + // handle error - this is generally a timeout or maxWaitingClients + // error + }); /** * Step 3 - Drain pool during shutdown (optional) @@ -89,7 +80,7 @@ resourcePromise.then(function(client) { // Only call this once in your application -- at the point you want // to shutdown and stop using this pool. myPool.drain().then(function() { - myPool.clear(); + myPool.clear(); }); ``` @@ -108,7 +99,7 @@ Whilst it is possible to directly instantiate the Pool class directly, it is rec The createPool function takes two arguments: - `factory` : an object containing functions to create/destroy/test resources for the `Pool` -- `opts` : an optional object/dictonary to allow configuring/altering behaviour the of the `Pool` +- `opts` : an optional object/dictonary to allow configuring/altering behaviour of the `Pool` ```js const genericPool = require('generic-pool') @@ -125,25 +116,26 @@ Can be any object/instance but must have the following properties: optionally it can also have the following property: -- `validate`: a function that the pool will call if it wants to validate a resource. It should accept one argument `resource` where `resource` is whatever `factory.create` made. Should return a `Promise` that resolves a `boolean` where `true` indicates the resource is still valid or `false` if the resource is invalid. +- `validate`: a function that the pool will call if it wants to validate a resource. It should accept one argument `resource` where `resource` is whatever `factory.create` made. Should return a `Promise` that resolves a `boolean` where `true` indicates the resource is still valid or `false` if the resource is invalid. _Note: The values returned from `create`, `destroy`, and `validate` are all wrapped in a `Promise.resolve` by the pool before being used internally._ **opts** -An optional object/dictionary with the any of the following properties: +An optional object/dictionary with the any of the following properties: - `max`: maximum number of resources to create at any given time. (default=1) - `min`: minimum number of resources to keep in pool at any given time. If this is set >= max, the pool will silently set the min to equal `max`. (default=0) - `maxWaitingClients`: maximum number of queued requests allowed, additional `acquire` calls will be callback with an `err` in a future cycle of the event loop. -- `testOnBorrow`: `boolean`: should the pool validate resources before giving them to clients. Requires that either `factory.validate` or `factory.validateAsync` to be specified +- `testOnBorrow`: `boolean`: should the pool validate resources before giving them to clients. Requires that `factory.validate` is specified. - `acquireTimeoutMillis`: max milliseconds an `acquire` call will wait for a resource before timing out. (default no limit), if supplied should non-zero positive integer. +- `destroyTimeoutMillis`: max milliseconds a `destroy` call will wait for a resource before timing out. (default no limit), if supplied should non-zero positive integer. - `fifo` : if true the oldest resources will be first to be allocated. If false the most recently released resources will be the first to be allocated. This in effect turns the pool's behaviour from a queue into a stack. `boolean`, (default true) - `priorityRange`: int between 1 and x - if set, borrowers can specify their relative priority in the queue if no resources are available. see example. (default 1) - `autostart`: boolean, should the pool start creating resources, initialize the evictor, etc once the constructor is called. If false, the pool can be started by calling `pool.start`, otherwise the first call to `acquire` will start the pool. (default true) - `evictionRunIntervalMillis`: How often to run eviction checks. Default: 0 (does not run). -- `numTestsPerRun`: Number of resources to check each eviction run. Default: 3. +- `numTestsPerEvictionRun`: Number of resources to check each eviction run. Default: 3. - `softIdleTimeoutMillis`: amount of time an object may sit idle in the pool before it is eligible for eviction by the idle object evictor (if any), with the extra condition that at least "min idle" object instances remain in the pool. Default -1 (nothing can get evicted) - `idleTimeoutMillis`: the minimum amount of time that an object may sit idle in the pool before it is eligible for eviction due to idle time. Supercedes `softIdleTimeoutMillis` Default: 30000 - `Promise`: Promise lib, a Promises/A+ implementation that the pool should use. Defaults to whatever `global.Promise` is (usually native promises). @@ -197,7 +189,7 @@ This function is for when you need to check if a resource has been acquired from - `resource`: any object which you need to test -and returns true (primitive, not Promise) if resource is currently borrowed from the pool, false otherwise. +and returns true (primitive, not Promise) if resource is currently borrowed from the pool, false otherwise. ### pool.destroy @@ -228,7 +220,7 @@ pool.on('factoryDestroyError', function(err){ The pool is an event emitter. Below are the events it emits and any args for those events - `factoryCreateError` : emitted when a promise returned by `factory.create` is rejected. If this event has no listeners then the `error` will be silently discarded - - `error`: whatever `reason` the promise was rejected with. + - `error`: whatever `reason` the promise was rejected with. - `factoryDestroyError` : emitted when a promise returned by `factory.destroy` is rejected. If this event has no listeners then the `error` will be silently discarded - `error`: whatever `reason` the promise was rejected with. @@ -241,6 +233,36 @@ pool.start() If `autostart` is `false` then this method can be used to start the pool and therefore begin creation of resources, start the evictor, and any other internal logic. +### pool.ready + +```js +pool.ready() +``` + +Waits for the pool to fully start. + +### pool.use + +```js + +const myTask = dbClient => { + return new Promise( (resolve, reject) => { + // do something with the client and resolve/reject + }) +} + +pool.use(myTask).then(/* a promise that will run after myTask resolves */) +``` + +This method handles acquiring a `resource` from the pool, handing it to your function and then calling `pool.release` or `pool.destroy` with resource after your function has finished. + +`use` takes one required argument: + +- `fn`: a function that accepts a `resource` and returns a `Promise`. Once that promise `resolve`s the `resource` is returned to the pool, else if it `reject`s then the resource is destroyed. +- `priority`: Optionally, you can specify the priority as number. See [Priority Queueing](#priority-queueing) section. + +and returns a `Promise` that either `resolve`s with the value from the user supplied `fn` or `reject`s with an error. + ## Idle Object Eviction The pool has an evictor (off by default) which will inspect idle items in the pool and `destroy` them if they are too old. @@ -253,7 +275,7 @@ By default the evictor does not run, to enable it you must set the `evictionRunI The pool supports optional priority queueing. This becomes relevant when no resources are available and the caller has to wait. `acquire()` accepts an optional priority int which specifies the caller's relative position in the queue. Each priority slot has it's own internal queue created for it. When a resource is available for borrowing, the first request in the highest priority queue will be given it. -Specifying a `priority` to `acquire` that is outside the `priorityRange` set at `Pool` creation time will result in the `priority` being converted the lowest possible `priority` +Specifying a `priority` to `acquire` that is outside the `priorityRange` set at `Pool` creation time will result in the `priority` being converted the lowest possible `priority` ```js // create pool with priorityRange of 3 diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..2c02bade --- /dev/null +++ b/index.d.ts @@ -0,0 +1,196 @@ +// Type definitions for node-pool 3.1 +// Derived from https://github.com/DefinitelyTyped/DefinitelyTyped +// -> https://github.com/DefinitelyTyped/DefinitelyTyped/blob/454dcbcbe5e932010b128dca9793758dd28adb45/types/generic-pool/index.d.ts + +/// + +import { EventEmitter } from "events"; + +export class Deferred { + get state(): 'PENDING' | 'FULFILLED' | 'REJECTED'; + + get promise(): Promise; + + reject(reason: any): void; + + resolve(value: T): void; +} + +export class ResourceRequest extends Deferred { + setTimeout(delay: number): void; + + removeTimeout(): void; +} + +export enum PooledResourceStateEnum { + ALLOCATED = 'ALLOCATED', + IDLE = 'IDLE', + INVALID = 'INVALID', + RETURNING = 'RETURNING', + VALIDATION = 'VALIDATION', +} + +export class PooledResource { + creationTime: number; + lastReturnTime?: number; + lastBorrowTime?: number; + lastIdleTime?: number; + obj: T; + state: PooledResourceStateEnum; + + allocate(): void; + + deallocate(): void; + + invalidate(): void; + + test(): void; + + idle(): void; + + returning(): void; +} + +export interface IDeque extends Iterable { + shift(): T | undefined; + + unshift(element: T): void; + + push(element: T): void; + + pop(): T | undefined; + + get head(): T | undefined; + + get tail(): T | undefined; + + get length(): number; + + iterator(): Iterator; + + reverseIterator(): Iterator; +} + +export class Deque implements IDeque { + shift(): T | undefined; + + unshift(element: T): void; + + push(element: T): void; + + pop(): T | undefined; + + get head(): T | undefined; + + get tail(): T | undefined; + + get length(): number; + + iterator(): Iterator; + + reverseIterator(): Iterator; + + [Symbol.iterator](): Iterator; +} + +export interface IPriorityQueue { + get length(): number; + + enqueue(obj: T, priority?: number): void; + + dequeue(): T | undefined; + + get head(): T | undefined; + + get tail(): T | undefined; +} + +export class PriorityQueue implements IPriorityQueue { + constructor(priorityRange: number); + + get length(): number; + + enqueue(obj: T, priority?: number): void; + + dequeue(): T | undefined; + + get head(): T | undefined; + + get tail(): T | undefined; +} + +export interface IEvictorConfig { + softIdleTimeoutMillis: number; + idleTimeoutMillis: number; + min: number; +} + +export interface IEvictor { + evict(config: IEvictorConfig, pooledResource: PooledResource, availableObjectsCount: number): boolean; +} + +export class DefaultEvictor implements IEvictor { + evict(config: IEvictorConfig, pooledResource: PooledResource, availableObjectsCount: number): boolean; +} + +export interface Factory { + create(): Promise; + + destroy(client: T): Promise; + + validate?(client: T): Promise; +} + +export interface Options { + max?: number; + min?: number; + maxWaitingClients?: number; + testOnBorrow?: boolean; + acquireTimeoutMillis?: number; + destroyTimeoutMillis?: number; + fifo?: boolean; + priorityRange?: number; + autostart?: boolean; + evictionRunIntervalMillis?: number; + numTestsPerEvictionRun?: number; + softIdleTimeoutMillis?: number; + idleTimeoutMillis?: number; +} + +export class Pool extends EventEmitter { + spareResourceCapacity: number; + size: number; + available: number; + borrowed: number; + pending: number; + max: number; + min: number; + + constructor( + Evictor: { new (): IEvictor }, + Deque: { new (): IDeque> }, + PriorityQueue: { new (priorityRange: number): IPriorityQueue> }, + factory: Factory, + options?: Options, + ); + + start(): void; + + acquire(priority?: number): Promise; + + release(resource: T): Promise; + + destroy(resource: T): Promise; + + drain(): Promise; + + clear(): Promise; + + use(cb: (resource: T) => U | Promise): Promise; + + isBorrowedResource(resource: T): boolean; + + ready(): Promise; +} + +export function createPool(factory: Factory, opts?: Options): Pool; diff --git a/lib/DefaultEvictor.js b/lib/DefaultEvictor.js index 918defae..658d345a 100644 --- a/lib/DefaultEvictor.js +++ b/lib/DefaultEvictor.js @@ -5,6 +5,7 @@ class DefaultEvictor { const idleTime = Date.now() - pooledResource.lastIdleTime; if ( + config.softIdleTimeoutMillis > 0 && config.softIdleTimeoutMillis < idleTime && config.min < availableObjectsCount ) { diff --git a/lib/Deque.js b/lib/Deque.js index f7ea50ac..552ec3b1 100644 --- a/lib/Deque.js +++ b/lib/Deque.js @@ -13,10 +13,10 @@ class Deque { /** * removes and returns the first element from the queue - * @return {[type]} [description] + * @return {any} [description] */ shift() { - if (this._length === 0) { + if (this.length === 0) { return undefined; } @@ -28,8 +28,8 @@ class Deque { /** * adds one elemts to the beginning of the queue - * @param {[type]} element [description] - * @return {[type]} [description] + * @param {any} element [description] + * @return {any} [description] */ unshift(element) { const node = DoublyLinkedList.createNode(element); @@ -39,8 +39,8 @@ class Deque { /** * adds one to the end of the queue - * @param {[type]} element [description] - * @return {[type]} [description] + * @param {any} element [description] + * @return {any} [description] */ push(element) { const node = DoublyLinkedList.createNode(element); @@ -52,7 +52,7 @@ class Deque { * removes and returns the last element from the queue */ pop() { - if (this._length === 0) { + if (this.length === 0) { return undefined; } @@ -76,10 +76,10 @@ class Deque { /** * get a reference to the item at the head of the queue - * @return {element} [description] + * @return {any} [description] */ get head() { - if (this._list.length === 0) { + if (this.length === 0) { return undefined; } const node = this._list.head; @@ -88,10 +88,10 @@ class Deque { /** * get a reference to the item at the tail of the queue - * @return {element} [description] + * @return {any} [description] */ get tail() { - if (this._list.length === 0) { + if (this.length === 0) { return undefined; } const node = this._list.tail; diff --git a/lib/DoublyLinkedListIterator.js b/lib/DoublyLinkedListIterator.js index 3916faff..7304a1a2 100644 --- a/lib/DoublyLinkedListIterator.js +++ b/lib/DoublyLinkedListIterator.js @@ -15,8 +15,8 @@ */ class DoublyLinkedListIterator { /** - * @param {Object} doublyLinkedListNode a node that is part of a doublyLinkedList - * @param {Boolean} reverse is this a reverse iterator? default: false + * @param {Object} doublyLinkedList a node that is part of a doublyLinkedList + * @param {Boolean} [reverse=false] is this a reverse iterator? default: false */ constructor(doublyLinkedList, reverse) { this._list = doublyLinkedList; @@ -85,7 +85,6 @@ class DoublyLinkedListIterator { * and adding it to another. * TODO: We can make this smarter by checking the direction of travel and only checking * the required next/prev/head/tail rather than all of them - * @param {[type]} node [description] * @return {Boolean} [description] */ _isCursorDetached() { diff --git a/lib/Pool.js b/lib/Pool.js index 79e38e6d..d0140528 100644 --- a/lib/Pool.js +++ b/lib/Pool.js @@ -7,6 +7,11 @@ const PoolOptions = require("./PoolOptions"); const ResourceRequest = require("./ResourceRequest"); const ResourceLoan = require("./ResourceLoan"); const PooledResource = require("./PooledResource"); +const DefaultEvictor = require("./DefaultEvictor"); +const Deque = require("./Deque"); +const Deferred = require("./Deferred"); +const PriorityQueue = require("./PriorityQueue"); +const DequeIterator = require("./DequeIterator"); const reflector = require("./utils").reflector; @@ -20,6 +25,9 @@ class Pool extends EventEmitter { /** * Generate an Object pool with a specified `factory` and `config`. * + * @param {typeof DefaultEvictor} Evictor + * @param {typeof Deque} Deque + * @param {typeof PriorityQueue} PriorityQueue * @param {Object} factory * Factory to be used for generating and destroying the items. * @param {Function} factory.create @@ -31,6 +39,7 @@ class Pool extends EventEmitter { * @param {Function} factory.validate * Test if a resource is still valid .Should return a promise that resolves to a boolean, true if resource is still valid and false * If it should be removed from pool. + * @param {Object} options */ constructor(Evictor, Deque, PriorityQueue, factory, options) { super(); @@ -66,7 +75,7 @@ class Pool extends EventEmitter { /** * A queue/stack of pooledResources awaiting acquisition * TODO: replace with LinkedList backed array - * @type {Array} + * @type {Deque} */ this._availableObjects = new Deque(); @@ -90,7 +99,7 @@ class Pool extends EventEmitter { /** * All objects associated with this pool in any state (except destroyed) - * @type {PooledResourceCollection} + * @type {Set} */ this._allObjects = new Set(); @@ -102,7 +111,7 @@ class Pool extends EventEmitter { /** * Infinitely looping iterator over available object - * @type {DLLArrayIterator} + * @type {DequeIterator} */ this._evictionIterator = this._availableObjects.iterator(); @@ -110,7 +119,7 @@ class Pool extends EventEmitter { /** * handle for setTimeout for next eviction run - * @type {[type]} + * @type {(number|null)} */ this._scheduledEviction = null; @@ -126,7 +135,9 @@ class Pool extends EventEmitter { this._allObjects.delete(pooledResource); // NOTE: this maybe very bad promise usage? const destroyPromise = this._factory.destroy(pooledResource.obj); - const wrappedDestroyPromise = this._Promise.resolve(destroyPromise); + const wrappedDestroyPromise = this._config.destroyTimeoutMillis + ? this._Promise.resolve(this._applyDestroyTimeout(destroyPromise)) + : this._Promise.resolve(destroyPromise); this._trackOperation( wrappedDestroyPromise, @@ -139,6 +150,15 @@ class Pool extends EventEmitter { this._ensureMinimum(); } + _applyDestroyTimeout(promise) { + const timeoutPromise = new this._Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("destroy timed out")); + }, this._config.destroyTimeoutMillis).unref(); + }); + return this._Promise.race([timeoutPromise, promise]); + } + /** * Attempt to move an available resource into test and then onto a waiting client * @return {Boolean} could we move an available resource into test @@ -184,7 +204,7 @@ class Pool extends EventEmitter { const pooledResource = this._availableObjects.shift(); this._dispatchPooledResourceToNextWaitingClient(pooledResource); - return; + return false; } /** @@ -247,12 +267,15 @@ class Pool extends EventEmitter { /** * Dispatches a pooledResource to the next waiting client (if any) else * puts the PooledResource back on the available list - * @param {[type]} pooledResource [description] - * @return {[type]} [description] + * @param {PooledResource} pooledResource [description] + * @return {Boolean} [description] */ _dispatchPooledResourceToNextWaitingClient(pooledResource) { const clientResourceRequest = this._waitingClientsQueue.dequeue(); - if (clientResourceRequest === undefined) { + if ( + clientResourceRequest === undefined || + clientResourceRequest.state !== Deferred.PENDING + ) { // While we were away either all the waiting clients timed out // or were somehow fulfilled. put our pooledResource back. this._addPooledResourceToAvailableObjects(pooledResource); @@ -294,11 +317,20 @@ class Pool extends EventEmitter { _createResource() { // An attempt to create a resource const factoryPromise = this._factory.create(); - const wrappedFactoryPromise = this._Promise.resolve(factoryPromise); + const wrappedFactoryPromise = this._Promise + .resolve(factoryPromise) + .then(resource => { + const pooledResource = new PooledResource(resource); + this._allObjects.add(pooledResource); + this._addPooledResourceToAvailableObjects(pooledResource); + }); this._trackOperation(wrappedFactoryPromise, this._factoryCreateOperations) - .then(resource => { - this._handleNewResource(resource); + .then(() => { + this._dispense(); + // Stop bluebird complaining about this side-effect only handler + // - a promise was created in a handler but was not returned from it + // https://goo.gl/rRqMUw return null; }) .catch(reason => { @@ -307,13 +339,6 @@ class Pool extends EventEmitter { }); } - _handleNewResource(resource) { - const pooledResource = new PooledResource(resource); - this._allObjects.add(pooledResource); - // TODO: check we aren't exceding our maxPoolSize before doing - this._dispatchPooledResourceToNextWaitingClient(pooledResource); - } - /** * @private */ @@ -341,16 +366,16 @@ class Pool extends EventEmitter { const iterationResult = this._evictionIterator.next(); // Safety check incase we could get stuck in infinite loop because we - // somehow emptied the array after chekcing it's length + // somehow emptied the array after checking its length. if (iterationResult.done === true && this._availableObjects.length < 1) { this._evictionIterator.reset(); return; } - // if this happens it should just mean we reached the end of the + // If this happens it should just mean we reached the end of the // list and can reset the cursor. if (iterationResult.done === true && this._availableObjects.length > 0) { this._evictionIterator.reset(); - break; + continue; } const resource = iterationResult.value; @@ -373,15 +398,18 @@ class Pool extends EventEmitter { _scheduleEvictorRun() { // Start eviction if set if (this._config.evictionRunIntervalMillis > 0) { + // @ts-ignore this._scheduledEviction = setTimeout(() => { this._evict(); this._scheduleEvictorRun(); - }, this._config.evictionRunIntervalMillis); + }, this._config.evictionRunIntervalMillis).unref(); } } _descheduleEvictorRun() { - clearTimeout(this._scheduledEviction); + if (this._scheduledEviction) { + clearTimeout(this._scheduledEviction); + } this._scheduledEviction = null; } @@ -402,13 +430,7 @@ class Pool extends EventEmitter { * when a new resource is available, passing the resource to the callback. * TODO: should we add a seperate "acquireWithPriority" function * - * @param {Function} callback - * Callback function to be called after the acquire is successful. - * If there is an error preventing the acquisition of resource, an error will - * be the first parameter, else it will be null. - * The acquired resource will be the second parameter. - * - * @param {Number} priority + * @param {Number} [priority=0] * Optional. Integer between 0 and (priorityRange - 1). Specifies the priority * of the caller if there are no available resources. Lower numbers mean higher * priority. @@ -428,6 +450,8 @@ class Pool extends EventEmitter { // TODO: should we defer this check till after this event loop incase "the situation" changes in the meantime if ( + this.spareResourceCapacity < 1 && + this._availableObjects.length < 1 && this._config.maxWaitingClients !== undefined && this._waitingClientsQueue.length >= this._config.maxWaitingClients ) { @@ -447,22 +471,42 @@ class Pool extends EventEmitter { } /** - * Check if resource is currently on loan from the pool - * - * @param {Function} resource - * Resource for checking. - * - * @returns {Boolean} - * True if resource belongs to this pool and false otherwise - */ + * [use method, aquires a resource, passes the resource to a user supplied function and releases it] + * @param {Function} fn [a function that accepts a resource and returns a promise that resolves/rejects once it has finished using the resource] + * @return {Promise} [resolves once the resource is released to the pool] + */ + use(fn, priority) { + return this.acquire(priority).then(resource => { + return fn(resource).then( + result => { + this.release(resource); + return result; + }, + err => { + this.destroy(resource); + throw err; + } + ); + }); + } + + /** + * Check if resource is currently on loan from the pool + * + * @param {Function} resource + * Resource for checking. + * + * @returns {Boolean} + * True if resource belongs to this pool and false otherwise + */ isBorrowedResource(resource) { - return this._resourceLoans.get(resource) !== undefined; + return this._resourceLoans.has(resource); } /** * Return the resource to the pool when it is no longer required. * - * @param {Object} obj + * @param {Object} resource * The acquired object to be put back to the pool. */ release(resource) { @@ -580,13 +624,34 @@ class Pool extends EventEmitter { // wait for outstanding factory.create to complete return this._Promise.all(reflectedCreatePromises).then(() => { // Destroy existing resources + // @ts-ignore for (const resource of this._availableObjects) { this._destroy(resource); } const reflectedDestroyPromises = Array.from( this._factoryDestroyOperations ).map(reflector); - return this._Promise.all(reflectedDestroyPromises); + return reflector(this._Promise.all(reflectedDestroyPromises)); + }); + } + + /** + * Waits until the pool is ready. + * We define ready by checking if the current resource number is at least + * the minimum number defined. + * @returns {Promise} that resolves when the minimum number is ready. + */ + ready() { + return new this._Promise(resolve => { + const isReady = () => { + if (this.available >= this.min) { + resolve(); + } else { + setTimeout(isReady, 100); + } + }; + + isReady(); }); } @@ -645,7 +710,7 @@ class Pool extends EventEmitter { /** * number of resources that are currently acquired - * @return {[type]} [description] + * @return {Number} [description] */ get borrowed() { return this._resourceLoans.size; @@ -653,7 +718,7 @@ class Pool extends EventEmitter { /** * number of waiting acquire calls - * @return {[type]} [description] + * @return {Number} [description] */ get pending() { return this._waitingClientsQueue.length; @@ -661,7 +726,7 @@ class Pool extends EventEmitter { /** * maximum size of the pool - * @return {[type]} [description] + * @return {Number} [description] */ get max() { return this._config.max; @@ -669,7 +734,7 @@ class Pool extends EventEmitter { /** * minimum size of the pool - * @return {[type]} [description] + * @return {Number} [description] */ get min() { return this._config.min; diff --git a/lib/PoolDefaults.js b/lib/PoolDefaults.js index 34b36068..56d004c3 100644 --- a/lib/PoolDefaults.js +++ b/lib/PoolDefaults.js @@ -21,6 +21,7 @@ class PoolDefaults { // FIXME: no defaults! this.acquireTimeoutMillis = null; + this.destroyTimeoutMillis = null; this.maxWaitingClients = null; this.min = null; diff --git a/lib/PoolOptions.js b/lib/PoolOptions.js index 9f7b1c97..f355948b 100644 --- a/lib/PoolOptions.js +++ b/lib/PoolOptions.js @@ -4,40 +4,48 @@ const PoolDefaults = require("./PoolDefaults"); class PoolOptions { /** - * @param {Object} config + * @param {Object} opts * configuration for the pool - * @param {Number} config.max + * @param {Number} [opts.max=null] * Maximum number of items that can exist at the same time. Default: 1. * Any further acquire requests will be pushed to the waiting list. - * @param {Number} config.min + * @param {Number} [opts.min=null] * Minimum number of items in pool (including in-use). Default: 0. * When the pool is created, or a resource destroyed, this minimum will * be checked. If the pool resource count is below the minimum, a new * resource will be created and added to the pool. - * @param {Number} config.maxWaitingClients + * @param {Number} [opts.maxWaitingClients=null] * maximum number of queued requests allowed after which acquire calls will be rejected - * @param {Number} config.acquireTimeoutMillis + * @param {Boolean} [opts.testOnBorrow=false] + * should the pool validate resources before giving them to clients. Requires that + * `factory.validate` is specified. + * @param {Boolean} [opts.testOnReturn=false] + * should the pool validate resources before returning them to the pool. + * @param {Number} [opts.acquireTimeoutMillis=null] * Delay in milliseconds after which the an `acquire` call will fail. optional. * Default: undefined. Should be positive and non-zero - * @param {Number} config.priorityRange + * @param {Number} [opts.destroyTimeoutMillis=null] + * Delay in milliseconds after which the an `destroy` call will fail, causing it to emit a factoryDestroyError event. optional. + * Default: undefined. Should be positive and non-zero + * @param {Number} [opts.priorityRange=1] * The range from 1 to be treated as a valid priority - * @param {Bool} [config.fifo=true] + * @param {Boolean} [opts.fifo=true] * Sets whether the pool has LIFO (last in, first out) behaviour with respect to idle objects. * if false then pool has FIFO behaviour - * @param {Bool} [config.autostart=true] + * @param {Boolean} [opts.autostart=true] * Should the pool start creating resources etc once the constructor is called - * @param {Number} opts.evictionRunIntervalMillis + * @param {Number} [opts.evictionRunIntervalMillis=0] * How often to run eviction checks. Default: 0 (does not run). - * @param {Number} opts.numTestsPerEvictionRun + * @param {Number} [opts.numTestsPerEvictionRun=3] * Number of resources to check each eviction run. Default: 3. - * @param {Number} opts.softIdleTimeoutMillis + * @param {Number} [opts.softIdleTimeoutMillis=-1] * amount of time an object may sit idle in the pool before it is eligible * for eviction by the idle object evictor (if any), with the extra condition * that at least "min idle" object instances remain in the pool. Default -1 (nothing can get evicted) - * @param {Number} opts.idleTimeoutMillis + * @param {Number} [opts.idleTimeoutMillis=30000] * the minimum amount of time that an object may sit idle in the pool before it is eligible for eviction * due to idle time. Supercedes "softIdleTimeoutMillis" Default: 30000 - * @param {Promise} [config.Promise=Promise] + * @param {typeof Promise} [opts.Promise=Promise] * What promise implementation should the pool use, defaults to native promises. */ constructor(opts) { @@ -63,14 +71,23 @@ class PoolOptions { : poolDefaults.autostart; if (opts.acquireTimeoutMillis) { + // @ts-ignore this.acquireTimeoutMillis = parseInt(opts.acquireTimeoutMillis, 10); } - if (opts.maxWaitingClients) { + if (opts.destroyTimeoutMillis) { + // @ts-ignore + this.destroyTimeoutMillis = parseInt(opts.destroyTimeoutMillis, 10); + } + + if (opts.maxWaitingClients !== undefined) { + // @ts-ignore this.maxWaitingClients = parseInt(opts.maxWaitingClients, 10); } + // @ts-ignore this.max = parseInt(opts.max, 10); + // @ts-ignore this.min = parseInt(opts.min, 10); this.max = Math.max(isNaN(this.max) ? 1 : this.max, 1); diff --git a/lib/PriorityQueue.js b/lib/PriorityQueue.js index d352b41c..ca9916b2 100644 --- a/lib/PriorityQueue.js +++ b/lib/PriorityQueue.js @@ -9,6 +9,7 @@ const Queue = require("./Queue"); class PriorityQueue { constructor(size) { this._size = Math.max(+size | 0, 1); + /** @type {Queue[]} */ this._slots = []; // initialize arrays to hold queue elements for (let i = 0; i < this._size; i++) { diff --git a/lib/Queue.js b/lib/Queue.js index 4961b0ab..94cab131 100644 --- a/lib/Queue.js +++ b/lib/Queue.js @@ -15,7 +15,7 @@ class Queue extends Deque { * Adds the obj to the end of the list for this slot * we completely override the parent method because we need access to the * node for our rejection handler - * @param {[type]} item [description] + * @param {any} resourceRequest [description] */ push(resourceRequest) { const node = DoublyLinkedList.createNode(resourceRequest); diff --git a/lib/ResourceLoan.js b/lib/ResourceLoan.js index 5985ed12..f657cb4e 100644 --- a/lib/ResourceLoan.js +++ b/lib/ResourceLoan.js @@ -10,8 +10,8 @@ const Deferred = require("./Deferred"); class ResourceLoan extends Deferred { /** * - * @param {PooledResource} pooledResource the PooledResource this loan belongs to - * @return {[type]} [description] + * @param {any} pooledResource the PooledResource this loan belongs to + * @return {any} [description] */ constructor(pooledResource, Promise) { super(Promise); diff --git a/lib/ResourceRequest.js b/lib/ResourceRequest.js index 4c950a6f..aa306193 100644 --- a/lib/ResourceRequest.js +++ b/lib/ResourceRequest.js @@ -52,7 +52,9 @@ class ResourceRequest extends Deferred { } removeTimeout() { - clearTimeout(this._timeout); + if (this._timeout) { + clearTimeout(this._timeout); + } this._timeout = null; } diff --git a/lib/errors.js b/lib/errors.js index c0ae289a..b02d822f 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -3,6 +3,7 @@ class ExtendableError extends Error { constructor(message) { super(message); + // @ts-ignore this.name = this.constructor.name; this.message = message; if (typeof Error.captureStackTrace === "function") { diff --git a/package.json b/package.json index 26ea6f3c..a4fa922d 100644 --- a/package.json +++ b/package.json @@ -1,79 +1,293 @@ { "name": "generic-pool", "description": "Generic resource pooling for Node.JS", - "version": "3.2.0", - "author": "James Cooper ", + "homepage": "https://github.com/coopernurse/node-pool#readme", + "version": "3.9.0", + "main": "index.js", + "author": { + "email": "james@bitmechanic.com", + "name": "James Cooper" + }, "contributors": [ { - "name": "James Cooper", - "email": "james@bitmechanic.com" + "name": "James Butler", + "email": "james.butler@sandfox.co.uk" + }, + { + "name": "Kiko Beats", + "email": "josefrancisco.verdu@gmail.com" + }, + { + "name": "Felipe Machado", + "email": "felipou@gmail.com" + }, + { + "name": "Idan Attias", + "email": "idana@wix.com" + }, + { + "name": "Bryan Donovan", + "email": "bdondo@gmail.com" + }, + { + "name": "C-h-e-r-r-y", + "email": "C-h-e-r-r-y@users.noreply.github.com" + }, + { + "name": "rebareba", + "email": "forcdc1990@gmail.com" + }, + { + "name": "t3hmrman", + "email": "t3hmrman@gmail.com" + }, + { + "name": "Thomas Dimson", + "email": "tdimson@gmail.com" + }, + { + "name": "Anup Baldawa", + "email": "anup@joinhoney.com" + }, + { + "name": "Kevin Burke", + "email": "burke@shyp.com" + }, + { + "name": "Teow Hua Jun", + "email": "huajun@Teows-MacBook-Pro.local" + }, + { + "name": "Joe Z", + "email": "jzarate@gmail.com" }, { "name": "Peter Galiba", - "email": "poetro@poetro.hu", - "url": "http://poetro.hu/" + "email": "poetro@poetro.hu" }, { - "name": "Gary Dusbabek" + "name": "Asbjørn Sannes", + "email": "asbjorn.sannes@interhost.no" }, { - "name": "Tom MacWright", - "url": "http://www.developmentseed.org/" + "name": "san00", + "email": "c5d59d07@opayq.com" }, { - "name": "Douglas Christopher Wilson", - "email": "doug@somethingdoug.com", - "url": "http://somethingdoug.com/" + "name": "Christian d'Heureuse", + "email": "chdh@inventec.ch" }, { - "name": "calibr" + "name": "Ryan Dao", + "email": "ddao@paypal.com" + }, + { + "name": "Diego Rodríguez Baquero", + "email": "diegorbaquero@gmail.com" + }, + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + }, + { + "name": "Geovani de Souza", + "email": "geovanisouza92@gmail.com" + }, + { + "name": "Jemila", + "email": "jemila.abulhawa@gmail.com" }, { "name": "Justin Robinson", - "email": "jrobinson@redventures.com>" + "email": "jrobinson@redventures.com" + }, + { + "name": "linchuang", + "email": "linchuang@tencent.com" }, { "name": "Nayana Hettiarachchi", "email": "nayana@corp-gems.com" }, { - "name": "Felipe Machado", - "email": "felipou@gmail.com" + "name": "restjohn", + "email": "restjohn@users.noreply.github.com" }, { - "name": "Felix Becker", - "email": "felix.b@outlook.com" + "name": "Sushant", + "email": "sushantdhiman@outlook.com" }, { - "name": "sandfox", - "email": "james.butler@sandfox.co.uk" + "name": "travis4all", + "email": "travis4all@diamon.dz" }, { - "name": "Lewis J Ellis", - "email": "me@lewisjellis.com" + "name": "Will Shaver", + "email": "will.shaver@emberex.com" + }, + { + "name": "windyrobin", + "email": "windyrobin@Gmail.com" + }, + { + "name": "王秋石", + "email": "12qiushi@163.com" + }, + { + "name": "Stephen Cresswell", + "email": "229672+cressie176@users.noreply.github.com" + }, + { + "name": "Arek Flinik", + "email": "aflinik@gmail.com" + }, + { + "name": "Alexander Tesfamichael", + "email": "Alex.Tesfamichael@gmail.com" + }, + { + "name": "calibr", + "email": "awcalibr@gmail.com" + }, + { + "name": "benny-medflyt", + "email": "benny@medflyt.com" + }, + { + "name": "Bryan Kaplan", + "email": "bryan@pinchit.com" + }, + { + "name": "Dumitru Uzun", + "email": "contact@duzun.me" + }, + { + "name": "Douglas Christopher Wilson", + "email": "doug@somethingdoug.com" + }, + { + "name": "Drew R", + "email": "drewrathbone@gmail.com" + }, + { + "name": "Magnus Eide", + "email": "eide@iterate.no" + }, + { + "name": "gdusbabek", + "email": "gdusbabek@gmail.com" + }, + { + "name": "Jason Rhodes", + "email": "jason.matthew.rhodes@gmail.com" + }, + { + "name": "John Dooley", + "email": "john.j.dooley@gmail.com" + }, + { + "name": "Lewis Ellis", + "email": "lewis@getsentry.com" + }, + { + "name": "Tom MacWright", + "email": "macwright@gmail.com" + }, + { + "name": "Louis Roché", + "email": "mail+github@louisroche.net" + }, + { + "name": "Mike Morris", + "email": "mikemorris@users.noreply.github.com" + }, + { + "name": "molipet", + "email": "molipet@gmail.com" + }, + { + "name": "An Nguyen Le", + "email": "nguyenan169@gmail.com" + }, + { + "name": "Piotr", + "email": "pwalc@agora.pl" + }, + { + "name": "Randall Leeds", + "email": "randall.leeds@gmail.com" + }, + { + "name": "Roy Binux", + "email": "root@binux.me" + }, + { + "name": "Sandro Santilli", + "email": "strk@keybit.net" + }, + { + "name": "Teemu Ikonen", + "email": "teemu.ikonen@iki.fi" + }, + { + "name": "Tevye Krynski", + "email": "tevye@mog.com" + }, + { + "name": "Thom Seddon", + "email": "thom@nightworld.com" + }, + { + "name": "Thomas Watson Steen", + "email": "w@tson.dk" + }, + { + "name": "Wilfred van der Deijl", + "email": "wilfred@vanderdeijl.com" + }, + { + "name": "Yanlong Wang", + "email": "yanlong.wang@naiver.org" + }, + { + "name": "Young Hahn", + "email": "young@developmentseed.org" + }, + { + "name": "Rajesh kumar", + "email": "zazzel.cvs@gmail.com" } ], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/coopernurse/node-pool.git" + }, + "bugs": { + "url": "https://github.com/coopernurse/node-pool/issues" + }, "keywords": [ "pool", "pooling", "throttle" ], - "main": "index.js", - "repository": { - "type": "git", - "url": "http://github.com/coopernurse/node-pool.git" - }, "devDependencies": { + "@types/node": "^8.5.1", "eslint": "^4.9.0", "eslint-config-prettier": "^2.6.0", "eslint-plugin-prettier": "^2.3.1", "eslint-plugin-promise": "^3.3.0", "prettier": "^1.7.4", - "tap": "^8.0.0" + "tap": "^8.0.0", + "typescript": "^2.6.2" }, "engines": { "node": ">= 4" }, + "files": [ + "index.d.ts", + "index.js", + "lib" + ], "scripts": { "lint": "eslint lib test index.js .eslintrc.js", "lint-fix": "eslint --fix lib test index.js .eslintrc.js", diff --git a/test/GH-159-test.js b/test/GH-159-test.js new file mode 100644 index 00000000..88d55631 --- /dev/null +++ b/test/GH-159-test.js @@ -0,0 +1,71 @@ +const tap = require("tap"); +const createPool = require("../").createPool; +const utils = require("./utils"); +const ResourceFactory = utils.ResourceFactory; + +class ResourceFactoryDelayCreateEachSecond { + constructor() { + this.callCreate = 0; + this.created = 0; + this.destroyed = 0; + this.bin = []; + } + + create() { + const that = this; + console.log(`** create call ${that.callCreate}`); + return new Promise(resolve => { + if (that.callCreate % 2 === 0) { + setTimeout(function() { + console.log(`** created ${that.created}`); + resolve({ id: that.created++ }); + }, 10); + } else { + console.log(`** created ${that.created}`); + resolve({ id: that.created++ }); + } + that.callCreate++; + }); + } + + validate() { + return Promise.resolve(true); + } + + destroy(resource) { + console.log(`** destroying ${resource.id}`); + this.destroyed++; + this.bin.push(resource); + return Promise.resolve(); + } +} + +tap.test("tests drain clear with autostart and min > 0", function(t) { + const count = 5; + let acquired = 0; + + const resourceFactory = new ResourceFactoryDelayCreateEachSecond(); + const config = { + max: 10, + min: 1, + evictionRunIntervalMillis: 500, + idleTimeoutMillis: 30000, + testOnBorrow: true, + autostart: true + }; + const pool = createPool(resourceFactory, config); + + return pool + .drain() + .then(function() { + console.log("** pool drained"); + return pool.clear(); + }) + .then(function() { + console.log("** pool cleared"); + t.equal(resourceFactory.created, resourceFactory.destroyed); + }) + .then(function() { + t.end(); + }); +}); diff --git a/test/generic-pool-destroytimeout-test.js b/test/generic-pool-destroytimeout-test.js new file mode 100644 index 00000000..b3fe6a5c --- /dev/null +++ b/test/generic-pool-destroytimeout-test.js @@ -0,0 +1,74 @@ +"use strict"; + +const tap = require("tap"); +const createPool = require("../").createPool; + +tap.test("destroyTimeout handles timed out destroy calls", function(t) { + const factory = { + create: function() { + return Promise.resolve({}); + }, + destroy: function() { + return new Promise(function(resolve) { + setTimeout(function() { + resolve(); + }, 100); + }); + } + }; + const config = { + destroyTimeoutMillis: 20 + }; + + const pool = createPool(factory, config); + + pool + .acquire() + .then(function(resource) { + pool.destroy(resource); + return new Promise(function(resolve, reject) { + pool.on("factoryDestroyError", function(err) { + t.match(err, /destroy timed out/); + resolve(); + }); + }); + }) + .then(t.end) + .catch(t.error); +}); + +tap.test("destroyTimeout handles non timed out destroy calls", function(t) { + const myResource = {}; + const factory = { + create: function() { + return Promise.resolve({}); + }, + destroy: function() { + return new Promise(function(resolve) { + setTimeout(function() { + resolve(); + }, 10); + }); + } + }; + + const config = { + destroyTimeoutMillis: 400 + }; + + const pool = createPool(factory, config); + + pool + .acquire() + .then(function(resource) { + pool.destroy(resource); + return new Promise(function(resolve) { + pool.on("factoryDestroyError", function(err) { + t.fail("wooops"); + }); + setTimeout(resolve, 20); + }); + }) + .then(t.end) + .catch(t.error); +}); diff --git a/test/generic-pool-test.js b/test/generic-pool-test.js index fdf75287..baaddfbb 100644 --- a/test/generic-pool-test.js +++ b/test/generic-pool-test.js @@ -257,6 +257,47 @@ tap.test("tests drain", function(t) { }); }); +tap.test("clear promise resolves with no value", function(t) { + let resources = []; + const factory = { + create: function create() { + return new Promise(function tryCreate(resolve, reject) { + let resource = resources.shift(); + if (resource) { + resolve(resource); + } else { + process.nextTick(tryCreate.bind(this, resolve, reject)); + } + }); + }, + destroy: function() { + return Promise.resolve(); + } + }; + const pool = createPool(factory, { max: 3, min: 3 }); + Promise.all([pool.acquire(), pool.acquire(), pool.acquire()]).then(all => { + all.forEach(resource => { + process.nextTick(pool.release.bind(pool), resource); + }); + }); + + t.equal(pool.pending, 3, "all acquisitions pending"); + + pool + .drain() + .then(() => pool.clear()) + .then(resolved => { + t.equal(resolved, undefined, "clear promise resolves with no value"); + t.end(); + }); + + process.nextTick(() => { + resources.push("a"); + resources.push("b"); + resources.push("c"); + }); +}); + tap.test("handle creation errors", function(t) { let created = 0; const resourceFactory = { @@ -568,13 +609,7 @@ tap.test( ); tap.test("returns only valid object to the pool", function(t) { - const pool = createPool({ - create: function() { - return Promise.resolve({ id: "validId" }); - }, - destroy: function(client) {}, - max: 1 - }); + const pool = createPool(new ResourceFactory(), { max: 1 }); pool .acquire() @@ -604,16 +639,7 @@ tap.test("returns only valid object to the pool", function(t) { }); tap.test("validate acquires object from the pool", function(t) { - const pool = createPool({ - create: function() { - return Promise.resolve({ id: "validId" }); - }, - validate: function(resource) { - return Promise.resolve(true); - }, - destroy: function(client) {}, - max: 1 - }); + const pool = createPool(new ResourceFactory(), { max: 1 }); pool .acquire() @@ -628,16 +654,7 @@ tap.test("validate acquires object from the pool", function(t) { }); tap.test("release to pool should work", function(t) { - const pool = createPool({ - create: function() { - return Promise.resolve({ id: "validId" }); - }, - validate: function(resource) { - return Promise.resolve(true); - }, - destroy: function(client) {}, - max: 1 - }); + const pool = createPool(new ResourceFactory(), { max: 1 }); pool .acquire() @@ -665,16 +682,7 @@ tap.test("release to pool should work", function(t) { tap.test( "isBorrowedResource should return true for borrowed resource", function(t) { - const pool = createPool({ - create: function() { - return Promise.resolve({ id: "validId" }); - }, - validate: function(resource) { - return Promise.resolve(true); - }, - destroy: function(client) {}, - max: 1 - }); + const pool = createPool(new ResourceFactory(), { max: 1 }); pool .acquire() @@ -691,16 +699,7 @@ tap.test( tap.test( "isBorrowedResource should return false for released resource", function(t) { - const pool = createPool({ - create: function() { - return Promise.resolve({ id: "validId" }); - }, - validate: function(resource) { - return Promise.resolve(true); - }, - destroy: function(client) {}, - max: 1 - }); + const pool = createPool(new ResourceFactory(), { max: 1 }); pool .acquire() @@ -718,16 +717,7 @@ tap.test( ); tap.test("destroy should redispense", function(t) { - const pool = createPool({ - create: function() { - return Promise.resolve({ id: "validId" }); - }, - validate: function(resource) { - return Promise.resolve(true); - }, - destroy: function(client) {}, - max: 1 - }); + const pool = createPool(new ResourceFactory(), { max: 1 }); pool .acquire() @@ -753,21 +743,10 @@ tap.test("destroy should redispense", function(t) { }); tap.test("evictor start with acquire when autostart is false", function(t) { - const pool = createPool( - { - create: function() { - return Promise.resolve({ id: "validId" }); - }, - validate: function(resource) { - return Promise.resolve(true); - }, - destroy: function(client) {} - }, - { - evictionRunIntervalMillis: 10000, - autostart: false - } - ); + const pool = createPool(new ResourceFactory(), { + evictionRunIntervalMillis: 10000, + autostart: false + }); t.equal(pool._scheduledEviction, null); @@ -781,3 +760,113 @@ tap.test("evictor start with acquire when autostart is false", function(t) { }) .catch(t.threw); }); + +tap.test("use method", function(t) { + const pool = createPool(new ResourceFactory()); + const result = pool.use(function(resource) { + t.equal(0, resource.id); + return Promise.resolve(); + }); + result.then(function() { + t.end(); + }); +}); + +tap.test("use method should resolve after fn promise is resolved", function(t) { + const pool = createPool(new ResourceFactory()); + let done_with_resource = false; + const result = pool.use(function(resource) { + return new Promise(function(resolve, reject) { + setImmediate(function() { + done_with_resource = true; + resolve("value"); + }); + }); + }); + result.then(val => { + t.equal(done_with_resource, true); + t.equal(val, "value"); + t.end(); + }); +}); + +tap.test("evictor should not run when softIdleTimeoutMillis is -1", function( + t +) { + const resourceFactory = new ResourceFactory(); + const pool = createPool(resourceFactory, { + evictionRunIntervalMillis: 10 + }); + pool + .acquire() + .then(res => pool.release(res)) + .then(() => { + return new Promise(res => setTimeout(res, 30)); + }) + .then(() => t.equal(resourceFactory.destroyed, 0)) + .then(() => { + utils.stopPool(pool); + t.end(); + }); +}); + +tap.test("should respect when maxWaitingClients is set to 0 ", function(t) { + let assertionCount = 0; + const resourceFactory = new ResourceFactory(); + const config = { + max: 2, + maxWaitingClients: 0 + }; + + const pool = createPool(resourceFactory, config); + + const borrowedResources = []; + + t.equal(pool.size, 0); + assertionCount += 1; + + pool + .acquire() + .then(function(obj) { + borrowedResources.push(obj); + t.equal(pool.size, 1); + assertionCount += 1; + }) + .then(function() { + return pool.acquire(); + }) + .then(function(obj) { + borrowedResources.push(obj); + t.equal(pool.size, 2); + assertionCount += 1; + }) + .then(function() { + return pool.acquire(); + }) + .then(function(obj) { + // should not go in here + t.equal(1, 2); + }) + .catch(error => { + t.equal(error.message, "max waitingClients count exceeded"); + t.end(); + }); +}); + +tap.test("should provide a way to wait until the pool is ready", function(t) { + const resourceFactory = new ResourceFactory(); + const config = { + min: 2, + max: 4 + }; + + const pool = createPool(resourceFactory, config); + + pool.ready().then(() => { + t.ok( + pool.available >= config.min, + "expected available resources to be at least as the minimum" + ); + t.end(); + }); +}); diff --git a/test/utils.js b/test/utils.js index 8cbc20c4..5b1e72f1 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,34 +1,39 @@ +"use strict"; + +const Pool = require("../lib/Pool"); + /** * Generic class for handling creation of resources * for testing */ -var ResourceFactory = function ResourceFactory() { - this.created = 0; - this.destroyed = 0; - this.bin = []; -}; +class ResourceFactory { + constructor() { + this.created = 0; + this.destroyed = 0; + this.bin = []; + } -ResourceFactory.prototype.create = function() { - var id = this.created++; - var resource = { - id: id - }; - return Promise.resolve(resource); -}; + create() { + return Promise.resolve({ id: this.created++ }); + } -ResourceFactory.prototype.destroy = function(resource) { - this.destroyed++; - this.bin.push(resource); - return Promise.resolve(); -}; + validate() { + return Promise.resolve(true); + } + destroy(resource) { + this.destroyed++; + this.bin.push(resource); + return Promise.resolve(); + } +} exports.ResourceFactory = ResourceFactory; /** * drains and terminates the pool * - * @param {[type]} pool [description] - * @return {[type]} [description] + * @param {Pool} pool [description] + * @return {Promise} [description] */ exports.stopPool = function(pool) { return pool.drain().then(function() { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..8ea6901e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "lib": ["es6", "dom"], + "outDir": "dist/", + "allowJs": true, + "checkJs": true, + "declaration": true, + "noEmit": true, + "strict": false + } +} \ No newline at end of file