Skip to content

Commit 53c7c81

Browse files
committed
Release connections after connection time out
If a connection timeout was thrown, the connection would remain queued and ultimately consume a connection from the pool. The connection wasn't used, so was permanently removed from the pool. After enough timeouts, the pool would be fully consumed and no connections would be released.
1 parent 945717d commit 53c7c81

File tree

2 files changed

+73
-10
lines changed

2 files changed

+73
-10
lines changed

index.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,23 @@ function kill(connection, query) {
1818
query('KILL ' + connection.escape(connection.connection.threadId));
1919
}
2020

21+
async function getConnectionWithTimeout(getConnection, timeoutMs) {
22+
const gettingConnection = getConnection();
23+
try {
24+
return await Promise.race([
25+
gettingConnection,
26+
timeout('connect', timeoutMs)
27+
]);
28+
} catch (err) {
29+
gettingConnection.then(conn => conn.release()).catch(() => {});
30+
throw err;
31+
}
32+
}
33+
2134
function queryWithTimeout(options, getConnection, query) {
2235
return async (firstArg, ...otherArgs) => {
23-
const connection = await Promise.race([
24-
getConnection(),
25-
timeout('connect', options.acquireTimeout)
26-
]);
36+
37+
const connection = await getConnectionWithTimeout(getConnection, options.acquireTimeout);
2738

2839
let queryTimeout = options.defaultQueryTimeout;
2940
if (typeof firstArg === 'object') {
@@ -61,4 +72,4 @@ async function connect({
6172
return pool;
6273
}
6374

64-
module.exports = { connect };
75+
module.exports = { connect };

test.js

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ describe('database', () => {
120120
});
121121
});
122122

123-
describe('when all connections are in use', () => {
124-
let queryError, queryDuration;
123+
describe('when querying while all connections are in use', () => {
124+
let queryError, queryDuration, connectionHog, queuedQuery;
125125
const acquireTimeout = 500;
126126

127127
beforeEach(async () => {
@@ -133,21 +133,73 @@ describe('database', () => {
133133
connectionLimit: 1
134134
});
135135

136-
db.query('DO SLEEP(5)'); // Hog the only available connection
136+
connectionHog = db.query('DO SLEEP(2)'); // Hog the only available connection
137+
138+
db.once('enqueue', () => queuedQuery = true);
139+
const start = new Date();
140+
try { await db.query('SELECT 1'); } // Query while connection is hogged
141+
catch (e) { queryError = e; }
142+
queryDuration = new Date() - start;
143+
});
144+
145+
it('times out getting a connection', () => {
146+
assert(queryError, 'query didn\'t error');
147+
assert.equal(queryError.message, 'Database connect timed out after 500ms');
148+
149+
assert.isAtLeast(queryDuration, acquireTimeout - 1); // Sometimes comes in early?!
150+
assert.isAtMost(queryDuration, acquireTimeout + 100); // 100ms grace
151+
});
152+
153+
it('makes the timed out connection available for use', async () => {
154+
assert.isTrue(queuedQuery, 'Timed out query was not queued');
155+
await connectionHog;
156+
assert.isDefined(await db.query('SELECT 1'));
157+
}).timeout(5000);
158+
});
159+
160+
describe('when acquiring the connection times out', () => {
161+
let queryError, badDbServer, queryDuration;
162+
const acquireTimeout = 500;
163+
164+
beforeEach(async () => {
165+
badDbServer = require('net').createServer().listen(3306);
166+
167+
db = await database.connect({ acquireTimeout });
137168

138169
const start = new Date();
139170
try { await db.query('SELECT 1'); }
140171
catch (e) { queryError = e; }
141172
queryDuration = new Date() - start;
142173
});
143174

144-
it('times out', async () => {
175+
afterEach(function (done) {
176+
this.timeout(10000);
177+
if (badDbServer.listening) { badDbServer.close(done); }
178+
else { done(); }
179+
});
180+
181+
it('times out', () => {
145182
assert(queryError, 'query didn\'t error');
146183
assert.equal(queryError.message, 'Database connect timed out after 500ms');
147184

148185
assert.isAtLeast(queryDuration, acquireTimeout - 1); // Sometimes comes in early?!
149186
assert.isAtMost(queryDuration, acquireTimeout + 100); // 100ms grace
150187
});
188+
189+
describe('when the connection then errors', () => {
190+
let connError;
191+
const unhandledError = err => connError = err;
192+
beforeEach(() => process.once('unhandledRejection', unhandledError));
193+
afterEach(() => process.off('unhandledRejection', unhandledError));
194+
195+
it('swallows the connection error', done => {
196+
badDbServer.close(() => {
197+
if (connError) { assert.fail(`Did not expect an error, but got '${connError}'`); }
198+
199+
done();
200+
});
201+
}).timeout(10000);
202+
});
151203
});
152204

153205
it('reuses connections', async () => {
@@ -161,4 +213,4 @@ describe('database', () => {
161213
const results = await db.query('SELECT 2');
162214
assert.isDefined(results);
163215
});
164-
});
216+
});

0 commit comments

Comments
 (0)