Open sergru opened 1 year ago
hm, promise pool.end() just delegates it to callback implementation, which in turn enqueues end command to every connection which is only executed after all previous command in the connection queue are finished.
I'll try to reproduce, don't see anything wrong on your side
I found the bug. Here is a code snippet from pool.js of my version.
getConnection(cb) { // <----------------------------- pool.js:37
...
this._allConnections.push(connection); // <------ pool.js:54
return connection.connect(err => {
if (this._closed) { // <----------------------- pool.js:56
return cb(new Error('Pool is closed.'));
}
...
end(cb) {
this._closed = true; // <-------------------------- pool.js:98
if (typeof cb !== 'function') {
Using my test as a workflow:
query('SELECT 1')
calls pool.getConnection(cb)
:pool.js:37
. At line pool.js:54
pool is still open (this._closed = false). At this point connection.connect(cb)
starts and its callback is waiting to be called.stop()
calls pool.end(cb)
:pool.js:97
and its first instruction is this._closed = true;
:pool.js:98
marking pool as closed.return cb(new Error('Pool is closed.'))
:pool.js:57
So, actually, the pool is marked as closed immediately and unconditionally when call pool.end()
that causing an errors in asynchronous parts (callbacks) of currently executing connections.
Usually, in situations like this I use tree-state flag: 0 - open, 1 - closing, 2 - closed. The 'closing' state allows to finish the currently running tasks (in your case, connections), but prohibits to run from queue waiting tasks or to add to queue new ones. This approach requires extra work to properly handle the state switching, but it's worth it.
Hope, this will help.
thanks for tracking down to the root cause @sergru
the intention of _closing
flag is to help preventing from adding commands to a connection that already has COM_QUIT
command in its queue
The more I think about possible scenarios the more difficult I think is to satisfy all of them. I'll try to post examples below
btw simple
start();
query('SELECT 1').then(stop)
solve the problem ( on a consumer side ). Still, a bit surprising behaviour from the driver in your original example
example in my head:
import mysql from './promise.js';
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'test',
connectionLimit: 2,
});
async function endAfterDelay(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('calling pool.end()');
pool.end()
resolve();
}, ms);
});
}
const result = await Promise.allSettled([
pool.query('select sleep(3)'),
pool.query('select sleep(5)'),
pool.query('select sleep(5)'),
endAfterDelay(4000),
]);
console.log(result);
what would be the expected behaviour here ( also with different values for connectionLimit
)?
Unlike regular connection where .query
is "add enough information to be able to send COM_QUERY command to the server when there is no more command in front of me", the pool.query is "wait until there is a free connection, and then do .query() on it"
Thanks for response.
query('SELECT 1').then(stop)
works for test but unfortunately will not help in my project.
In my real scenario pool.end()
is called on the app stop event (from UI button or SIGINT and SIGTERM events) while multiple connection.query()
are simultaneously executing. I need that pool.end()
starts its work only when all connection.query()
finished.
I think I can use process.nextTick()
to stop accepting queries, wait all connections finished and then call poop.end()
. So this will be my app closing
state implementation.
can you describe a behaviour on a driver side that would have a better ergonomics for you?
Right now what we have is: pool.query()
might potentially fail for a number of reasons and you need to be prepared to handle them. One possible reason is: sometime between the pool.query()
and connection is actually free pool.end()
is called
hm, another problem, even worse then the one you described:
if you call pool.query()
after calling pool.end()
it never resolves.
I think the correct behaviour is to notify all pool connections in the wait queue with "Pool is closed" error
hm, not sure if this actually the case. Looking at the code there is a check for that
Sorry, I've been interrupted for a while.
In my example:
query('SELECT 1');
stop();
the execution sequence is:
Pool::getConnection
that checks this._closed
which is false, line 38:
https://github.com/sidorares/node-mysql2/blob/dd4d1d6b6086c763f54b3b006ec6acf4b10bae4c/lib/pool.js#L37-L40PromisePool::end()
that sets this._closed
to true, line 98
https://github.com/sidorares/node-mysql2/blob/dd4d1d6b6086c763f54b3b006ec6acf4b10bae4c/lib/pool.js#L97-L98this._closed
already true, line 56:
https://github.com/sidorares/node-mysql2/blob/dd4d1d6b6086c763f54b3b006ec6acf4b10bae4c/lib/pool.js#L55-L58
and the app throws exception.Here is my simplified work scenario is:
(1) - When app starts I call start()
to initialize the pool (PromisePool).
(2) - Then on each http request I call query({sql, params})
for connection gotten from pool. http handles it in separate threads.
(3) - Then on arbitrary moment I call stop()
from UI or by SIGINT or SIGTERM events and expect graceful finish of all running query() before exit.
But exception happened instead.
I can make repro.js for this scenario if you like. But the simple test I sent first reproduce the exact problem.
I took the liberty to implement the workaround to my issue. Here is my changes in mysql2/promise.js, mysql2/lib/pool.js, and test script repro-exception.js . In this implementation there are no exceptions Error: Pool is closed anymore, and all asynchronous calls of query()
and stop()
executed as expected. The log of all calls added for debug purposes.
Files from mysql2:
Test file:
Kill .txt extra extension when download files. Somehow .js files are not allowed to be dropped in github.
Search for ADDED:
to see all my changes in 2 mysql2 files.
I am working on server that handles several requests asynchronously. Found the problem when close the app gracefully while mysql query is still running. To stop the app, I call pool.end() created by mysql.createPool({}).promise();. But looks like, pool.end() just closes the pool without checking that some connections from the pool are still running. Connection query throws the exception with Error: Pool is closed. Or I do something wrong or inappropriate. Any suggestions?
Exception is:
Here is simple test to reproduce:
My code is bit more sophisticated. I can have several complex queries executing in the pool and some queries waiting in a pool queue. But the root of the problem is the same.
If I wrap stop() in setTimeout() with short delay:
the test finishes normally with exit code 0.
I am using: