lightning-pool
Version:
Fastest generic Pool written with TypeScript
471 lines (470 loc) • 16.1 kB
JavaScript
import DoublyLinked from 'doublylinked';
import { EventEmitter } from 'events';
import promisify from 'putil-promisify';
import { AbortError } from './abort-error.js';
import { PoolState, ResourceState } from './constants.js';
import { PoolOptions } from './pool-options.js';
import { PoolRequest } from './pool-request.js';
import { ResourceItem } from './resource-item.js';
export class Pool extends EventEmitter {
constructor(factory, config) {
super();
this._requestQueue = new DoublyLinked();
this._allResources = new Map();
this._acquiredResources = new DoublyLinked();
this._idleResources = new DoublyLinked();
this._creating = 0;
this._requestsProcessing = 0;
this._state = PoolState.IDLE;
if (typeof factory !== 'object') {
throw new TypeError('You must provide `factory` object');
}
if (typeof factory.create !== 'function') {
throw new TypeError('factory.create must be a function');
}
if (typeof factory.destroy !== 'function') {
throw new TypeError('factory.destroy must be a function');
}
if (factory.validate && typeof factory.validate !== 'function') {
throw new TypeError('factory.validate can be a function');
}
if (factory.reset && typeof factory.reset !== 'function') {
throw new TypeError('factory.reset can be a function');
}
const opts = (this._options = new PoolOptions(this));
if (config)
this.options.assign(config);
opts.on('change', (prop, val) => {
if (prop === 'houseKeepInterval')
this._setHouseKeep(val);
if (prop === 'min' || prop === 'minIdle')
this._ensureMin();
});
this._factory = factory;
}
/**
* Returns Pool options
*/
get options() {
return this._options;
}
/**
* Returns number of resources that are currently acquired
*/
get acquired() {
return this._acquiredResources.length;
}
/**
* Returns number of unused resources in the pool
*/
get available() {
return this._idleResources.length;
}
/**
* Returns number of resources currently creating
*/
get creating() {
return this._creating;
}
/**
* Returns number of callers waiting to acquire a resource
*/
get pending() {
return this._requestQueue.length + this._requestsProcessing;
}
/**
* Returns number of resources in the pool
* regardless of whether they are idle or in use
*/
get size() {
return this._allResources.size;
}
/**
* Returns state of the pool
*/
get state() {
return this._state;
}
/**
* Starts the pool and begins creating of resources, starts house keeping and any other internal logic.
* Note: This method is not need to be called. Pool instance will automatically be started when acquire() method is called
*/
start() {
if (this._state === PoolState.STARTED)
return;
if (this._state >= PoolState.CLOSING) {
throw new Error('Closed pool can not be started again');
}
this._state = PoolState.STARTED;
this._setHouseKeep(this.options.houseKeepInterval);
this._ensureMin();
this.emit('start');
}
close(arg0, arg1) {
let terminateWait = Infinity;
let callback;
if (typeof arg0 === 'function')
callback = arg0;
else {
terminateWait = typeof arg0 === 'number' ? arg0 : arg0 ? 0 : Infinity;
callback = arg1;
}
if (!callback) {
return promisify.fromCallback(cb => this.close(terminateWait, cb));
}
if (this._state === PoolState.CLOSED || this._state === PoolState.IDLE) {
return callback();
}
if (this._state === PoolState.CLOSING) {
this.once('close', callback);
return;
}
this.emit('closing');
if (this._houseKeepTimer)
clearTimeout(this._houseKeepTimer);
this._state = PoolState.CLOSING;
this._requestQueue.forEach(t => t.stopTimout());
this._requestQueue = new DoublyLinked();
this._requestsProcessing = 0;
if (terminateWait <= 0) {
this._acquiredResources.forEach(t => this.destroy(t.resource));
}
else {
const startTime = Date.now();
this._closeWaitTimer = setInterval(() => {
if (!this._allResources.size) {
clearInterval(this._closeWaitTimer);
this._closeWaitTimer = undefined;
return;
}
if (Date.now() > startTime + terminateWait) {
clearInterval(this._closeWaitTimer);
this._closeWaitTimer = undefined;
this._acquiredResources.forEach(t => this.release(t.resource));
this.emit('terminate');
}
}, 50);
}
this._setHouseKeep(5);
this.once('close', callback);
}
closeAsync(arg0) {
return promisify.fromCallback(cb => this.close(arg0, cb));
}
acquire(callback) {
if (!callback)
return promisify.fromCallback(cb => this.acquire(cb));
try {
this.start();
}
catch (e) {
return callback(e);
}
if (this.options.maxQueue && this.pending >= this.options.maxQueue) {
return callback(new Error('Pool queue is full'));
}
this._requestQueue.push(new PoolRequest(this, callback));
this._processNextRequest();
}
/**
* Releases an allocated `resource` and let it back to pool.
*/
release(resource, callback) {
const item = this._allResources.get(resource);
if (item && item.state !== ResourceState.IDLE) {
this._itemSetIdle(item, callback);
}
this._processNextRequest();
}
/**
* Async version of release().
*/
releaseAsync(resource) {
return promisify.fromCallback(cb => this.release(resource, cb));
}
/**
* Releases, destroys and removes any `resource` from `Pool`.
*/
destroy(resource, callback) {
try {
const item = this._allResources.get(resource);
if (item)
this._itemDestroy(item, callback);
else if (callback)
callback();
}
finally {
this._processNextRequest();
}
}
/**
* Async version of destroy().
*/
destroyAsync(resource) {
return promisify.fromCallback(cb => this.destroy(resource, cb));
}
/**
* Returns if a `resource` has been acquired from the pool and not yet released or destroyed.
*/
isAcquired(resource) {
const item = this._allResources.get(resource);
return !!(item && item.acquiredNode);
}
/**
* Returns if the pool contains a `resource`
*/
includes(resource) {
return this._allResources.has(resource);
}
_processNextRequest() {
if (this._state !== PoolState.STARTED ||
this._requestsProcessing >= this.options.max - this.acquired) {
return;
}
const request = this._requestQueue.shift();
if (!request)
return;
this._requestsProcessing++;
const handleCallback = (err, item) => {
this._requestsProcessing--;
request.stopTimout();
try {
if (item) {
/* istanbul ignore next : Hard to simulate */
if (this._state !== PoolState.STARTED) {
this._itemDestroy(item);
return;
}
this._itemSetAcquired(item);
this._ensureMin();
request.callback(undefined, item.resource);
this.emit('acquire', item.resource);
}
else
request.callback(err);
}
catch {
// ignored
}
this._processNextRequest();
};
const item = this._idleResources.shift();
if (item) {
/* Validate resource */
if (this.options.validation && this._factory.validate) {
this._itemValidate(item, (err, result) => {
/* Destroy resource on validation error */
if (err || result === false) {
this._itemDestroy(item);
this.emit('validate-error', err, item.resource);
this._requestsProcessing--;
this._requestQueue.unshift(request);
this._processNextRequest();
}
else
handleCallback(undefined, item);
});
return;
}
return handleCallback(undefined, item);
}
/** There is no idle resource. We need to create new one **/
this._createResource(request, handleCallback);
}
emit(event, ...args) {
// Prevents errors while calling emit()
try {
return super.emit(event, ...args);
}
catch {
return true;
}
}
/**
* Creates new resource
*/
_createResource(request, callback) {
const maxRetries = this.options.acquireMaxRetries;
let tries = 0;
this._creating++;
const handleCallback = (err, obj) => {
if (request && request.timedOut)
return;
if (err || !obj) {
tries++;
this.emit('error', err, {
requestTime: request ? request.created : Date.now(),
tries,
maxRetries: this.options.acquireMaxRetries,
});
if (err instanceof AbortError || tries >= maxRetries) {
this._creating--;
return callback && callback(err);
}
return setTimeout(() => tryCreate(), this.options.acquireRetryWait);
}
this._creating--;
if (this._allResources.has(obj)) {
return (callback &&
callback(new Error('Factory error. Resource already in pool')));
}
const item = new ResourceItem(obj);
this._itemSetIdle(item);
this._allResources.set(obj, item);
if (callback)
callback(undefined, item);
this.emit('create', obj);
};
const tryCreate = () => {
try {
const o = this._factory.create({ tries, maxRetries });
/* istanbul ignore next */
if (!o) {
return handleCallback(new AbortError('Factory returned no resource'));
}
promisify.await(o, handleCallback);
}
catch (e) {
handleCallback(e);
}
};
tryCreate();
}
_setHouseKeep(ms) {
if (this._houseKeepTimer)
clearInterval(this._houseKeepTimer);
this._houseKeepTimer = undefined;
if ((ms > 0 && this.state === PoolState.STARTED) ||
this.state === PoolState.CLOSING) {
this._houseKeepTimer = setInterval(() => this._houseKeep(), ms);
}
}
_houseKeep() {
const isClosing = this._state === PoolState.CLOSING;
const now = Date.now();
let m = this._allResources.size - this.options.min;
let n = this._idleResources.length - this.options.minIdle;
if (isClosing || (m > 0 && n > 0)) {
this._idleResources.every((item) => {
if (isClosing || item.idleTime + this.options.idleTimeoutMillis < now) {
this._itemDestroy(item);
return isClosing || !!(--n && --m);
}
return false;
});
}
if (isClosing) {
/* Check again 5 ms later */
if (this._allResources.size)
return;
clearInterval(this._houseKeepTimer);
this._state = PoolState.CLOSED;
this._requestsProcessing = 0;
this.emit('close');
}
}
_ensureMin() {
process.nextTick(() => {
let k = Math.max(this.options.min - this._allResources.size, this.options.minIdle - this._idleResources.length) - this.creating;
while (k-- > 0)
this._createResource();
});
}
_itemSetAcquired(item) {
if (item.state !== ResourceState.ACQUIRED) {
this._itemDetach(item);
item.state = ResourceState.ACQUIRED;
this._acquiredResources.push(item);
item.acquiredNode = this._acquiredResources.tail;
}
}
_itemDetach(item) {
switch (item.state) {
case ResourceState.IDLE:
item.idleTime = 0;
/* istanbul ignore next*/
if (item.idleNode)
item.idleNode.remove();
item.idleNode = undefined;
break;
case ResourceState.ACQUIRED:
case ResourceState.VALIDATION:
/* istanbul ignore next*/
if (item.acquiredNode)
item.acquiredNode.remove();
item.acquiredNode = undefined;
break;
default:
break;
}
}
_itemSetIdle(item, callback) {
const isAcquired = item.state === ResourceState.ACQUIRED;
const handleCallback = (err) => {
if (err)
return this._itemDestroy(item, callback);
this._itemDetach(item);
item.idleTime = Date.now();
item.state = ResourceState.IDLE;
if (this.options.fifo) {
this._idleResources.push(item);
item.idleNode = this._idleResources.tail;
}
else {
this._idleResources.unshift(item);
item.idleNode = this._idleResources.head;
}
if (isAcquired)
this.emit('return', item.resource);
if (callback)
callback();
// noinspection JSAccessibilityCheck
this._processNextRequest();
};
if (isAcquired && this._factory.reset) {
try {
const o = this._factory.reset(item.resource);
promisify.await(o, handleCallback);
}
catch (e) {
handleCallback(e);
}
}
else
handleCallback();
}
_itemDestroy(item, callback) {
this._itemDetach(item);
const handleCallback = (err) => {
item.destroyed = true;
this._allResources.delete(item.resource);
if (err)
this.emit('destroy-error', err, item.resource);
else
this.emit('destroy', item.resource);
if (callback)
callback(err);
};
try {
const o = this._factory.destroy(item.resource);
promisify.await(o, handleCallback);
}
catch (e) {
handleCallback(e);
}
finally {
this._processNextRequest();
}
}
_itemValidate(item, callback) {
item.state = ResourceState.VALIDATION;
try {
const o = this._factory.validate?.(item.resource);
// @ts-ignore
promisify.await(o, callback);
}
catch (e) {
callback?.(e);
}
}
}