@rtinternal/oracledb
Version:
[Fork of oracle/node-oracledb ] Oracle Database driver for Node.js maintained by Oracle Corp.
570 lines (486 loc) • 18.7 kB
JavaScript
// Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved
//-----------------------------------------------------------------------------
//
// You may not use the identified files except in compliance with the Apache
// License, Version 2.0 (the "License.")
//
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
//-----------------------------------------------------------------------------
'use strict';
const EventEmitter = require('events');
const nodbUtil = require('./util.js');
const util = require('util');
const PoolStatistics = require('./poolStatistics.js');
//-----------------------------------------------------------------------------
// _checkRequestQueue()
// When a connection is returned to the pool, this method is called (via an
// event handler) to determine when requests for connections should be
// resumed and cancels any timeout that may have been associated with the
// request. This method is also called from reconfigure() so that waiting
// connection requests can be processed. Note the use of a local variable for
// the number of connections out. This is because the connection requests will
// not resume until after the loop is finished, and therefore the number of
// connections the pool thinks is out will not be incremented.
//-----------------------------------------------------------------------------
function _checkRequestQueue() {
let connectionsOut = this._connectionsOut;
while (this._connRequestQueue.length > 0 && connectionsOut < this.poolMax) {
connectionsOut += 1;
const payload = this._connRequestQueue.shift();
if (this._enableStatistics) {
this._totalRequestsDequeued += 1;
this._updateWaitStatistics(payload);
}
if (payload.timeoutHandle) {
clearTimeout(payload.timeoutHandle);
}
// inform the waiter that processing can continue
payload.resolve();
}
}
//-----------------------------------------------------------------------------
// getConnection()
// Gets a connection from the pool and returns it to the caller. If there are
// fewer connections out than the poolMax setting, then the request will
// return immediately; otherwise, the request will be queued for up to
// queueTimeout milliseconds.
//-----------------------------------------------------------------------------
async function getConnection(a1) {
let poolMax;
let options = {};
// check arguments
nodbUtil.checkArgCount(arguments, 0, 1);
if (arguments.length == 1) {
nodbUtil.assert(nodbUtil.isObject(a1), 'NJS-005', 1);
options = a1;
}
// if pool is draining/closed, throw an appropriate error
this._checkPoolOpen(true);
// manage stats, if applicable
if (this._enableStatistics) {
this._totalConnectionRequests += 1;
}
// getting the poolMax setting on the pool may fail if the pool is no longer
// valid
try {
poolMax = this.poolMax;
} catch (err) {
if (this._enableStatistics) {
this._totalFailedRequests += 1;
}
throw err;
}
if (this._connectionsOut >= poolMax ||
this.status === this._oracledb.POOL_STATUS_RECONFIGURING) {
// when the queue is huge, throw error early without waiting for queue timeout
if (this._connRequestQueue.length >= this._queueMax &&
this._queueMax >= 0) {
if (this._enableStatistics) {
this._totalRequestsRejected += 1;
}
throw new Error(nodbUtil.getErrorMessage('NJS-076', this._queueMax));
}
// if too many connections are out, wait until room is made available or the
// queue timeout expires
await new Promise((resolve, reject) => {
// set up a payload which will be added to the queue for processing
const payload = { resolve: resolve, reject: reject };
// if using a queue timeout, establish the timeout so that when it
// expires the payload will be removed from the queue and an exception
// thrown
if (this._queueTimeout !== 0) {
payload.timeoutHandle = setTimeout(() => {
const ix = this._connRequestQueue.indexOf(payload);
if (ix >= 0) {
this._connRequestQueue.splice(ix, 1);
}
if (this._enableStatistics) {
this._totalRequestTimeouts += 1;
this._updateWaitStatistics(payload);
}
reject(new Error(nodbUtil.getErrorMessage('NJS-040',
this._queueTimeout)));
}, this._queueTimeout);
}
// add payload to the queue
this._connRequestQueue.push(payload);
if (this._enableStatistics) {
payload.enqueuedTime = Date.now();
this._totalRequestsEnqueued += 1;
this._maximumQueueLength = Math.max(this._maximumQueueLength,
this._connRequestQueue.length);
}
});
// check if pool is draining/closed after delay has
// completed and throw an appropriate error
this._checkPoolOpen(true);
}
// room is available in the queue, so proceed to acquire a connection from
// the pool; adjust the connections out immediately in order to ensure that
// another attempt doesn't proceed while this one is underway
this._connectionsOut += 1;
try {
// acquire connection from the pool
const conn = await this._getConnection(options);
// invoke tag fixup callback method if one has been specified and the
// actual tag on the connection doesn't match the one requested, or the
// connection is freshly created; if the callback fails, close the
// connection and remove it from the pool
const requestedTag = options.tag || "";
if (typeof this.sessionCallback === 'function' &&
(conn._newSession || conn.tag != requestedTag)) {
try {
await new Promise((resolve, reject) => {
this.sessionCallback(conn, requestedTag, function(err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
} catch (err) {
await conn.close({ drop: true });
throw err;
}
}
// when connection is closed, check to see if another request should be
// processed and update any stats, as needed
conn.on('_afterConnClose', () => {
this._connectionsOut -= 1;
this.emit('_checkRequestQueue');
if (this._connectionsOut == 0) {
this.emit('_allCheckedIn');
}
});
return (conn);
} catch (err) {
this._connectionsOut -= 1;
if (this._enableStatistics) {
this._totalFailedRequests += 1;
}
this.emit('_checkRequestQueue');
throw err;
}
}
//-----------------------------------------------------------------------------
// reconfigure()
// Reconfigure the pool, change the value for given pool-properties.
//-----------------------------------------------------------------------------
async function reconfigure(options) {
// check arguments
nodbUtil.checkArgCount(arguments, 1, 1);
nodbUtil.assert(nodbUtil.isObject(options));
// reconfiguration can happen only when status is OPEN
this._checkPoolOpen(false);
if ((options.queueMax !== undefined) &&
(typeof options.queueMax !== "number"))
throw new Error(nodbUtil.getErrorMessage('NJS-004', "queueMax"));
if ((options.queueTimeout !== undefined) &&
(typeof options.queueTimeout !== "number"))
throw new Error(nodbUtil.getErrorMessage('NJS-004', "queueTimeout"));
if ((options.enableStatistics !== undefined) &&
(typeof options.enableStatistics !== "boolean"))
throw new Error(nodbUtil.getErrorMessage('NJS-004', "enableStatistics"));
if ((options.resetStatistics !== undefined) &&
(typeof options.resetStatistics != "boolean"))
throw new Error(nodbUtil.getErrorMessage('NJS-004', "resetStatistics"));
this._status = this._oracledb.POOL_STATUS_RECONFIGURING;
try {
// poolMin/poolMax/poolIncrement/poolPingInterval/poolTimeout/
// poolMaxPerShard/stmtCacheSize/sodaMetaDataCache parameters
await this._reconfigure(options);
// pool JS parameters: queueMax, queueTimeout, enableStatistics,
// resetStatistics
// reset the statistics-metrics only if 'resetStatistics' is true or
// 'enableStatistics' is being set to true
if (options.resetStatistics == true || (options.enableStatistics == true &&
this._enableStatistics == false)) {
this._resetStatistics();
}
if (options.queueMax !== undefined) {
this._queueMax = options.queueMax;
}
if (options.queueTimeout !== undefined) {
this._queueTimeout = options.queueTimeout;
}
if (options.enableStatistics !== undefined) {
this._enableStatistics = options.enableStatistics;
}
} finally {
this._status = this._oracledb.POOL_STATUS_OPEN;
}
this.emit('_checkRequestQueue');
}
//-----------------------------------------------------------------------------
// close()
// Close the pool, optionally allowing for a period of time to pass for
// connections to "drain" from the pool.
//-----------------------------------------------------------------------------
async function close(a1) {
let drainTime = 0;
let forceClose = false;
// check arguments
nodbUtil.checkArgCount(arguments, 0, 1);
if (arguments.length == 1) {
// drain time must be a valid number; timeouts larger than a 32-bit signed
// integer are not supported
nodbUtil.assert(typeof a1 === 'number', 'NJS-005', 1);
if (a1 < 0 || isNaN(a1) || a1 > 2 ** 31) {
throw new Error(nodbUtil.getErrorMessage('NJS-005', 1));
}
// no need to worry about drain time if no connections are out!
forceClose = true;
if (this._connectionsOut > 0) {
drainTime = a1 * 1000;
}
}
// if the pool is draining/reconfiguring/closed, throw an appropriate error
this._checkPoolOpen(false);
// wait for the pool to become empty or for the drain timeout to expire
// (whichever comes first)
if (drainTime > 0) {
this._status = this._oracledb.POOL_STATUS_DRAINING;
await new Promise(resolve => {
const timeout = setTimeout(() => {
this.removeAllListeners('_allCheckedIn');
resolve();
}, drainTime);
this.once('_allCheckedIn', () => {
clearTimeout(timeout);
resolve();
});
});
}
// close the pool
await this._close({forceClose: forceClose});
this._status = this._oracledb.POOL_STATUS_CLOSED;
this.emit('_afterPoolClose');
}
//-----------------------------------------------------------------------------
// logStatistics()
// Method to print statistical related information and pool related
// information when enableStatistics is set to true.
// NOTE: This function replaces the DEPRECATED _logStats() function
//-----------------------------------------------------------------------------
function logStatistics() {
const stats = this.getStatistics();
if (stats === null) {
throw new Error(nodbUtil.getErrorMessage('NJS-083'));
}
stats.logStatistics();
}
//-----------------------------------------------------------------------------
// getStatistics()
// Method to obtain a JSON object with all statistical metrics and pool
// properties
//-----------------------------------------------------------------------------
function getStatistics() {
this._checkPoolOpen(false);
if (this._enableStatistics !== true) {
return null;
}
return new PoolStatistics(this);
}
//-----------------------------------------------------------------------------
// _setup()
// Sets up the pool instance with additional attributes used for logging
// statistics and managing the connection queue.
//-----------------------------------------------------------------------------
function _setup(poolAttrs, poolAlias, oracledb) {
if (typeof poolAttrs.queueTimeout !== 'undefined') {
this._queueTimeout = poolAttrs.queueTimeout;
} else {
this._queueTimeout = oracledb.queueTimeout;
}
if (typeof poolAttrs.queueMax !== 'undefined') {
this._queueMax = poolAttrs.queueMax;
} else {
this._queueMax = oracledb.queueMax;
}
if (typeof poolAttrs.poolMaxPerShard !== 'undefined') {
this.poolMaxPerShard = poolAttrs.poolMaxPerShard;
}
if (typeof poolAttrs.enableStatistics !== 'undefined') {
this._enableStatistics = poolAttrs.enableStatistics;
} else {
this._enableStatistics = false; // default statistics is disabled.
}
if (!this._enableStatistics) {
// DEPRECATED property _enableStats.
if (typeof poolAttrs._enableStats !== 'undefined') {
this._enableStatistics = poolAttrs._enableStats;
}
}
if (typeof poolAttrs.sessionCallback !== 'undefined') {
if (typeof poolAttrs.sessionCallback === 'function' ||
typeof poolAttrs.sessionCallback === 'string')
this._sessionCallback = poolAttrs.sessionCallback;
}
// Properties - edition, events, externalAuth - values can be set globally
// on oracledb and can be overridden at pool creation time.
if (typeof poolAttrs.edition !== 'undefined') {
this.edition = poolAttrs.edition;
} else {
this.edition = oracledb.edition;
}
if (typeof poolAttrs.events !== 'undefined') {
this.events = poolAttrs.events;
} else {
this.events = oracledb.events;
}
if (typeof poolAttrs.externalAuth !== 'undefined') {
this.externalAuth = poolAttrs.externalAuth;
} else {
this.externalAuth = oracledb.externalAuth;
}
// Properties - homogeneous, user, connectString - are NOT global properties
if (typeof poolAttrs.homogeneous !== 'undefined') {
this.homogeneous = poolAttrs.homogeneous;
} else {
this.homogeneous = true;
}
this.user = poolAttrs.user || poolAttrs.userName;
this.connectString = poolAttrs.connectString || poolAttrs.connectionString;
// register event handler for when request queue should be checked
this.on('_checkRequestQueue', this._checkRequestQueue);
// Using Object.defineProperties to add properties to the Pool instance with
// special properties, such as enumerable but not writable.
Object.defineProperties(
this,
{
queueMax: { // maximum number of pending pool connections that can be queued
enumerable: true,
get: function() {
return (this._queueMax);
},
},
queueTimeout: { // milliseconds a connection request can spend in queue before being failed
enumerable: true,
get: function() {
return (this._queueTimeout);
},
},
_enableStats: { // DEPRECATED. true means pool stats will be recorded
get: function() {
return (this._enableStatistics);
}
},
enableStatistics: { // true means pool stats will be recorded
enumerable: true,
get: function() {
return (this._enableStatistics);
}
},
_connectionsOut: { // number of connections checked out from the pool. Must be inc/dec in the main thread in JS
value: 0,
writable: true
},
_connRequestQueue: {
value: [],
writable: true
},
_status: { // open/closing/closed
value: oracledb.POOL_STATUS_OPEN,
writable: true
},
poolAlias: {
enumerable: true,
get: function() {
return (poolAlias);
}
},
status: { // open/closing/closed
enumerable: true,
get: function() {
return (this._status);
}
},
sessionCallback: { // session callback
enumerable: true,
get: function() {
return this._sessionCallback;
}
}
}
);
this._resetStatistics();
}
class Pool extends EventEmitter {
_extend(oracledb) {
this._oracledb = oracledb;
this._setup = _setup;
this._checkRequestQueue = _checkRequestQueue;
this.close = nodbUtil.callbackify(close);
this.getConnection = nodbUtil.callbackify(getConnection);
this.reconfigure = nodbUtil.callbackify(reconfigure);
this.logStatistics = logStatistics;
this.getStatistics = getStatistics;
this.terminate = this.close;
this._queueMax = 0;
this._queueTimeout = 0;
this._enableStatistics = false;
this._timeOfReset = this._createdDate = Date.now();
this._sessionCallback = undefined;
// DEPRECATED alias
this._logStats = this.logStatistics;
}
// check if pool is draining/reconfiguring/closed and throw an
// appropriate error
_checkPoolOpen(ignoreReconfiguring) {
// if already in reconfiguring status, nothing to do.
if (this.status === this._oracledb.POOL_STATUS_DRAINING) {
throw new Error(nodbUtil.getErrorMessage('NJS-064'));
} else if (this.status === this._oracledb.POOL_STATUS_CLOSED) {
throw new Error(nodbUtil.getErrorMessage('NJS-065'));
} else if (!ignoreReconfiguring) {
if (this.status === this._oracledb.POOL_STATUS_RECONFIGURING) {
throw new Error(nodbUtil.getErrorMessage('NJS-082'));
}
}
}
// temporary method for determining if an object is a date until
// napi_is_date() can be used (when Node-API v5 can be used)
_isDate(val) {
return (util.isDate(val));
}
//---------------------------------------------------------------------------
// _resetStatistics()
// To initialize the counters/timers
//---------------------------------------------------------------------------
_resetStatistics() {
this._timeOfReset = Date.now();
this._totalConnectionRequests = 0;
this._totalRequestsEnqueued = 0;
this._totalRequestsDequeued = 0;
this._totalFailedRequests = 0;
this._totalRequestsRejected = 0;
this._totalRequestTimeouts = 0;
this._maximumQueueLength = this._connRequestQueue.length;
this._totalTimeInQueue = 0;
this._minTimeInQueue = 0;
this._maxTimeInQueue = 0;
}
// update pool wait statistics after a connect request has spent some time in
// the queue
_updateWaitStatistics(payload) {
const waitTime = Date.now() - payload.enqueuedTime;
this._totalTimeInQueue += waitTime;
if (this._minTimeInQueue === 0) {
this._minTimeInQueue = waitTime;
} else {
this._minTimeInQueue = Math.min(this._minTimeInQueue, waitTime);
}
this._maxTimeInQueue = Math.max(this._maxTimeInQueue, waitTime);
}
}
module.exports = Pool;