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

Commit 680b4328 by Mick Hansen

Merge pull request #5263 from EToreo/master

Added retry and options to retry when queries fail with SQL_BUSY state.
2 parents eca0a902 9ad93d24
# Future
- [ADDED] Support silent: true in bulk update [#5200](https://github.com/sequelize/sequelize/issues/5200)
- [ADDED] `retry` object now part of global settings and can be overridden per call. The default is 5 retries with a backoff function. `retry` object can be passed to options with max: 0 to turn off this behavior.
- [ADDED] Sqlite now retries database queries that return SQL_BUSY as the status.
- [FIXED] Postgres destroy with `where` fails on JSONB data [#5092](https://github.com/sequelize/sequelize/issues/5092)
# 3.17.3
......
......@@ -350,7 +350,7 @@ var QueryGenerator = {
return 'SAVEPOINT ' + this.quoteIdentifier(transaction.name) + ';';
}
return 'BEGIN ' + transaction.options.type + ' TRANSACTION;';
return 'BEGIN ' + transaction.options.type + ' TRANSACTION;';
},
setAutocommitQuery: function() {
......
......@@ -2,6 +2,7 @@
var url = require('url')
, Path = require('path')
, retry = require('retry-as-promised')
, Utils = require('./utils')
, Model = require('./model')
, DataTypes = require('./data-types')
......@@ -81,6 +82,9 @@ var url = require('url')
* @param {Boolean} [options.quoteIdentifiers=true] Set to `false` to make table names and attributes case-insensitive on Postgres and skip double quoting of them.
* @param {String} [options.transactionType='DEFERRED'] Set the default transaction type. See `Sequelize.Transaction.TYPES` for possible options. Sqlite only.
* @param {String} [options.isolationLevel='REPEATABLE_READ'] Set the default transaction isolation level. See `Sequelize.Transaction.ISOLATION_LEVELS` for possible options.
* @param {Object} [options.retry] Set of flags that control when a query is automatically retried.
* @param {Array} [options.retry.match] Only retry a query if the error matches one of these strings.
* @param {Integer} [options.retry.max] How many times a failing query is automatically retried. Set to 0 to disable retrying on SQL_BUSY error.
* @param {Boolean} [options.typeValidation=false] Run built in type validators on insert and update, e.g. validate that arguments passed to integer fields are integer-like.
* @param {Boolean} [options.benchmark=false] Print query execution time in milliseconds when logging SQL.
*/
......@@ -145,6 +149,7 @@ var Sequelize = function(database, username, password, options) {
pool: {},
quoteIdentifiers: true,
hooks: {},
retry: {max: 5, match: ['SQLITE_BUSY: database is locked']},
transactionType: Transaction.TYPES.DEFERRED,
isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ,
databaseVersion: 0,
......@@ -682,6 +687,9 @@ Sequelize.prototype.import = function(path) {
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Instance} [options.instance] A sequelize instance used to build the return instance
* @param {Model} [options.model] A sequelize model used to build the returned model instances (used to be called callee)
* @param {Object} [options.retry] Set of flags that control when a query is automatically retried.
* @param {Array} [options.retry.match] Only retry a query if the error matches one of these strings.
* @param {Integer} [options.retry.max] How many times a failing query is automatically retried.
* @param {String} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only)
* @param {Boolean} [options.supportsSearchPath] If false do not prepend the query with the search_path (Postgres only)
* @param {Object} [options.mapToModel=false] Map returned fields to model's fields if `options.model` or `options.instance` is present. Mapping will occur before building the model instance.
......@@ -798,10 +806,12 @@ Sequelize.prototype.query = function(sql, options) {
).then(function (connection) {
var query = new self.dialect.Query(connection, self, options);
return query.run(sql, bindParameters).finally(function() {
if (options.transaction) return;
return self.connectionManager.releaseConnection(connection);
});
return retry(function() {
return query.run(sql, bindParameters).finally(function() {
if (options.transaction) return;
return self.connectionManager.releaseConnection(connection);
});
}, Utils._.extend(self.options.retry, options.retry || {}));
}).finally(function () {
if (self.test.$trackRunningQueries) {
self.test.$runningQueries--;
......
......@@ -41,6 +41,7 @@
"moment": "^2.11.1",
"moment-timezone": "^0.5.0",
"node-uuid": "~1.4.4",
"retry-as-promised": "^2.0.0",
"semver": "^5.0.1",
"shimmer": "1.1.0",
"toposort-class": "^1.0.1",
......
......@@ -264,6 +264,49 @@ describe(Support.getTestDialectTeaser('Transaction'), function() {
});
});
}
if (dialect === 'sqlite') {
it('automatically retries on SQLITE_BUSY failure', function () {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING });
return User.sync({ force: true }).then(function() {
var newTransactionFunc = function() {
return sequelize.transaction({type: Support.Sequelize.Transaction.TYPES.EXCLUSIVE}).then(function(t){
return User.create({}, {transaction:t}).then(function( ) {
return t.commit();
});
});
};
return Promise.join(newTransactionFunc(), newTransactionFunc()).then(function(results) {
return User.findAll().then(function(users) {
expect(users.length).to.equal(2);
});
});
});
});
});
it('fails with SQLITE_BUSY when retry.match is changed', function () {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
var User = sequelize.define('User', { id: {type: Support.Sequelize.INTEGER, primaryKey: true}, username: Support.Sequelize.STRING });
return User.sync({ force: true }).then(function() {
var newTransactionFunc = function() {
return sequelize.transaction({type: Support.Sequelize.Transaction.TYPES.EXCLUSIVE, retry: {match: ['NO_MATCH']}}).then(function(t){
// introduce delay to force the busy state race condition to fail
return Promise.delay(1000).then(function () {
return User.create({id: null, username: 'test ' + t.id}, {transaction:t}).then(function() {
return t.commit();
});
});
});
};
return expect(Promise.join(newTransactionFunc(), newTransactionFunc())).to.be.rejectedWith('SQLITE_BUSY: database is locked');
});
});
});
}
if (current.dialect.supports.lock) {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!