591 lines
16 KiB
JavaScript
591 lines
16 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const EventEmitter = require('events');
|
||
|
const util = require('util');
|
||
|
const Queue = require('denque');
|
||
|
const Errors = require('./misc/errors');
|
||
|
const Utils = require('./misc/utils');
|
||
|
|
||
|
function PoolBase(options, processTask, createConnectionPool, pingPromise) {
|
||
|
//*****************************************************************
|
||
|
// public methods
|
||
|
//*****************************************************************
|
||
|
|
||
|
/**
|
||
|
* Retrieve a connection from pool.
|
||
|
* Create a new one, if limit is not reached.
|
||
|
* wait until acquireTimeout.
|
||
|
*
|
||
|
* @return {Promise}
|
||
|
*/
|
||
|
this.getConnection = function () {
|
||
|
return addRequest(this);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Execute a query on one connection from pool.
|
||
|
*
|
||
|
* @param sql sql command
|
||
|
* @param value parameter value of sql command (not mandatory)
|
||
|
* @return {Promise}
|
||
|
*/
|
||
|
this.query = function (sql, value) {
|
||
|
return addRequest(this, sql, value, false);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Execute a batch on one connection from pool.
|
||
|
*
|
||
|
* @param sql sql command
|
||
|
* @param value parameter value of sql command (not mandatory)
|
||
|
* @return {Promise}
|
||
|
*/
|
||
|
this.batch = function (sql, value) {
|
||
|
return addRequest(this, sql, value, true);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Close all connection in pool
|
||
|
*
|
||
|
* @return Promise
|
||
|
*/
|
||
|
this.end = function () {
|
||
|
if (closed) {
|
||
|
return Promise.reject(
|
||
|
Errors.createError(
|
||
|
'pool is already closed',
|
||
|
false,
|
||
|
null,
|
||
|
'HY000',
|
||
|
Errors.ER_POOL_ALREADY_CLOSED,
|
||
|
undefined,
|
||
|
false
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
closed = true;
|
||
|
clearInterval(idleMaintainingTask);
|
||
|
|
||
|
//close unused connections
|
||
|
const idleConnectionsEndings = [];
|
||
|
let conn;
|
||
|
while ((conn = idleConnections.shift())) {
|
||
|
idleConnectionsEndings.push(conn.forceEnd());
|
||
|
}
|
||
|
|
||
|
firstTaskTimeout = clearTimeout(firstTaskTimeout);
|
||
|
|
||
|
//reject all waiting task
|
||
|
if (taskQueue.size() > 0) {
|
||
|
let task;
|
||
|
const err = Errors.createError(
|
||
|
'pool is ending, connection request aborted',
|
||
|
false,
|
||
|
null,
|
||
|
'HY000',
|
||
|
Errors.ER_CLOSING_POOL,
|
||
|
undefined,
|
||
|
false
|
||
|
);
|
||
|
while ((task = taskQueue.shift())) {
|
||
|
process.nextTick(task.reject, err);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Promise.all(idleConnectionsEndings);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get current active connections.
|
||
|
* @return {number}
|
||
|
*/
|
||
|
this.activeConnections = function () {
|
||
|
return Object.keys(activeConnections).length;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get current total connection number.
|
||
|
* @return {number}
|
||
|
*/
|
||
|
this.totalConnections = function () {
|
||
|
return this.activeConnections() + this.idleConnections();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get current idle connection number.
|
||
|
* @return {number}
|
||
|
*/
|
||
|
this.idleConnections = function () {
|
||
|
return idleConnections.size();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get current stacked connection request.
|
||
|
* @return {number}
|
||
|
*/
|
||
|
this.taskQueueSize = function () {
|
||
|
return taskQueue.size();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* First connection creation.
|
||
|
* activation is slightly different than pooling grow : If connection fails, there is many retries for 30s
|
||
|
* (option initializationTimeout).
|
||
|
* If connection fails, error will be thrown to request / console if no request, to ensure that error is thrown.
|
||
|
*/
|
||
|
this.initialize = function () {
|
||
|
connectionInCreation = true;
|
||
|
const self = this;
|
||
|
const timeoutEnd = Date.now() + opts.initializationTimeout;
|
||
|
connectionCreationLoop(self, 0, timeoutEnd)
|
||
|
.then((conn) => {
|
||
|
//add to pool
|
||
|
if (closed) {
|
||
|
conn.forceEnd().catch((err) => {});
|
||
|
} else {
|
||
|
addPoolConnection(self, conn);
|
||
|
if (opts.idleTimeout > 0) {
|
||
|
idleMaintainingTask = setInterval(idleMaintainer, 500, self);
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
.catch((err) => {
|
||
|
connectionInCreation = false;
|
||
|
const task = taskQueue.shift();
|
||
|
if (task) {
|
||
|
firstTaskTimeout = clearTimeout(firstTaskTimeout);
|
||
|
process.nextTick(task.reject, err);
|
||
|
resetTimeoutToNextTask();
|
||
|
} else if (!closed) {
|
||
|
console.error(err);
|
||
|
}
|
||
|
})
|
||
|
.finally(() => {
|
||
|
ensurePoolSize(self);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
this.escape = (value) => {
|
||
|
return Utils.escape(options.connOptions, searchInfo(), value);
|
||
|
};
|
||
|
|
||
|
this.escapeId = (value) => {
|
||
|
return Utils.escapeId(options.connOptions, searchInfo(), value);
|
||
|
};
|
||
|
|
||
|
//*****************************************************************
|
||
|
// internal methods
|
||
|
//*****************************************************************
|
||
|
|
||
|
/**
|
||
|
* Search info object of an existing connection. to know server type and version.
|
||
|
* @returns information object if connection available.
|
||
|
*/
|
||
|
const searchInfo = () => {
|
||
|
let info = null;
|
||
|
let conn = idleConnections.get(0);
|
||
|
|
||
|
if (conn == null) {
|
||
|
conn = Object.keys(activeConnections)[0];
|
||
|
}
|
||
|
|
||
|
if (conn != null) {
|
||
|
info = conn.info;
|
||
|
}
|
||
|
return info;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get a connection from pool / execute query
|
||
|
*
|
||
|
* @param pool current pool
|
||
|
* @param sql sql value (not mandatory)
|
||
|
* @param values sql parameter (not mandatory)
|
||
|
* @param isBatch is batch request
|
||
|
* @return {*}
|
||
|
*/
|
||
|
const addRequest = function (pool, sql, values, isBatch) {
|
||
|
if (closed) {
|
||
|
return Promise.reject(
|
||
|
Errors.createError(
|
||
|
'pool is closed',
|
||
|
false,
|
||
|
null,
|
||
|
'HY000',
|
||
|
Errors.ER_POOL_ALREADY_CLOSED,
|
||
|
undefined,
|
||
|
false
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return getIdleValidConnection(pool).then(
|
||
|
(conn) => {
|
||
|
pool.emit('acquire', conn);
|
||
|
return processTask(conn, sql, values, isBatch);
|
||
|
},
|
||
|
() => {
|
||
|
process.nextTick(() => pool.emit('enqueue'));
|
||
|
|
||
|
//no idle connection available
|
||
|
//create a new connection if limit is not reached
|
||
|
ensurePoolSize(pool);
|
||
|
|
||
|
//connections are all used, stack demand.
|
||
|
return new Promise((resolve, reject) => {
|
||
|
const task = {
|
||
|
timeout: Date.now() + opts.acquireTimeout,
|
||
|
reject: reject,
|
||
|
resolve: resolve,
|
||
|
sql: sql,
|
||
|
values: values,
|
||
|
isBatch: isBatch
|
||
|
};
|
||
|
if (!firstTaskTimeout) {
|
||
|
firstTaskTimeout = setTimeout(rejectAndResetTimeout, opts.acquireTimeout, task);
|
||
|
}
|
||
|
taskQueue.push(task);
|
||
|
});
|
||
|
}
|
||
|
);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Return an idle Connection.
|
||
|
* If connection has not been used for some time ( minDelayValidation), validate connection status.
|
||
|
*
|
||
|
* @param pool pool
|
||
|
* @returns {Promise<Connection|null>)} connection of null of no valid idle connection.
|
||
|
*/
|
||
|
const getIdleValidConnection = function (pool) {
|
||
|
if (idleConnections.isEmpty()) {
|
||
|
return Promise.reject(null);
|
||
|
}
|
||
|
const conn = idleConnections.shift();
|
||
|
activeConnections[conn.threadId] = conn;
|
||
|
if (opts.minDelayValidation <= 0 || Date.now() - conn.lastUse > opts.minDelayValidation) {
|
||
|
return pingPromise(conn)
|
||
|
.then(() => {
|
||
|
initLeakProcess(conn);
|
||
|
return Promise.resolve(conn);
|
||
|
})
|
||
|
.catch((err) => {
|
||
|
delete activeConnections[conn.threadId];
|
||
|
pool.emit('_remove-conn');
|
||
|
return getIdleValidConnection(pool);
|
||
|
});
|
||
|
} else {
|
||
|
//just check connection state
|
||
|
if (conn.isValid()) {
|
||
|
initLeakProcess(conn);
|
||
|
return Promise.resolve(conn);
|
||
|
} else {
|
||
|
delete activeConnections[conn.threadId];
|
||
|
pool.emit('_remove-conn');
|
||
|
return getIdleValidConnection(pool);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Task request timeout handler
|
||
|
* @param task
|
||
|
*/
|
||
|
const timeoutTask = (task) => {
|
||
|
firstTaskTimeout = null;
|
||
|
if (task === taskQueue.peekFront()) {
|
||
|
taskQueue.shift();
|
||
|
process.nextTick(
|
||
|
task.reject,
|
||
|
Errors.createError(
|
||
|
'retrieve connection from pool timeout after ' +
|
||
|
Math.abs(Date.now() - (task.timeout - opts.acquireTimeout)) +
|
||
|
'ms',
|
||
|
false,
|
||
|
null,
|
||
|
'HY000',
|
||
|
Errors.ER_GET_CONNECTION_TIMEOUT,
|
||
|
undefined,
|
||
|
false
|
||
|
)
|
||
|
);
|
||
|
} else {
|
||
|
throw new Error('Rejection by timeout without task !!!');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reject task, and reset timeout to next waiting task if any.
|
||
|
* @param task
|
||
|
*/
|
||
|
const rejectAndResetTimeout = (task) => {
|
||
|
timeoutTask(task);
|
||
|
resetTimeoutToNextTask();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Loop for connection creation.
|
||
|
* This permits to wait before next try after a connection fail.
|
||
|
*
|
||
|
* @param pool current pool
|
||
|
* @param iteration current iteration
|
||
|
* @param timeoutEnd ending timeout
|
||
|
* @returns {Promise<any>} Connection if found, error if not
|
||
|
*/
|
||
|
const connectionCreationLoop = function (pool, iteration, timeoutEnd) {
|
||
|
return new Promise(function (resolve, reject) {
|
||
|
const creationTryout = function (resolve, reject) {
|
||
|
if (closed) {
|
||
|
reject(
|
||
|
Errors.createError(
|
||
|
'Cannot create new connection to pool, pool closed',
|
||
|
true,
|
||
|
null,
|
||
|
'08S01',
|
||
|
Errors.ER_ADD_CONNECTION_CLOSED_POOL,
|
||
|
null
|
||
|
)
|
||
|
);
|
||
|
return;
|
||
|
}
|
||
|
iteration++;
|
||
|
createConnectionPool(pool)
|
||
|
.then((conn) => {
|
||
|
resolve(conn);
|
||
|
})
|
||
|
.catch((err) => {
|
||
|
//if timeout is reached or authentication fail return error
|
||
|
if (
|
||
|
closed ||
|
||
|
(err.errno && (err.errno === 1524 || err.errno === 1045 || err.errno === 1698)) ||
|
||
|
timeoutEnd < Date.now()
|
||
|
) {
|
||
|
reject(err);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
setTimeout(creationTryout.bind(null, resolve, reject), 500);
|
||
|
});
|
||
|
};
|
||
|
//initial without timeout
|
||
|
creationTryout(resolve, reject);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const addPoolConnection = function (pool, conn) {
|
||
|
conn.lastUse = Date.now();
|
||
|
const initialDestroyFct = conn.destroy;
|
||
|
conn.destroy = () => {
|
||
|
removeLeakProcess(conn);
|
||
|
delete activeConnections[conn.threadId];
|
||
|
initialDestroyFct();
|
||
|
pool.emit('_remove-conn');
|
||
|
};
|
||
|
|
||
|
//Connection error
|
||
|
// -> evict connection from pool
|
||
|
conn.on('error', (err) => {
|
||
|
let idx = 0;
|
||
|
let currConn;
|
||
|
removeLeakProcess(conn);
|
||
|
delete activeConnections[conn.threadId];
|
||
|
while ((currConn = idleConnections.peekAt(idx))) {
|
||
|
if (currConn === conn) {
|
||
|
idleConnections.removeOne(idx);
|
||
|
break;
|
||
|
} else {
|
||
|
//since connection did have an error, other waiting connection might too
|
||
|
//forcing validation when borrowed next time, even if "minDelayValidation" is not reached.
|
||
|
currConn.lastUse = Math.min(Date.now() - opts.minDelayValidation, currConn.lastUse);
|
||
|
}
|
||
|
idx++;
|
||
|
}
|
||
|
pool.emit('_remove-conn');
|
||
|
});
|
||
|
connectionInCreation = false;
|
||
|
idleConnections.push(conn);
|
||
|
pool.emit('_idle-conn');
|
||
|
process.nextTick(() => pool.emit('connection', conn));
|
||
|
};
|
||
|
|
||
|
this._releaseConnection = function (conn) {
|
||
|
removeLeakProcess(conn);
|
||
|
conn.lastUse = Date.now();
|
||
|
delete activeConnections[conn.threadId];
|
||
|
const pool = this;
|
||
|
if (closed) {
|
||
|
return conn.forceEnd().catch(() => {
|
||
|
return Promise.resolve();
|
||
|
});
|
||
|
} else if (conn.isValid()) {
|
||
|
pool.emit('release', conn);
|
||
|
idleConnections.push(conn);
|
||
|
process.nextTick(() => pool.emit('_idle-conn'));
|
||
|
} else {
|
||
|
ensurePoolSize(pool);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Grow pool connections until reaching connection limit.
|
||
|
*/
|
||
|
const ensurePoolSize = function (pool) {
|
||
|
if (
|
||
|
!connectionInCreation &&
|
||
|
pool.idleConnections() < opts.minimumIdle &&
|
||
|
pool.totalConnections() < opts.connectionLimit &&
|
||
|
!closed
|
||
|
) {
|
||
|
connectionInCreation = true;
|
||
|
process.nextTick(() => {
|
||
|
const timeoutEnd = Date.now() + opts.initializationTimeout;
|
||
|
if (!closed) {
|
||
|
connectionCreationLoop(pool, 0, timeoutEnd)
|
||
|
.then((conn) => {
|
||
|
if (closed) {
|
||
|
return conn.forceEnd().catch((err) => {});
|
||
|
}
|
||
|
addPoolConnection(pool, conn);
|
||
|
})
|
||
|
.catch((err) => {
|
||
|
if (pool.totalConnections() === 0) {
|
||
|
const task = taskQueue.shift();
|
||
|
if (task) {
|
||
|
firstTaskTimeout = clearTimeout(firstTaskTimeout);
|
||
|
process.nextTick(task.reject, err);
|
||
|
resetTimeoutToNextTask();
|
||
|
}
|
||
|
} else if (!closed) {
|
||
|
console.error(`pool fail to create connection (${err.message})`);
|
||
|
}
|
||
|
|
||
|
//delay next try
|
||
|
setTimeout(() => {
|
||
|
connectionInCreation = false;
|
||
|
if (taskQueue.size() > 0) {
|
||
|
ensurePoolSize(pool);
|
||
|
}
|
||
|
}, 500);
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const resetTimeoutToNextTask = () => {
|
||
|
//handle next Timer
|
||
|
const currTime = Date.now();
|
||
|
let nextTask;
|
||
|
while ((nextTask = taskQueue.peekFront())) {
|
||
|
if (nextTask.timeout < currTime) {
|
||
|
timeoutTask(nextTask);
|
||
|
} else {
|
||
|
firstTaskTimeout = setTimeout(rejectAndResetTimeout, nextTask.timeout - currTime, nextTask);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Permit to remove idle connection if unused for some time.
|
||
|
* @param pool current pool
|
||
|
*/
|
||
|
const idleMaintainer = function (pool) {
|
||
|
let toRemove = Math.max(1, pool.idleConnections() - opts.minimumIdle);
|
||
|
while (toRemove > 0) {
|
||
|
const conn = idleConnections.peek();
|
||
|
--toRemove;
|
||
|
if (conn && conn.lastUse + opts.idleTimeout * 1000 < Date.now()) {
|
||
|
idleConnections.shift();
|
||
|
conn.forceEnd().catch((err) => {});
|
||
|
continue;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
ensurePoolSize(pool);
|
||
|
};
|
||
|
|
||
|
this._discardConnection = (conn) => {
|
||
|
removeLeakProcess(conn);
|
||
|
delete activeConnections[conn.threadId];
|
||
|
conn.forceEnd().catch((err) => {});
|
||
|
this.emit('_remove-conn');
|
||
|
};
|
||
|
|
||
|
const logLeak = (conn) => {
|
||
|
console.log(
|
||
|
'Possible connection leak on thread ' +
|
||
|
conn.info.threadId +
|
||
|
' (connection not returned to pool since ' +
|
||
|
(Date.now() - conn.lastUse) +
|
||
|
'ms. Did connection.released() been implemented'
|
||
|
);
|
||
|
conn.leaked = true;
|
||
|
};
|
||
|
|
||
|
const _initLeakProcess = (conn) => {
|
||
|
conn.lastUse = Date.now();
|
||
|
conn.leaked = false;
|
||
|
conn.leakProcess = setTimeout(logLeak, opts.leakDetectionTimeout, conn);
|
||
|
};
|
||
|
|
||
|
const _removeLeakProcess = (conn) => {
|
||
|
conn.leakProcess = clearTimeout(conn.leakProcess);
|
||
|
if (conn.leaked) {
|
||
|
console.log(
|
||
|
'Previous possible leak connection with thread ' +
|
||
|
conn.info.threadId +
|
||
|
' was returned to pool'
|
||
|
);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Launch next waiting task request if available connections.
|
||
|
*/
|
||
|
const handleTaskQueue = function () {
|
||
|
firstTaskTimeout = clearTimeout(firstTaskTimeout);
|
||
|
const task = taskQueue.shift();
|
||
|
|
||
|
if (task) {
|
||
|
const conn = idleConnections.shift();
|
||
|
if (conn) {
|
||
|
initLeakProcess(conn);
|
||
|
this.emit('acquire', conn);
|
||
|
activeConnections[conn.threadId] = conn;
|
||
|
resetTimeoutToNextTask();
|
||
|
processTask(conn, task.sql, task.values, task.isBatch)
|
||
|
.then(task.resolve)
|
||
|
.catch(task.reject);
|
||
|
} else {
|
||
|
taskQueue.unshift(task);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const opts = options;
|
||
|
let closed = false;
|
||
|
let connectionInCreation = false;
|
||
|
const initLeakProcess = opts.leakDetectionTimeout > 0 ? _initLeakProcess : () => {};
|
||
|
const removeLeakProcess = opts.leakDetectionTimeout > 0 ? _removeLeakProcess : () => {};
|
||
|
const idleConnections = new Queue();
|
||
|
const activeConnections = {};
|
||
|
const taskQueue = new Queue();
|
||
|
let idleMaintainingTask;
|
||
|
let firstTaskTimeout;
|
||
|
Object.defineProperty(this, 'closed', {
|
||
|
get() {
|
||
|
return closed;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
EventEmitter.call(this);
|
||
|
|
||
|
this.on('_idle-conn', handleTaskQueue.bind(this));
|
||
|
this.on('_remove-conn', ensurePoolSize.bind(this, this));
|
||
|
this.on('connection', ensurePoolSize.bind(this, this));
|
||
|
}
|
||
|
|
||
|
util.inherits(PoolBase, EventEmitter);
|
||
|
module.exports = PoolBase;
|