diff --git a/lib/connection_config.js b/lib/connection_config.js index a18b30b330..d5d4f7b9a0 100644 --- a/lib/connection_config.js +++ b/lib/connection_config.js @@ -65,7 +65,8 @@ const validOptions = { idleTimeout: 1, Promise: 1, queueLimit: 1, - waitForConnections: 1 + waitForConnections: 1, + acquireTimeout: 1 }; class ConnectionConfig { diff --git a/lib/pool.js b/lib/pool.js index 1b4993ff1a..6fbd86553a 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -56,7 +56,7 @@ class Pool extends EventEmitter { config: this.config.connectionConfig }); this._allConnections.push(connection); - return connection.connect(err => { + return connection.connect((err) => { if (this._closed) { return cb(new Error('Pool is closed.')); } @@ -78,6 +78,24 @@ class Pool extends EventEmitter { return cb(new Error('Queue limit reached.')); } this.emit('enqueue'); + + if (this.config.acquireTimeout > 0) { + // eslint-disable-next-line prefer-const + let timer; + + const wrappedCb = (err, connection) => { + clearTimeout(timer); + cb(err, connection); + }; + + timer = setTimeout(() => { + spliceConnection(this._connectionQueue, wrappedCb); + cb(new Error('Timeout acquiring connection')); + }, this.config.acquireTimeout); + + return this._connectionQueue.push(wrappedCb); + } + return this._connectionQueue.push(cb); } @@ -102,7 +120,7 @@ class Pool extends EventEmitter { this._closed = true; clearTimeout(this._removeIdleTimeoutConnectionsTimer); if (typeof cb !== 'function') { - cb = function(err) { + cb = function (err) { if (err) { throw err; } @@ -111,7 +129,7 @@ class Pool extends EventEmitter { let calledBack = false; let closedConnections = 0; let connection; - const endCB = function(err) { + const endCB = function (err) { if (calledBack) { return; } @@ -139,7 +157,8 @@ class Pool extends EventEmitter { this.config.connectionConfig ); if (typeof cmdQuery.namedPlaceholders === 'undefined') { - cmdQuery.namedPlaceholders = this.config.connectionConfig.namedPlaceholders; + cmdQuery.namedPlaceholders = + this.config.connectionConfig.namedPlaceholders; } this.getConnection((err, conn) => { if (err) { diff --git a/lib/pool_config.js b/lib/pool_config.js index 0a4a26097f..3d8f01a5ce 100644 --- a/lib/pool_config.js +++ b/lib/pool_config.js @@ -15,6 +15,9 @@ class PoolConfig { this.connectionLimit = isNaN(options.connectionLimit) ? 10 : Number(options.connectionLimit); + this.acquireTimeout = Number.isInteger(options.acquireTimeout) && options.acquireTimeout >= 0 + ? options.acquireTimeout + : 10000; this.maxIdle = isNaN(options.maxIdle) ? this.connectionLimit : Number(options.maxIdle); diff --git a/test/integration/promise-wrappers/test-promise-wrappers.test.cjs b/test/integration/promise-wrappers/test-promise-wrappers.test.cjs index 6f3a2b1f45..53d4118b0f 100644 --- a/test/integration/promise-wrappers/test-promise-wrappers.test.cjs +++ b/test/integration/promise-wrappers/test-promise-wrappers.test.cjs @@ -75,6 +75,47 @@ function testErrors() { }); } +function testPoolAcquireTimeout() { + (async () => { + const ACQUIRE_TIMEOUT = 500; + const pool = createPool({ + ...config, + connectionLimit: 2, + acquireTimeout: ACQUIRE_TIMEOUT, + }); + + const c1 = await pool.getConnection(); + + assert.ok(c1); + + const c2 = await pool.getConnection(); + + assert.ok(c2); + + const c3StartedAt = Date.now(); + try { + await pool.getConnection(); + assert.fail('should not reach here'); + } catch (e) { + assert.equal(Date.now() - c3StartedAt >= ACQUIRE_TIMEOUT, true); + assert.equal(e.message, 'Timeout acquiring connection'); + } + + c2.release(); + + const c4 = await pool.getConnection(); + + c1.release(); + c4.release(); + + await pool.end(); + + assert.ok(c4, 'acquireTimeout is working correctly'); + })().catch((err) => { + assert.fail(err); + }); +} + function testObjParams() { let connResolved; createConnection(config) @@ -473,6 +514,7 @@ testChangeUser(); testConnectionProperties(); testPoolConnectionDestroy(); testPromiseLibrary(); +testPoolAcquireTimeout(); process.on('exit', () => { assert.equal(doneCalled, true, 'done not called'); diff --git a/test/integration/test-pool-acquire-timeout.test.cjs b/test/integration/test-pool-acquire-timeout.test.cjs new file mode 100644 index 0000000000..c4e89189c3 --- /dev/null +++ b/test/integration/test-pool-acquire-timeout.test.cjs @@ -0,0 +1,50 @@ +'use strict'; + +const mysql = require('../..'); +const assert = require('assert'); +const common = require('../common.test.cjs'); + +const poolConfig = common.getConfig(); + +const ACQUIRE_TIMEOUT = 500; +const pool = new mysql.createPool({ + ...poolConfig, + acquireTimeout: ACQUIRE_TIMEOUT, + connectionLimit: 2, +}); + +pool.getConnection((err, c1) => { + assert.equal(!!c1, true); + assert.ifError(err); + + pool.getConnection((err, c2) => { + assert.ifError(err); + assert.equal(!!c2, true); + + const C3_STARTED_AT = Date.now(); + + pool.getConnection((e3, c3) => { + const C3_DONE_AT = Date.now(); + assert.equal(C3_DONE_AT - C3_STARTED_AT >= ACQUIRE_TIMEOUT, true); + assert.equal(C3_DONE_AT - C3_STARTED_AT < ACQUIRE_TIMEOUT * 2, true); + + assert.notEqual(e3, null); + assert.equal( + e3.message, + 'Timeout acquiring connection', + 'Acquire timeout error message is correct', + ); + assert.equal(!c3, true); + c1.release(); + + pool.getConnection((e4, c4) => { + assert.equal(e4, null); + assert.equal(!!c4, true); + + c4.release(); + c2.release(); + pool.end(); + }); + }); + }); +}); diff --git a/typings/mysql/lib/Pool.d.ts b/typings/mysql/lib/Pool.d.ts index 042495e71f..575c5200db 100644 --- a/typings/mysql/lib/Pool.d.ts +++ b/typings/mysql/lib/Pool.d.ts @@ -22,6 +22,13 @@ export interface PoolOptions extends ConnectionOptions { */ connectionLimit?: number; + /** + * The maximum time (in milliseconds) to wait for getting a connection from the pool. (Default: 10 seconds) + * + * @default 10000 + */ + acquireTimeout?: number; + /** * The maximum number of idle connections. (Default: same as `connectionLimit`) */