657 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			657 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| //  SPDX-License-Identifier: LGPL-2.1-or-later
 | |
| //  Copyright (c) 2015-2024 MariaDB Corporation Ab
 | |
| 
 | |
| 'use strict';
 | |
| 
 | |
| const { EventEmitter } = require('events');
 | |
| 
 | |
| const Queue = require('denque');
 | |
| const Errors = require('./misc/errors');
 | |
| const Utils = require('./misc/utils');
 | |
| const Connection = require('./connection');
 | |
| 
 | |
| class Pool extends EventEmitter {
 | |
|   opts;
 | |
|   #closed = false;
 | |
|   #connectionInCreation = false;
 | |
|   #errorCreatingConnection = null;
 | |
|   #idleConnections = new Queue();
 | |
|   #activeConnections = {};
 | |
|   #requests = new Queue();
 | |
|   #unusedConnectionRemoverId;
 | |
|   #requestTimeoutId;
 | |
|   #connErrorNumber = 0;
 | |
|   #initialized = false;
 | |
|   _sizeHandlerTimeout;
 | |
| 
 | |
|   constructor(options) {
 | |
|     super();
 | |
|     this.opts = options;
 | |
| 
 | |
|     this.on('_idle', this._requestsHandler);
 | |
|     this.on('validateSize', this._sizeHandler);
 | |
|     this._sizeHandler();
 | |
|   }
 | |
| 
 | |
|   //*****************************************************************
 | |
|   // pool automatic handlers
 | |
|   //*****************************************************************
 | |
| 
 | |
|   _doCreateConnection(resolve, reject, timeoutEnd) {
 | |
|     this._createConnection(timeoutEnd)
 | |
|       .then((conn) => {
 | |
|         if (this.#closed) {
 | |
|           conn.forceEnd(
 | |
|             null,
 | |
|             () => {},
 | |
|             () => {}
 | |
|           );
 | |
|           reject(
 | |
|             new Errors.createFatalError(
 | |
|               'Cannot create new connection to pool, pool closed',
 | |
|               Errors.ER_ADD_CONNECTION_CLOSED_POOL
 | |
|             )
 | |
|           );
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         conn.lastUse = Date.now();
 | |
|         const nativeDestroy = conn.destroy.bind(conn);
 | |
|         const pool = this;
 | |
| 
 | |
|         conn.destroy = function () {
 | |
|           pool._endLeak(conn);
 | |
|           delete pool.#activeConnections[conn.threadId];
 | |
|           nativeDestroy();
 | |
|           pool.emit('validateSize');
 | |
|         };
 | |
| 
 | |
|         conn.once('error', function () {
 | |
|           let idx = 0;
 | |
|           let currConn;
 | |
|           pool._endLeak(conn);
 | |
|           delete pool.#activeConnections[conn.threadId];
 | |
|           while ((currConn = pool.#idleConnections.peekAt(idx))) {
 | |
|             if (currConn === conn) {
 | |
|               pool.#idleConnections.removeOne(idx);
 | |
|               continue;
 | |
|             }
 | |
|             //since connection did have an error, other waiting connection might too
 | |
|             //force validation when borrowed next time, even if "minDelayValidation" is not reached.
 | |
|             currConn.lastUse = Math.min(currConn.lastUse, Date.now() - pool.opts.minDelayValidation);
 | |
|             idx++;
 | |
|           }
 | |
|           setTimeout(() => {
 | |
|             if (!pool.#requests.isEmpty()) {
 | |
|               pool._sizeHandler();
 | |
|             }
 | |
|           }, 0);
 | |
|         });
 | |
| 
 | |
|         this.#idleConnections.push(conn);
 | |
|         this.#connectionInCreation = false;
 | |
|         this.emit('_idle');
 | |
|         this.emit('connection', conn);
 | |
|         resolve(conn);
 | |
|       })
 | |
|       .catch((err) => {
 | |
|         //if timeout is reached or authentication fail return error
 | |
|         if (err instanceof AggregateError) {
 | |
|           err = err.errors[0];
 | |
|         }
 | |
|         if (
 | |
|           this.#closed ||
 | |
|           (err.errno && (err.errno === 1524 || err.errno === 1045 || err.errno === 1698)) ||
 | |
|           timeoutEnd < Date.now()
 | |
|         ) {
 | |
|           err.message = err.message + this._errorMsgAddon();
 | |
|           reject(err);
 | |
|           return;
 | |
|         }
 | |
|         setTimeout(this._doCreateConnection.bind(this, resolve, reject, timeoutEnd), 500);
 | |
|       });
 | |
|   }
 | |
| 
 | |
|   _destroy(conn) {
 | |
|     this._endLeak(conn);
 | |
|     delete this.#activeConnections[conn.threadId];
 | |
|     conn.lastUse = Date.now();
 | |
|     conn.forceEnd(
 | |
|       null,
 | |
|       () => {},
 | |
|       () => {}
 | |
|     );
 | |
| 
 | |
|     if (this.totalConnections() === 0) {
 | |
|       this._stopReaping();
 | |
|     }
 | |
| 
 | |
|     this.emit('validateSize');
 | |
|   }
 | |
| 
 | |
|   release(conn) {
 | |
|     // ensure releasing only once
 | |
|     if (this.#activeConnections[conn.threadId]) {
 | |
|       this._endLeak(conn);
 | |
|       this.#activeConnections[conn.threadId] = null;
 | |
|       conn.lastUse = Date.now();
 | |
| 
 | |
|       if (this.#closed) {
 | |
|         conn.forceEnd(
 | |
|           null,
 | |
|           () => {},
 | |
|           () => {}
 | |
|         );
 | |
|       } else if (conn.isValid()) {
 | |
|         this.emit('release', conn);
 | |
|         this.#idleConnections.push(conn);
 | |
|         process.nextTick(this.emit.bind(this, '_idle'));
 | |
|       } else {
 | |
|         this.emit('validateSize');
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _checkLeak(conn) {
 | |
|     conn.lastUse = Date.now();
 | |
|     conn.leaked = false;
 | |
|     conn.leakProcess = setTimeout(
 | |
|       (conn) => {
 | |
|         conn.leaked = true;
 | |
|         conn.opts.logger.warning(
 | |
|           `A possible connection leak on the thread ${
 | |
|             conn.info.threadId
 | |
|           } (the connection not returned to the pool since ${
 | |
|             Date.now() - conn.lastUse
 | |
|           } ms). Has the connection.release() been called ?` + this._errorMsgAddon()
 | |
|         );
 | |
|       },
 | |
|       this.opts.leakDetectionTimeout,
 | |
|       conn
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _endLeak(conn) {
 | |
|     if (conn.leakProcess) {
 | |
|       clearTimeout(conn.leakProcess);
 | |
|       conn.leakProcess = null;
 | |
|       if (conn.leaked) {
 | |
|         conn.opts.logger.warning(
 | |
|           `Previous possible leak connection with thread ${conn.info.threadId} was returned to pool`
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Permit to remove idle connection if unused for some time.
 | |
|    */
 | |
|   _startReaping() {
 | |
|     if (!this.#unusedConnectionRemoverId && this.opts.idleTimeout > 0) {
 | |
|       this.#unusedConnectionRemoverId = setInterval(this._reaper.bind(this), 500);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _stopReaping() {
 | |
|     if (this.#unusedConnectionRemoverId && this.totalConnections() === 0) {
 | |
|       clearInterval(this.#unusedConnectionRemoverId);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _reaper() {
 | |
|     const idleTimeRemoval = Date.now() - this.opts.idleTimeout * 1000;
 | |
|     let maxRemoval = Math.max(0, this.#idleConnections.length - this.opts.minimumIdle);
 | |
|     while (maxRemoval > 0) {
 | |
|       const conn = this.#idleConnections.peek();
 | |
|       maxRemoval--;
 | |
|       if (conn && conn.lastUse < idleTimeRemoval) {
 | |
|         this.#idleConnections.shift();
 | |
|         conn.forceEnd(
 | |
|           null,
 | |
|           () => {},
 | |
|           () => {}
 | |
|         );
 | |
|         continue;
 | |
|       }
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     if (this.totalConnections() === 0) {
 | |
|       this._stopReaping();
 | |
|     }
 | |
|     this.emit('validateSize');
 | |
|   }
 | |
| 
 | |
|   _shouldCreateMoreConnections() {
 | |
|     return (
 | |
|       !this.#connectionInCreation &&
 | |
|       this.#idleConnections.length < this.opts.minimumIdle &&
 | |
|       this.totalConnections() < this.opts.connectionLimit &&
 | |
|       !this.#closed
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Grow pool connections until reaching connection limit.
 | |
|    */
 | |
|   _sizeHandler() {
 | |
|     if (this._shouldCreateMoreConnections() && !this._sizeHandlerTimeout) {
 | |
|       this.#connectionInCreation = true;
 | |
|       setImmediate(
 | |
|         function () {
 | |
|           const timeoutEnd = Date.now() + this.opts.initializationTimeout;
 | |
|           new Promise((resolve, reject) => {
 | |
|             this._doCreateConnection(resolve, reject, timeoutEnd);
 | |
|           })
 | |
|             .then(() => {
 | |
|               this.#initialized = true;
 | |
|               this.#errorCreatingConnection = null;
 | |
|               this.#connErrorNumber = 0;
 | |
|               if (this._shouldCreateMoreConnections()) {
 | |
|                 this.emit('validateSize');
 | |
|               }
 | |
|               this._startReaping();
 | |
|             })
 | |
|             .catch((err) => {
 | |
|               this.#connectionInCreation = false;
 | |
|               if (!this.#closed) {
 | |
|                 if (!this.#initialized) {
 | |
|                   err.message = 'Error during pool initialization: ' + err.message;
 | |
|                 } else {
 | |
|                   err.message = 'Pool fails to create connection: ' + err.message;
 | |
|                 }
 | |
|                 this.#errorCreatingConnection = err;
 | |
|                 this.emit('error', err);
 | |
| 
 | |
|                 //delay next try
 | |
|                 this._sizeHandlerTimeout = setTimeout(
 | |
|                   function () {
 | |
|                     this._sizeHandlerTimeout = null;
 | |
|                     if (!this.#requests.isEmpty()) {
 | |
|                       this._sizeHandler();
 | |
|                     }
 | |
|                   }.bind(this),
 | |
|                   Math.min(++this.#connErrorNumber * 500, 10000)
 | |
|                 );
 | |
|               }
 | |
|             });
 | |
|         }.bind(this)
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Launch next waiting task request if available connections.
 | |
|    */
 | |
|   _requestsHandler() {
 | |
|     clearTimeout(this.#requestTimeoutId);
 | |
|     this.#requestTimeoutId = null;
 | |
|     const request = this.#requests.shift();
 | |
|     if (request) {
 | |
|       const conn = this.#idleConnections.shift();
 | |
|       if (conn) {
 | |
|         if (this.opts.leakDetectionTimeout > 0) this._checkLeak(conn);
 | |
|         this.emit('acquire', conn);
 | |
|         this.#activeConnections[conn.threadId] = conn;
 | |
|         request.resolver(conn);
 | |
|       } else {
 | |
|         this.#requests.unshift(request);
 | |
|       }
 | |
|       this._requestTimeoutHandler();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _hasIdleConnection() {
 | |
|     return !this.#idleConnections.isEmpty();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Return an idle Connection.
 | |
|    * If connection has not been used for some time ( minDelayValidation), validate connection status.
 | |
|    *
 | |
|    * @returns {Promise<Connection>} connection of null of no valid idle connection.
 | |
|    */
 | |
|   async _doAcquire() {
 | |
|     if (!this._hasIdleConnection() || this.#closed) return Promise.reject();
 | |
|     let conn;
 | |
|     let mustRecheckSize = false;
 | |
|     while ((conn = this.#idleConnections.shift()) != null) {
 | |
|       //just check connection state first
 | |
|       if (conn.isValid()) {
 | |
|         this.#activeConnections[conn.threadId] = conn;
 | |
|         //if not used for some time, validate connection with a COM_PING
 | |
|         if (this.opts.minDelayValidation <= 0 || Date.now() - conn.lastUse > this.opts.minDelayValidation) {
 | |
|           try {
 | |
|             const cmdParam = {
 | |
|               opts: { timeout: this.opts.pingTimeout }
 | |
|             };
 | |
|             await new Promise(conn.ping.bind(conn, cmdParam));
 | |
|           } catch (e) {
 | |
|             delete this.#activeConnections[conn.threadId];
 | |
|             continue;
 | |
|           }
 | |
|         }
 | |
|         if (this.opts.leakDetectionTimeout > 0) this._checkLeak(conn);
 | |
|         if (mustRecheckSize) setImmediate(this.emit.bind(this, 'validateSize'));
 | |
|         return Promise.resolve(conn);
 | |
|       }
 | |
|       mustRecheckSize = true;
 | |
|     }
 | |
|     setImmediate(this.emit.bind(this, 'validateSize'));
 | |
|     return Promise.reject();
 | |
|   }
 | |
| 
 | |
|   _requestTimeoutHandler() {
 | |
|     //handle next Timer
 | |
|     this.#requestTimeoutId = null;
 | |
|     const currTime = Date.now();
 | |
|     let request;
 | |
|     while ((request = this.#requests.peekFront())) {
 | |
|       if (request.timeout <= currTime) {
 | |
|         this.#requests.shift();
 | |
| 
 | |
|         let cause = this.activeConnections() === 0 ? this.#errorCreatingConnection : null;
 | |
|         let err = Errors.createError(
 | |
|           `retrieve connection from pool timeout after ${Math.abs(
 | |
|             Date.now() - (request.timeout - this.opts.acquireTimeout)
 | |
|           )}ms${this._errorMsgAddon()}`,
 | |
|           Errors.ER_GET_CONNECTION_TIMEOUT,
 | |
|           null,
 | |
|           'HY000',
 | |
|           null,
 | |
|           false,
 | |
|           request.stack,
 | |
|           null,
 | |
|           cause
 | |
|         );
 | |
| 
 | |
|         request.reject(err);
 | |
|       } else {
 | |
|         this.#requestTimeoutId = setTimeout(this._requestTimeoutHandler.bind(this), request.timeout - currTime);
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Search info object of an existing connection. to know server type and version.
 | |
|    * @returns information object if connection available.
 | |
|    */
 | |
|   _searchInfo() {
 | |
|     let info = null;
 | |
|     let conn = this.#idleConnections.get(0);
 | |
| 
 | |
|     if (!conn) {
 | |
|       for (const threadId in Object.keys(this.#activeConnections)) {
 | |
|         conn = this.#activeConnections[threadId];
 | |
|         if (!conn) {
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (conn) {
 | |
|       info = conn.info;
 | |
|     }
 | |
|     return info;
 | |
|   }
 | |
| 
 | |
|   async _createConnection(timeoutEnd) {
 | |
|     // ensure setting a connection timeout if no connection timeout is set
 | |
|     const minTimeout = Math.max(1, timeoutEnd - 100);
 | |
|     const connectionOpts =
 | |
|       !this.opts.connOptions.connectTimeout || this.opts.connOptions.connectTimeout > minTimeout
 | |
|         ? Object.assign({}, this.opts.connOptions, { connectTimeout: minTimeout })
 | |
|         : this.opts.connOptions;
 | |
|     const conn = new Connection(connectionOpts);
 | |
|     await conn.connect();
 | |
|     const pool = this;
 | |
|     conn.forceEnd = conn.end;
 | |
|     conn.release = function (resolve) {
 | |
|       if (pool.#closed || !conn.isValid()) {
 | |
|         pool._destroy(conn);
 | |
|         resolve();
 | |
|         return;
 | |
|       }
 | |
|       if (pool.opts.noControlAfterUse) {
 | |
|         pool.release(conn);
 | |
|         resolve();
 | |
|         return;
 | |
|       }
 | |
|       //if server permit it, reset the connection, or rollback only if not
 | |
|       // COM_RESET_CONNECTION exist since mysql 5.7.3 and mariadb 10.2.4
 | |
|       // but not possible to use it with mysql waiting for https://bugs.mysql.com/bug.php?id=97633 correction.
 | |
|       // and mariadb only since https://jira.mariadb.org/browse/MDEV-18281
 | |
|       let revertFunction;
 | |
|       if (
 | |
|         pool.opts.resetAfterUse &&
 | |
|         conn.info.isMariaDB() &&
 | |
|         ((conn.info.serverVersion.minor === 2 && conn.info.hasMinVersion(10, 2, 22)) ||
 | |
|           conn.info.hasMinVersion(10, 3, 13))
 | |
|       ) {
 | |
|         revertFunction = conn.reset.bind(conn, {});
 | |
|       } else revertFunction = conn.changeTransaction.bind(conn, { sql: 'ROLLBACK' });
 | |
| 
 | |
|       new Promise(revertFunction).then(pool.release.bind(pool, conn), pool._destroy.bind(pool, conn)).finally(resolve);
 | |
|     };
 | |
|     conn.end = conn.release;
 | |
|     return conn;
 | |
|   }
 | |
| 
 | |
|   _leakedConnections() {
 | |
|     let counter = 0;
 | |
|     for (const connection of Object.values(this.#activeConnections)) {
 | |
|       if (connection && connection.leaked) counter++;
 | |
|     }
 | |
|     return counter;
 | |
|   }
 | |
| 
 | |
|   _errorMsgAddon() {
 | |
|     if (this.opts.leakDetectionTimeout > 0) {
 | |
|       return `\n    (pool connections: active=${this.activeConnections()} idle=${this.idleConnections()} leak=${this._leakedConnections()} limit=${
 | |
|         this.opts.connectionLimit
 | |
|       })`;
 | |
|     }
 | |
|     return `\n    (pool connections: active=${this.activeConnections()} idle=${this.idleConnections()} limit=${
 | |
|       this.opts.connectionLimit
 | |
|     })`;
 | |
|   }
 | |
| 
 | |
|   toString() {
 | |
|     return `active=${this.activeConnections()} idle=${this.idleConnections()} limit=${this.opts.connectionLimit}`;
 | |
|   }
 | |
| 
 | |
|   //*****************************************************************
 | |
|   // public methods
 | |
|   //*****************************************************************
 | |
| 
 | |
|   get closed() {
 | |
|     return this.#closed;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get current total connection number.
 | |
|    * @return {number}
 | |
|    */
 | |
|   totalConnections() {
 | |
|     return this.activeConnections() + this.idleConnections();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get current active connections.
 | |
|    * @return {number}
 | |
|    */
 | |
|   activeConnections() {
 | |
|     let counter = 0;
 | |
|     for (const connection of Object.values(this.#activeConnections)) {
 | |
|       if (connection) counter++;
 | |
|     }
 | |
|     return counter;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get current idle connection number.
 | |
|    * @return {number}
 | |
|    */
 | |
|   idleConnections() {
 | |
|     return this.#idleConnections.length;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get current stacked connection request.
 | |
|    * @return {number}
 | |
|    */
 | |
|   taskQueueSize() {
 | |
|     return this.#requests.length;
 | |
|   }
 | |
| 
 | |
|   escape(value) {
 | |
|     return Utils.escape(this.opts.connOptions, this._searchInfo(), value);
 | |
|   }
 | |
| 
 | |
|   escapeId(value) {
 | |
|     return Utils.escapeId(this.opts.connOptions, this._searchInfo(), value);
 | |
|   }
 | |
| 
 | |
|   //*****************************************************************
 | |
|   // promise methods
 | |
|   //*****************************************************************
 | |
| 
 | |
|   /**
 | |
|    * Retrieve a connection from pool.
 | |
|    * Create a new one, if limit is not reached.
 | |
|    * wait until acquireTimeout.
 | |
|    * @param cmdParam for stackTrace error
 | |
|    * @return {Promise}
 | |
|    */
 | |
|   getConnection(cmdParam) {
 | |
|     if (this.#closed) {
 | |
|       return Promise.reject(
 | |
|         Errors.createError(
 | |
|           'pool is closed',
 | |
|           Errors.ER_POOL_ALREADY_CLOSED,
 | |
|           null,
 | |
|           'HY000',
 | |
|           cmdParam === null ? null : cmdParam.sql,
 | |
|           false,
 | |
|           cmdParam.stack
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|     return this._doAcquire().then(
 | |
|       (conn) => {
 | |
|         // connection is available. process task
 | |
|         this.emit('acquire', conn);
 | |
|         return conn;
 | |
|       },
 | |
|       () => {
 | |
|         if (this.#closed) {
 | |
|           throw Errors.createError(
 | |
|             'Cannot add request to pool, pool is closed',
 | |
|             Errors.ER_POOL_ALREADY_CLOSED,
 | |
|             null,
 | |
|             'HY000',
 | |
|             cmdParam === null ? null : cmdParam.sql,
 | |
|             false,
 | |
|             cmdParam.stack
 | |
|           );
 | |
|         }
 | |
|         // no idle connection available
 | |
|         // create a new connection if limit is not reached
 | |
|         setImmediate(this.emit.bind(this, 'validateSize'));
 | |
|         return new Promise(
 | |
|           function (resolver, rejecter) {
 | |
|             // stack request
 | |
|             setImmediate(this.emit.bind(this, 'enqueue'));
 | |
|             const request = new Request(Date.now() + this.opts.acquireTimeout, cmdParam.stack, resolver, rejecter);
 | |
|             this.#requests.push(request);
 | |
|             if (!this.#requestTimeoutId) {
 | |
|               this.#requestTimeoutId = setTimeout(this._requestTimeoutHandler.bind(this), this.opts.acquireTimeout);
 | |
|             }
 | |
|           }.bind(this)
 | |
|         );
 | |
|       }
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Close all connection in pool
 | |
|    * Ends in multiple step :
 | |
|    * - close idle connections
 | |
|    * - ensure that no new request is possible
 | |
|    *   (active connection release are automatically closed on release)
 | |
|    * - if remaining, after 10 seconds, close remaining active connections
 | |
|    *
 | |
|    * @return Promise
 | |
|    */
 | |
|   end() {
 | |
|     if (this.#closed) {
 | |
|       return Promise.reject(Errors.createError('pool is already closed', Errors.ER_POOL_ALREADY_CLOSED));
 | |
|     }
 | |
|     this.#closed = true;
 | |
|     clearInterval(this.#unusedConnectionRemoverId);
 | |
|     clearInterval(this._sizeHandlerTimeout);
 | |
|     const cmdParam = {};
 | |
|     if (this.opts.trace) Error.captureStackTrace(cmdParam);
 | |
|     //close unused connections
 | |
|     const idleConnectionsEndings = [];
 | |
|     let conn;
 | |
|     while ((conn = this.#idleConnections.shift())) {
 | |
|       idleConnectionsEndings.push(new Promise(conn.forceEnd.bind(conn, cmdParam)));
 | |
|     }
 | |
| 
 | |
|     clearTimeout(this.#requestTimeoutId);
 | |
|     this.#requestTimeoutId = null;
 | |
| 
 | |
|     //reject all waiting task
 | |
|     if (!this.#requests.isEmpty()) {
 | |
|       const err = Errors.createError(
 | |
|         'pool is ending, connection request aborted',
 | |
|         Errors.ER_CLOSING_POOL,
 | |
|         null,
 | |
|         'HY000',
 | |
|         null,
 | |
|         false,
 | |
|         cmdParam.stack
 | |
|       );
 | |
|       let task;
 | |
|       while ((task = this.#requests.shift())) {
 | |
|         task.reject(err);
 | |
|       }
 | |
|     }
 | |
|     const pool = this;
 | |
|     return Promise.all(idleConnectionsEndings).then(async () => {
 | |
|       if (pool.activeConnections() > 0) {
 | |
|         // wait up to 10 seconds, that active connection are released
 | |
|         let remaining = 100;
 | |
|         while (remaining-- > 0) {
 | |
|           if (pool.activeConnections() > 0) {
 | |
|             await new Promise((res) => setTimeout(() => res(), 100));
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // force close any remaining active connections
 | |
|         for (const connection of Object.values(pool.#activeConnections)) {
 | |
|           if (connection) connection.destroy();
 | |
|         }
 | |
|       }
 | |
|       return Promise.resolve();
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| class Request {
 | |
|   constructor(timeout, stack, resolver, rejecter) {
 | |
|     this.timeout = timeout;
 | |
|     this.stack = stack;
 | |
|     this.resolver = resolver;
 | |
|     this.rejecter = rejecter;
 | |
|   }
 | |
| 
 | |
|   reject(err) {
 | |
|     process.nextTick(this.rejecter, err);
 | |
|   }
 | |
| }
 | |
| 
 | |
| module.exports = Pool;
 |