Skip to content

Commit 175293d

Browse files
committed
BUG#33761268: skip connection requests on unavailable pool
When a connection pool is full, any new connection request is retried within a sliding window of time until it reaches the time specified by the "queueTimeout" option. By default, the connection request will be retried forever, until the connection is effectively acquired. Currently, if the pool is closed in the meantime, all connection requests that have been queued will still be retried in the future, as long as the application process does not terminate. This is bad and might result in dangling sockets and formal MySQL server connections, which is a waste of resources and a potential source of memory leaks. This patch introduces the changes necessary to ensure all queued connection requests in a pool are skipped when the pool is closed or is in the process of being closed (where it should still no longer be available in order to avoid parallelism issues).
1 parent 9b738dd commit 175293d

File tree

5 files changed

+231
-20
lines changed

5 files changed

+231
-20
lines changed

lib/DevAPI/Client.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2021, Oracle and/or its affiliates.
2+
* Copyright (c) 2018, 2022, Oracle and/or its affiliates.
33
*
44
* This program is free software; you can redistribute it and/or modify
55
* it under the terms of the GNU General Public License, version 2.0, as
@@ -127,6 +127,13 @@ function Client (options = {}) {
127127

128128
return state.pool.getConnection()
129129
.then(con => {
130+
// If the connection does not exist, probably because
131+
// the pool was closed in the meantime, there is nothing
132+
// else to do.
133+
if (!con) {
134+
return;
135+
}
136+
130137
return session(con);
131138
});
132139
}

lib/DevAPI/ConnectionPool.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2021, Oracle and/or its affiliates.
2+
* Copyright (c) 2018, 2022, Oracle and/or its affiliates.
33
*
44
* This program is free software; you can redistribute it and/or modify
55
* it under the terms of the GNU General Public License, version 2.0, as
@@ -77,6 +77,9 @@ function ConnectionPool (options = {}) {
7777
// Contains the list of active connections i.e. that have not been
7878
// closed neither by the client nor the server.
7979
active: [],
80+
// Informs if the pool is avaiable or not. After it starts to be
81+
// destroyed, it should become unavailable.
82+
available: true,
8083
// Contains the list of connections that have expired i.e. where
8184
// maxIdleTime was exceeded or have been closed by the server.
8285
expired: [],
@@ -113,6 +116,7 @@ function ConnectionPool (options = {}) {
113116
*/
114117
create ({ active = [], expired = [], idle = [] } = {}) {
115118
state.active = active;
119+
state.available = true;
116120
state.expired = expired;
117121
state.idle = idle;
118122
state.tickets = active.length;
@@ -128,10 +132,13 @@ function ConnectionPool (options = {}) {
128132
* @returns {Promise}
129133
*/
130134
destroy () {
135+
// We should put the pool in a state that prevents queued
136+
// connection requests from being fulfilled.
137+
state.available = false;
131138
// Expired connections have already been destroyed by this point.
132139
return Promise.all(state.idle.concat(state.active).map(con => con.destroy()))
133140
.then(() => {
134-
this.reset();
141+
return this.reset();
135142
})
136143
.catch(err => {
137144
this.reset();
@@ -168,6 +175,16 @@ function ConnectionPool (options = {}) {
168175
getConnection (requestedAt = Date.now()) {
169176
return this.update()
170177
.then(() => {
178+
// If previous connection requests were queued and the
179+
// pool is closed or being closed, we should not try to
180+
// create, refurbish or re-use connections at the risk of
181+
// leaving a system socket open which will be not cleaned
182+
// up after the process finishes, leading to a resource
183+
// leak. There is nothing else left to do.
184+
if (!this.isAvailable()) {
185+
return;
186+
}
187+
171188
const elapsedTime = Date.now() - requestedAt;
172189
const queueTimeout = options.pooling.queueTimeout;
173190

@@ -247,6 +264,17 @@ function ConnectionPool (options = {}) {
247264
return state.idle;
248265
},
249266

267+
/**
268+
* Checks if the pool is being closed.
269+
* @private
270+
* @function
271+
* @name module:ConnectionPool#isAvailable
272+
* @returns {boolean}
273+
*/
274+
isAvailable () {
275+
return state.available;
276+
},
277+
250278
/**
251279
* Checks if a pool is full i.e. a total of "maxSize" number of
252280
* connections are currently active.
@@ -270,6 +298,9 @@ function ConnectionPool (options = {}) {
270298
*/
271299
reset () {
272300
state.active = [];
301+
// The original pool instance will never be available again.
302+
// A new one should be created by the client wrapper.
303+
state.available = false;
273304
state.expired = [];
274305
state.idle = [];
275306
state.tickets = 0;

test/functional/default/connection/pool.js

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, Oracle and/or its affiliates.
2+
* Copyright (c) 2021, 2022, Oracle and/or its affiliates.
33
*
44
* This program is free software; you can redistribute it and/or modify
55
* it under the terms of the GNU General Public License, version 2.0, as
@@ -657,6 +657,24 @@ describe('connection pool', () => {
657657
return pool.close();
658658
});
659659
});
660+
661+
it('queued connection requests are not fullfilled', () => {
662+
const poolingConfig = Object.assign({}, config, baseConfig);
663+
const pool = mysqlx.getClient(poolingConfig, { pooling: { maxSize: 2 } });
664+
665+
return Promise.all([pool.getSession(), pool.getSession()])
666+
.then(() => {
667+
return new Promise(resolve => {
668+
// We need to ensure the test only finishes after the
669+
// queued connection request is processed.
670+
pool.getSession().then(resolve);
671+
pool.close();
672+
});
673+
})
674+
.then(session => {
675+
return expect(session).to.not.exist;
676+
});
677+
});
660678
});
661679

662680
context('when acquiring connections in parallel', () => {
@@ -675,6 +693,7 @@ describe('connection pool', () => {
675693
const maxSize = totalRequestsInQueue / 2;
676694
const queueTimeout = 500;
677695
const pool = mysqlx.getClient(poolingConfig, { pooling: { enabled: true, maxSize, queueTimeout } });
696+
const connections = [...Array(totalRequestsInQueue)];
678697

679698
const connectAndWaitBeforeClose = () => {
680699
return pool.getSession()
@@ -691,9 +710,7 @@ describe('connection pool', () => {
691710
});
692711
};
693712

694-
const work = [...Array(totalRequestsInQueue)].map(() => connectAndWaitBeforeClose());
695-
696-
return Promise.all(work)
713+
return Promise.all(connections.map(() => connectAndWaitBeforeClose()))
697714
.then(() => {
698715
return pool.close();
699716
});
@@ -704,6 +721,7 @@ describe('connection pool', () => {
704721
const maxSize = totalRequestsInQueue / 2;
705722
const queueTimeout = 500;
706723
const pool = mysqlx.getClient(poolingConfig, { pooling: { enabled: true, maxSize, queueTimeout } });
724+
const connections = [...Array(totalRequestsInQueue)];
707725
const error = `Could not retrieve a connection from the pool. Timeout of ${queueTimeout} ms was exceeded.`;
708726

709727
const connectAndWaitBeforeClose = () => {
@@ -721,9 +739,7 @@ describe('connection pool', () => {
721739
});
722740
};
723741

724-
const work = [...Array(totalRequestsInQueue)].map(() => connectAndWaitBeforeClose());
725-
726-
return Promise.all(work)
742+
return Promise.all(connections.map(() => connectAndWaitBeforeClose()))
727743
.then(() => {
728744
return expect.fail();
729745
})
@@ -799,4 +815,53 @@ describe('connection pool', () => {
799815
});
800816
});
801817
});
818+
819+
context('when the pool has been closed', () => {
820+
it('fulfills new connection requests', () => {
821+
const poolingConfig = Object.assign({}, config, baseConfig);
822+
const pool = mysqlx.getClient(poolingConfig, { pooling: { maxSize: 2 } });
823+
824+
return Promise.all([pool.getSession(), pool.getSession()])
825+
.then(() => {
826+
return pool.close();
827+
})
828+
.then(() => {
829+
return pool.getSession();
830+
})
831+
.then(session => {
832+
// eslint-disable-next-line no-unused-expressions
833+
expect(session).to.exist;
834+
return pool.close();
835+
});
836+
});
837+
838+
it('queued connection requests are not fullfilled', () => {
839+
const poolingConfig = Object.assign({}, config, baseConfig);
840+
const pool = mysqlx.getClient(poolingConfig, { pooling: { maxSize: 2 } });
841+
842+
return Promise.all([pool.getSession(), pool.getSession()])
843+
.then(() => {
844+
return new Promise(resolve => {
845+
// We need to ensure the test only finishes after the
846+
// queued connection request is processed.
847+
pool.getSession().then(resolve);
848+
pool.close();
849+
});
850+
})
851+
.then(() => {
852+
return Promise.all([pool.getSession(), pool.getSession()]);
853+
})
854+
.then(() => {
855+
return new Promise(resolve => {
856+
// We need to ensure the test only finishes after the
857+
// queued connection request is processed.
858+
pool.getSession().then(resolve);
859+
pool.close();
860+
});
861+
})
862+
.then(session => {
863+
return expect(session).to.not.exist;
864+
});
865+
});
866+
});
802867
});

test/unit/DevAPI/Client.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2021, Oracle and/or its affiliates.
2+
* Copyright (c) 2018, 2022, Oracle and/or its affiliates.
33
*
44
* This program is free software; you can redistribute it and/or modify
55
* it under the terms of the GNU General Public License, version 2.0, as
@@ -89,6 +89,21 @@ describe('DevAPI Client', () => {
8989
expect(res).to.equal('qux');
9090
});
9191
});
92+
93+
it('does not create a session if the pool is not able to return a valid connection', () => {
94+
const options = { foo: 'bar', pooling: { enabled: true } };
95+
const create = td.function();
96+
const getConnection = td.function();
97+
98+
td.when(pool(options)).thenReturn({ create });
99+
td.when(create()).thenReturn({ getConnection });
100+
td.when(getConnection()).thenResolve('baz');
101+
102+
return client(options).getSession()
103+
.then(res => {
104+
return expect(res).to.not.exist;
105+
});
106+
});
92107
});
93108

94109
context('close()', () => {

0 commit comments

Comments
 (0)