UNPKG

oracledb

Version:

A Node.js module for Oracle Database access from JavaScript and TypeScript

822 lines (721 loc) 29 kB
// Copyright (c) 2016, 2024, Oracle and/or its affiliates. //----------------------------------------------------------------------------- // // This software is dual-licensed to you under the Universal Permissive License // (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License // 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose // either license. // // If you elect to accept the software under the Apache License, Version 2.0, // the following applies: // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://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 Connection = require('./connection.js'); const EventEmitter = require('events'); const constants = require('./constants.js'); const errors = require('./errors.js'); const settings = require('./settings.js'); const nodbUtil = require('./util.js'); const impl = require('./impl'); const PoolStatistics = require('./poolStatistics.js'); class Pool extends EventEmitter { constructor() { super(); this._impl = new impl.PoolImpl(); this._queueMax = 0; this._queueTimeout = 0; this._enableStatistics = false; this._timeOfReset = this._createdDate = Date.now(); this._sessionCallback = undefined; this._connRequestQueue = []; this._connectionClass = settings.connectionClass; } //--------------------------------------------------------------------------- // _checkPoolOpen() // // Check if the pool is open (not draining/reconfiguring/closed) and throw an // appropriate exception if not. //--------------------------------------------------------------------------- _checkPoolOpen(ignoreReconfiguring) { if (this.status === constants.POOL_STATUS_DRAINING) { errors.throwErr(errors.ERR_POOL_CLOSING); } else if (this.status === constants.POOL_STATUS_CLOSED) { errors.throwErr(errors.ERR_POOL_CLOSED); } else if (!ignoreReconfiguring) { if (this.status === constants.POOL_STATUS_RECONFIGURING) { errors.throwErr(errors.ERR_POOL_RECONFIGURING); } } } //--------------------------------------------------------------------------- // _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. //--------------------------------------------------------------------------- _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(); } } //--------------------------------------------------------------------------- // _enableStats (DEPRECATED) // // Property for whether statistics are enabled on the pool. //--------------------------------------------------------------------------- get _enableStats() { return this._enableStatistics; } //--------------------------------------------------------------------------- // _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; } //--------------------------------------------------------------------------- // _setup() // // Sets up the pool instance with additional attributes used for logging // statistics and managing the connection queue. //--------------------------------------------------------------------------- _setup(options, poolAlias) { this._queueTimeout = options.queueTimeout; this._queueMax = options.queueMax; this._enableStatistics = options.enableStatistics; this._edition = options.edition; this._eventsFlag = options.events; this._externalAuth = options.externalAuth; this._homogeneous = options.homogeneous; this._user = options.user; this._connectString = options.connectString; this._status = constants.POOL_STATUS_OPEN; this._connectionsOut = 0; this._poolAlias = poolAlias; // register event handler for when request queue should be checked this.on('_checkRequestQueue', this._checkRequestQueue); this._resetStatistics(); } //--------------------------------------------------------------------------- // _updateWaitStatistics() // // 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); } //--------------------------------------------------------------------------- // _verifyGetConnectionOptions() // // Verify the getConnection() options are acceptable. Performs any // transformations that are needed before returning the options to the // caller. //--------------------------------------------------------------------------- _verifyGetConnectionOptions(options) { // define normalized options (value returned to caller) const outOptions = {}; // only one of "user" and "username" may be specified (and must be strings) if (options.user !== undefined) { errors.assertParamPropValue(typeof options.user === 'string', 1, "user"); outOptions.user = options.user; } if (options.username !== undefined) { errors.assert(outOptions.user === undefined, errors.ERR_DBL_USER); errors.assertParamPropValue(typeof options.username === 'string', 1, "username"); outOptions.user = options.username; } if (this.externalAuth && outOptions.user && (outOptions.user[0] !== '[' || outOptions.user.slice(-1) !== ']')) { // username is not enclosed in []. errors.throwErr(errors.ERR_WRONG_USER_FORMAT_EXTAUTH_PROXY); } // password must be a string if (options.password !== undefined) { errors.assertParamPropValue(typeof options.password === 'string', 1, "password"); if (this.externalAuth) { errors.throwErr(errors.ERR_WRONG_CRED_FOR_EXTAUTH); } outOptions.password = options.password; } // tag must be a string if (options.tag !== undefined) { errors.assertParamPropValue(typeof options.tag === 'string', 1, "tag"); outOptions.tag = options.tag; } // matchAnyTag must be a boolean if (options.matchAnyTag !== undefined) { errors.assertParamPropValue(typeof options.matchAnyTag === 'boolean', 1, "matchAnyTag"); outOptions.matchAnyTag = options.matchAnyTag; } // shardingKey must be an array of values if (options.shardingKey !== undefined) { const value = options.shardingKey; errors.assertParamPropValue(nodbUtil.isShardingKey(value), 1, "shardingKey"); outOptions.shardingKey = options.shardingKey; } // superShardingKey must be an array of values if (options.superShardingKey !== undefined) { const value = options.superShardingKey; errors.assertParamPropValue(nodbUtil.isShardingKey(value), 1, "superShardingKey"); outOptions.superShardingKey = options.superShardingKey; } // privilege must be one of a set of named constants if (options.privilege !== undefined) { errors.assertParamPropValue(nodbUtil.isPrivilege(options.privilege), 1, "privilege"); outOptions.privilege = options.privilege; } return outOptions; } //--------------------------------------------------------------------------- // close() // // Close the pool, optionally allowing for a period of time to pass for // connections to "drain" from the pool. //--------------------------------------------------------------------------- async close(a1) { let drainTime = 0; let forceClose = false; // check arguments errors.assertArgCount(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 errors.assertParamValue(typeof a1 === 'number', 1); if (a1 < 0 || isNaN(a1) || a1 > 2 ** 31) { errors.throwErr(errors.ERR_INVALID_PARAMETER_VALUE, 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 = constants.POOL_STATUS_DRAINING; await new Promise(resolve => { const timeout = setTimeout(() => { this.removeAllListeners('_allCheckedIn'); resolve(); }, drainTime); this.once('_allCheckedIn', () => { clearTimeout(timeout); resolve(); }); }); } // if any connections are still out and the pool is not being force closed, // throw an exception if (!forceClose && this._connectionsOut > 0) { errors.throwErr(errors.ERR_POOL_HAS_BUSY_CONNECTIONS); } // close the pool await this._impl.close(); this._status = constants.POOL_STATUS_CLOSED; this.emit('_afterPoolClose'); } //--------------------------------------------------------------------------- // connectionsInUse // // Property for the number of connections in use by the pool. //--------------------------------------------------------------------------- get connectionsInUse() { return this._impl.getConnectionsInUse(); } //--------------------------------------------------------------------------- // connectionsOpen // // Property for the number of connections opened by the pool. //--------------------------------------------------------------------------- get connectionsOpen() { return this._impl.getConnectionsOpen(); } //--------------------------------------------------------------------------- // connectString // // Property for the connect string used to create the pool. //--------------------------------------------------------------------------- get connectString() { return this._connectString; } //--------------------------------------------------------------------------- // thin() // // return true, if driver mode is thin while creating pool // return false, if driver mode is thick while creating pool //--------------------------------------------------------------------------- get thin() { return settings.thin; } //--------------------------------------------------------------------------- // edition // // Property for the edition used to create the pool. //--------------------------------------------------------------------------- get edition() { return this._edition; } //--------------------------------------------------------------------------- // enableStatistics // // Property for whether statistics are enabled on the pool. //--------------------------------------------------------------------------- get enableStatistics() { return this._enableStatistics; } //--------------------------------------------------------------------------- // events // // Property for the events flag value used to create the pool. //--------------------------------------------------------------------------- get events() { return this._eventsFlag; } //--------------------------------------------------------------------------- // externalAuth // // Property for the externalAuth flag value used to create the pool. //--------------------------------------------------------------------------- get externalAuth() { return this._externalAuth; } //--------------------------------------------------------------------------- // 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 getConnection(a1) { let poolMax; let options = {}; // check arguments errors.assertArgCount(arguments, 0, 1); if (arguments.length == 1) { errors.assertParamValue(nodbUtil.isObject(a1), 1); options = this._verifyGetConnectionOptions(a1); } // get connection class value from pool options.connectionClass = this._connectionClass; // 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; } while (this._connectionsOut >= poolMax || this.status === constants.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; } errors.throwErr(errors.ERR_QUEUE_MAX_EXCEEDED, 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); } try { errors.throwErr(errors.ERR_CONN_REQUEST_TIMEOUT, this._queueTimeout); } catch (err) { reject(err); } }, 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 = new Connection(); conn._impl = await this._impl.getConnection(options); conn._pool = this; // 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._impl._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; } } //--------------------------------------------------------------------------- // getStatistics() // // Method to obtain a JSON object with all statistical metrics and pool // properties //--------------------------------------------------------------------------- getStatistics() { this._checkPoolOpen(false); if (this._enableStatistics !== true) { return null; } return new PoolStatistics(this); } //--------------------------------------------------------------------------- // homogeneous // // Property for the homogeneous flag value used to create the pool. //--------------------------------------------------------------------------- get homogeneous() { return this._homogeneous; } //--------------------------------------------------------------------------- // 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. //--------------------------------------------------------------------------- logStatistics() { const stats = this.getStatistics(); if (stats === null) { errors.throwErr(errors.ERR_POOL_STATISTICS_DISABLED); } stats.logStatistics(); } //--------------------------------------------------------------------------- // poolAlias // // Property for the alias assigned to the pool. // --------------------------------------------------------------------------- get poolAlias() { return this._poolAlias; } //--------------------------------------------------------------------------- // poolIncrement // // Property for the number of connections to create each time the pool needs // to grow. // --------------------------------------------------------------------------- get poolIncrement() { return this._impl.getPoolIncrement(); } //--------------------------------------------------------------------------- // poolMax // // Property for the maximum number of connections allowed in the pool. //--------------------------------------------------------------------------- get poolMax() { return this._impl.getPoolMax(); } //--------------------------------------------------------------------------- // poolMaxPerShard // // Property for the maximum number of connections allowed in the pool for // each shard. //--------------------------------------------------------------------------- get poolMaxPerShard() { return this._impl.getPoolMaxPerShard(); } //--------------------------------------------------------------------------- // poolMin // // Property for the minimum number of connections allowed in the pool. //--------------------------------------------------------------------------- get poolMin() { return this._impl.getPoolMin(); } //--------------------------------------------------------------------------- // poolPingInterval // // Property for the ping interval to use for the pool. //--------------------------------------------------------------------------- get poolPingInterval() { return this._impl.getPoolPingInterval(); } //--------------------------------------------------------------------------- // poolPingTimeout // // Property for the ping timeout associated with the pool. //--------------------------------------------------------------------------- get poolPingTimeout() { return this._impl.getPoolPingTimeout(); } //--------------------------------------------------------------------------- // poolTimeout // // Property for the timeout associated with the pool. //--------------------------------------------------------------------------- get poolTimeout() { return this._impl.getPoolTimeout(); } //--------------------------------------------------------------------------- // queueMax // // Property for the maximum number of pending pool connections that can be // queued. //--------------------------------------------------------------------------- get queueMax() { return this._queueMax; } //--------------------------------------------------------------------------- // queueTimeout // // Property for the milliseconds a connection request can spend in the queue // before an exception is thrown. //--------------------------------------------------------------------------- get queueTimeout() { return this._queueTimeout; } //--------------------------------------------------------------------------- // reconfigure() // // Reconfigure the pool, change the value for given pool-properties. //--------------------------------------------------------------------------- async reconfigure(options) { // check arguments errors.assertArgCount(arguments, 1, 1); errors.assertParamValue(nodbUtil.isObject(options)); errors.assertParamPropUnsignedInt(options, 1, "queueMax"); errors.assertParamPropUnsignedInt(options, 1, "queueTimeout"); errors.assertParamPropBool(options, 1, "enableStatistics"); errors.assertParamPropBool(options, 1, "resetStatistics"); errors.assertParamPropUnsignedInt(options, 1, "poolMin"); errors.assertParamPropUnsignedIntNonZero(options, 1, "poolMax"); errors.assertParamPropUnsignedInt(options, 1, "poolMaxPerShard"); errors.assertParamPropUnsignedInt(options, 1, "poolIncrement"); errors.assertParamPropInt(options, 1, "poolPingInterval"); errors.assertParamPropUnsignedInt(options, 1, "poolTimeout"); errors.assertParamPropUnsignedInt(options, 1, "stmtCacheSize"); errors.assertParamPropBool(options, 1, "sodaMetaDataCache"); // poolMax must be greater than or equal to poolMin if (options.poolMin > options.poolMax) { errors.throwErr(errors.ERR_INVALID_NUMBER_OF_CONNECTIONS, options.poolMax, options.poolMin); } // reconfiguration can happen only when status is OPEN this._checkPoolOpen(false); this._status = constants.POOL_STATUS_RECONFIGURING; try { // poolMin/poolMax/poolIncrement/poolPingInterval/poolTimeout/ // poolMaxPerShard/stmtCacheSize/sodaMetaDataCache parameters await this._impl.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 = constants.POOL_STATUS_OPEN; } this.emit('_checkRequestQueue'); } //--------------------------------------------------------------------------- // sessionCallback // // Property for the session callback associated with the pool. //--------------------------------------------------------------------------- get sessionCallback() { return this._sessionCallback; } //--------------------------------------------------------------------------- // setAccessToken() // // Set parameters for token based authentication. //--------------------------------------------------------------------------- async setAccessToken(options) { errors.assertArgCount(arguments, 1, 1); errors.assertParamValue(nodbUtil.isObject(options), 1); errors.assertParamPropString(options, 1, "token"); errors.assertParamPropString(options, 1, "privateKey"); await this._impl.setAccessToken(options); } //--------------------------------------------------------------------------- // sodaMetaDataCache // // Property for whether the SODA metadata cache is enabled or not. //--------------------------------------------------------------------------- get sodaMetaDataCache() { return this._impl.getSodaMetaDataCache(); } //--------------------------------------------------------------------------- // status // // Property for the pool's status. //--------------------------------------------------------------------------- get status() { return this._status; } //--------------------------------------------------------------------------- // stmtCacheSize // // Property for the size of the statement cache to use when creating // connections in the pool. //--------------------------------------------------------------------------- get stmtCacheSize() { return this._impl.getStmtCacheSize(); } //--------------------------------------------------------------------------- // user // // Property for the user used to create the pool. //--------------------------------------------------------------------------- get user() { return this._user; } //--------------------------------------------------------------------------- // connectTraceConfig // // Property for getting the connection related config. //--------------------------------------------------------------------------- get connectTraceConfig() { return this._impl && this._impl._getConnectTraceConfig(); } } nodbUtil.wrapFns(Pool.prototype, false, "close", "getConnection", "reconfigure", "setAccessToken"); // DEPRECATED aliases Pool.prototype.terminate = Pool.prototype.close; Pool.prototype._logStats = Pool.prototype.logStatistics; module.exports = Pool;