不要怂,就是干,撸起袖子干!

Commit 6388507e by Pedro Augusto de Paula Barbosa Committed by GitHub

fix(mysql): release connection on deadlocks (#13102)

* test(mysql, mariadb): improve transaction tests

- Greatly improve test for `SELECT ... LOCK IN SHARE MODE`
- Greatly improve test for deadlock handling

* fix(mysql): release connection on deadlocks

This is a follow-up for a problem not covered by #12841.

* refactor(mariadb): `query.js` similar to mysql's

* Update comments with a reference to this PR
1 parent ced4dc78
...@@ -7,6 +7,7 @@ const DataTypes = require('../../data-types'); ...@@ -7,6 +7,7 @@ const DataTypes = require('../../data-types');
const { logger } = require('../../utils/logger'); const { logger } = require('../../utils/logger');
const ER_DUP_ENTRY = 1062; const ER_DUP_ENTRY = 1062;
const ER_DEADLOCK = 1213;
const ER_ROW_IS_REFERENCED = 1451; const ER_ROW_IS_REFERENCED = 1451;
const ER_NO_REFERENCED_ROW = 1452; const ER_NO_REFERENCED_ROW = 1452;
...@@ -46,40 +47,25 @@ class Query extends AbstractQuery { ...@@ -46,40 +47,25 @@ class Query extends AbstractQuery {
try { try {
results = await connection.query(this.sql, parameters); results = await connection.query(this.sql, parameters);
complete(); } catch (error) {
if (options.transaction && error.errno === ER_DEADLOCK) {
// Log warnings if we've got them. // MariaDB automatically rolls-back transactions in the event of a deadlock.
if (showWarnings && results && results.warningStatus > 0) { // However, we still initiate a manual rollback to ensure the connection gets released - see #13102.
await this.logWarnings(results);
}
} catch (err) {
// MariaDB automatically rolls-back transactions in the event of a
// deadlock.
//
// Even though we shouldn't need to do this, we initiate a manual
// rollback. Without the rollback, the next transaction using the
// connection seems to retain properties of the previous transaction
// (e.g. isolation level) and not work as expected.
//
// For example (in our tests), a follow-up READ_COMMITTED transaction
// doesn't work as expected unless we explicitly rollback the
// transaction: it would fail to read a value inserted outside of that
// transaction.
if (options.transaction && err.errno === 1213) {
try { try {
await options.transaction.rollback(); await options.transaction.rollback();
} catch (err) { } catch (error_) {
// Ignore errors - since MariaDB automatically rolled back, we're // Ignore errors - since MariaDB automatically rolled back, we're
// not that worried about this redundant rollback failing. // not that worried about this redundant rollback failing.
} }
options.transaction.finished = 'rollback'; options.transaction.finished = 'rollback';
} }
error.sql = sql;
error.parameters = parameters;
throw this.formatError(error);
} finally {
complete(); complete();
err.sql = sql;
err.parameters = parameters;
throw this.formatError(err);
} }
if (showWarnings && results && results.warningStatus > 0) { if (showWarnings && results && results.warningStatus > 0) {
......
...@@ -6,6 +6,7 @@ const _ = require('lodash'); ...@@ -6,6 +6,7 @@ const _ = require('lodash');
const { logger } = require('../../utils/logger'); const { logger } = require('../../utils/logger');
const ER_DUP_ENTRY = 1062; const ER_DUP_ENTRY = 1062;
const ER_DEADLOCK = 1213;
const ER_ROW_IS_REFERENCED = 1451; const ER_ROW_IS_REFERENCED = 1451;
const ER_NO_REFERENCED_ROW = 1452; const ER_NO_REFERENCED_ROW = 1452;
...@@ -57,19 +58,27 @@ class Query extends AbstractQuery { ...@@ -57,19 +58,27 @@ class Query extends AbstractQuery {
.setMaxListeners(100); .setMaxListeners(100);
}); });
} }
} catch (err) { } catch (error) {
// MySQL automatically rolls-back transactions in the event of a deadlock if (options.transaction && error.errno === ER_DEADLOCK) {
if (options.transaction && err.errno === 1213) { // MySQL automatically rolls-back transactions in the event of a deadlock.
// However, we still initiate a manual rollback to ensure the connection gets released - see #13102.
try {
await options.transaction.rollback();
} catch (error_) {
// Ignore errors - since MySQL automatically rolled back, we're
// not that worried about this redundant rollback failing.
}
options.transaction.finished = 'rollback'; options.transaction.finished = 'rollback';
} }
err.sql = sql; error.sql = sql;
err.parameters = parameters; error.parameters = parameters;
throw this.formatError(err); throw this.formatError(error);
} finally {
complete();
} }
complete();
if (showWarnings && results && results.warningStatus > 0) { if (showWarnings && results && results.warningStatus > 0) {
await this.logWarnings(results); await this.logWarnings(results);
} }
......
...@@ -80,6 +80,7 @@ ...@@ -80,6 +80,7 @@
"nyc": "^15.0.0", "nyc": "^15.0.0",
"p-map": "^4.0.0", "p-map": "^4.0.0",
"p-props": "^4.0.0", "p-props": "^4.0.0",
"p-settle": "^4.1.1",
"p-timeout": "^4.0.0", "p-timeout": "^4.0.0",
"pg": "^8.2.1", "pg": "^8.2.1",
"pg-hstore": "^2.x", "pg-hstore": "^2.x",
......
...@@ -18,8 +18,8 @@ describe(Support.getTestDialectTeaser('Replication'), () => { ...@@ -18,8 +18,8 @@ describe(Support.getTestDialectTeaser('Replication'), () => {
this.sequelize = Support.getSequelizeInstance(null, null, null, { this.sequelize = Support.getSequelizeInstance(null, null, null, {
replication: { replication: {
write: Support.getConnectionOptions(), write: Support.getConnectionOptionsWithoutPool(),
read: [Support.getConnectionOptions()] read: [Support.getConnectionOptionsWithoutPool()]
} }
}); });
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { isDeepStrictEqual } = require('util');
const _ = require('lodash'); const _ = require('lodash');
const Sequelize = require('../index'); const Sequelize = require('../index');
const Config = require('./config/config'); const Config = require('./config/config');
...@@ -119,11 +120,10 @@ const Support = { ...@@ -119,11 +120,10 @@ const Support = {
return this.getSequelizeInstance(config.database, config.username, config.password, sequelizeOptions); return this.getSequelizeInstance(config.database, config.username, config.password, sequelizeOptions);
}, },
getConnectionOptions() { getConnectionOptionsWithoutPool() {
const config = Config[this.getTestDialect()]; // Do not break existing config object - shallow clone before `delete config.pool`
const config = { ...Config[this.getTestDialect()] };
delete config.pool; delete config.pool;
return config; return config;
}, },
...@@ -207,6 +207,10 @@ const Support = { ...@@ -207,6 +207,10 @@ const Support = {
return `[${dialect.toUpperCase()}] ${moduleName}`; return `[${dialect.toUpperCase()}] ${moduleName}`;
}, },
getPoolMax() {
return Config[this.getTestDialect()].pool.max;
},
expectsql(query, assertions) { expectsql(query, assertions) {
const expectations = assertions.query || assertions; const expectations = assertions.query || assertions;
let expectation = expectations[Support.sequelize.dialect.name]; let expectation = expectations[Support.sequelize.dialect.name];
...@@ -234,6 +238,10 @@ const Support = { ...@@ -234,6 +238,10 @@ const Support = {
const bind = assertions.bind[Support.sequelize.dialect.name] || assertions.bind['default'] || assertions.bind; const bind = assertions.bind[Support.sequelize.dialect.name] || assertions.bind['default'] || assertions.bind;
expect(query.bind).to.deep.equal(bind); expect(query.bind).to.deep.equal(bind);
} }
},
isDeepEqualToOneOf(actual, expectedOptions) {
return expectedOptions.some(expected => isDeepStrictEqual(actual, expected));
} }
}; };
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!