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

Commit 003aabcc by Andrew Schmadel Committed by Sushant

fix(mysql/mariadb): treat deadlocked transactions as rollback (#11074)

1 parent 4e0ce386
...@@ -34,10 +34,10 @@ class Query extends AbstractQuery { ...@@ -34,10 +34,10 @@ class Query extends AbstractQuery {
run(sql, parameters) { run(sql, parameters) {
this.sql = sql; this.sql = sql;
const { connection } = this; const { connection, options } = this;
const showWarnings = this.sequelize.options.showWarnings const showWarnings = this.sequelize.options.showWarnings
|| this.options.showWarnings; || options.showWarnings;
const complete = this._logQuery(sql, debug); const complete = this._logQuery(sql, debug);
...@@ -56,6 +56,11 @@ class Query extends AbstractQuery { ...@@ -56,6 +56,11 @@ class Query extends AbstractQuery {
return results; return results;
}) })
.catch(err => { .catch(err => {
// MariaDB automatically rolls-back transactions in the event of a deadlock
if (options.transaction && err.errno === 1213) {
options.transaction.finished = 'rollback';
}
complete(); complete();
err.sql = sql; err.sql = sql;
......
...@@ -29,10 +29,10 @@ class Query extends AbstractQuery { ...@@ -29,10 +29,10 @@ class Query extends AbstractQuery {
run(sql, parameters) { run(sql, parameters) {
this.sql = sql; this.sql = sql;
const { connection } = this; const { connection, options } = this;
//do we need benchmark for this query execution //do we need benchmark for this query execution
const showWarnings = this.sequelize.options.showWarnings || this.options.showWarnings; const showWarnings = this.sequelize.options.showWarnings || options.showWarnings;
const complete = this._logQuery(sql, debug); const complete = this._logQuery(sql, debug);
...@@ -41,6 +41,10 @@ class Query extends AbstractQuery { ...@@ -41,6 +41,10 @@ class Query extends AbstractQuery {
complete(); complete();
if (err) { if (err) {
// MySQL automatically rolls-back transactions in the event of a deadlock
if (options.transaction && err.errno === 1213) {
options.transaction.finished = 'rollback';
}
err.sql = sql; err.sql = sql;
reject(this.formatError(err)); reject(this.formatError(err));
......
...@@ -1417,7 +1417,8 @@ class QueryInterface { ...@@ -1417,7 +1417,8 @@ class QueryInterface {
options = Object.assign({}, options, { options = Object.assign({}, options, {
transaction: transaction.parent || transaction, transaction: transaction.parent || transaction,
supportsSearchPath: false supportsSearchPath: false,
completesTransaction: true
}); });
const sql = this.QueryGenerator.commitTransactionQuery(transaction); const sql = this.QueryGenerator.commitTransactionQuery(transaction);
...@@ -1435,7 +1436,8 @@ class QueryInterface { ...@@ -1435,7 +1436,8 @@ class QueryInterface {
options = Object.assign({}, options, { options = Object.assign({}, options, {
transaction: transaction.parent || transaction, transaction: transaction.parent || transaction,
supportsSearchPath: false supportsSearchPath: false,
completesTransaction: true
}); });
options.transaction.name = transaction.parent ? transaction.name : undefined; options.transaction.name = transaction.parent ? transaction.name : undefined;
const sql = this.QueryGenerator.rollbackTransactionQuery(transaction); const sql = this.QueryGenerator.rollbackTransactionQuery(transaction);
......
...@@ -614,17 +614,22 @@ class Sequelize { ...@@ -614,17 +614,22 @@ class Sequelize {
[sql, bindParameters] = this.dialect.Query.formatBindParameters(sql, options.bind, this.options.dialect); [sql, bindParameters] = this.dialect.Query.formatBindParameters(sql, options.bind, this.options.dialect);
} }
const checkTransaction = () => {
if (options.transaction && options.transaction.finished && !options.completesTransaction) {
const error = new Error(`${options.transaction.finished} has been called on this transaction(${options.transaction.id}), you can no longer use it. (The rejected query is attached as the 'sql' property of this error)`);
error.sql = sql;
throw error;
}
};
const retryOptions = Object.assign({}, this.options.retry, options.retry || {}); const retryOptions = Object.assign({}, this.options.retry, options.retry || {});
return Promise.resolve(retry(() => Promise.try(() => { return Promise.resolve(retry(() => Promise.try(() => {
if (options.transaction === undefined && Sequelize._cls) { if (options.transaction === undefined && Sequelize._cls) {
options.transaction = Sequelize._cls.get('transaction'); options.transaction = Sequelize._cls.get('transaction');
} }
if (options.transaction && options.transaction.finished) {
const error = new Error(`${options.transaction.finished} has been called on this transaction(${options.transaction.id}), you can no longer use it. (The rejected query is attached as the 'sql' property of this error)`); checkTransaction();
error.sql = sql;
throw error;
}
return options.transaction return options.transaction
? options.transaction.connection ? options.transaction.connection
...@@ -632,6 +637,7 @@ class Sequelize { ...@@ -632,6 +637,7 @@ class Sequelize {
}).then(connection => { }).then(connection => {
const query = new this.dialect.Query(connection, this, options); const query = new this.dialect.Query(connection, this, options);
return this.runHooks('beforeQuery', options, query) return this.runHooks('beforeQuery', options, query)
.then(() => checkTransaction())
.then(() => query.run(sql, bindParameters)) .then(() => query.run(sql, bindParameters))
.finally(() => this.runHooks('afterQuery', options, query)) .finally(() => this.runHooks('afterQuery', options, query))
.finally(() => { .finally(() => {
......
...@@ -70,7 +70,6 @@ if (dialect === 'mysql') { ...@@ -70,7 +70,6 @@ if (dialect === 'mysql') {
reltype: 'child' reltype: 'child'
})); }));
}); });
}); });
}); });
} }
...@@ -366,6 +366,68 @@ if (current.dialect.supports.transactions) { ...@@ -366,6 +366,68 @@ if (current.dialect.supports.transactions) {
}); });
}); });
if (dialect === 'mysql' || dialect === 'mariadb') {
describe('deadlock handling', () => {
it('should treat deadlocked transaction as rollback', function() {
const Task = this.sequelize.define('task', {
id: {
type: Sequelize.INTEGER,
primaryKey: true
}
});
// This gets called twice simultaneously, and we expect at least one of the calls to encounter a
// deadlock (which effectively rolls back the active transaction).
// We only expect createTask() to insert rows if a transaction is active. If deadlocks are handled
// properly, it should only execute a query if we're actually inside a real transaction. If it does
// execute a query, we expect the newly-created rows to be destroyed when we forcibly rollback by
// throwing an error.
// tl;dr; This test is designed to ensure that this function never inserts and commits a new row.
const update = (from, to) => this.sequelize.transaction(transaction => {
return Task.findAll({
where: {
id: {
[Sequelize.Op.eq]: from
}
},
lock: 'UPDATE',
transaction
})
.then(() => Promise.delay(10))
.then(() => {
return Task.update({ id: to }, {
where: {
id: {
[Sequelize.Op.ne]: to
}
},
lock: transaction.LOCK.UPDATE,
transaction
});
})
.catch(e => { console.log(e.message); })
.then(() => Task.create({ id: 2 }, { transaction }))
.catch(e => { console.log(e.message); })
.then(() => { throw new Error('Rollback!'); });
}).catch(() => {});
return this.sequelize.sync({ force: true })
.then(() => Task.create({ id: 0 }))
.then(() => Task.create({ id: 1 }))
.then(() => Promise.all([
update(1, 0),
update(0, 1)
]))
.then(() => {
return Task.count().then(count => {
// If we were actually inside a transaction when we called `Task.create({ id: 2 })`, no new rows should be added.
expect(count).to.equal(2, 'transactions were fully rolled-back, and no new rows were added');
});
});
});
});
}
if (dialect === 'sqlite') { if (dialect === 'sqlite') {
it('provides persistent transactions', () => { it('provides persistent transactions', () => {
const sequelize = new Support.Sequelize('database', 'username', 'password', { dialect: 'sqlite' }), const sequelize = new Support.Sequelize('database', 'username', 'password', { dialect: 'sqlite' }),
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!