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

Commit 7882c9a4 by Eric Thompson

Added retry and options to retry when queries fail with SQL_BUSY state.

1 parent ad124e3b
# FUTURE # FUTURE
- [ADDED] Support silent: true in bulk update [#5200](https://github.com/sequelize/sequelize/issues/5200) - [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.
# 3.17.3 # 3.17.3
- [FIXED] Regression with array values from security fix in 3.17.2 - [FIXED] Regression with array values from security fix in 3.17.2
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
var url = require('url') var url = require('url')
, Path = require('path') , Path = require('path')
, retry = require('retry-as-promised')
, Utils = require('./utils') , Utils = require('./utils')
, Model = require('./model') , Model = require('./model')
, DataTypes = require('./data-types') , DataTypes = require('./data-types')
...@@ -81,6 +82,9 @@ var url = require('url') ...@@ -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 {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.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 {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.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. * @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) { ...@@ -145,6 +149,7 @@ var Sequelize = function(database, username, password, options) {
pool: {}, pool: {},
quoteIdentifiers: true, quoteIdentifiers: true,
hooks: {}, hooks: {},
retry: {max: 5, match: ['SQLITE_BUSY: database is locked']},
transactionType: Transaction.TYPES.DEFERRED, transactionType: Transaction.TYPES.DEFERRED,
isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ, isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ,
databaseVersion: 0, databaseVersion: 0,
...@@ -682,6 +687,9 @@ Sequelize.prototype.import = function(path) { ...@@ -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 {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 {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 {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 {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 {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. * @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) { ...@@ -798,10 +806,12 @@ Sequelize.prototype.query = function(sql, options) {
).then(function (connection) { ).then(function (connection) {
var query = new self.dialect.Query(connection, self, options); var query = new self.dialect.Query(connection, self, options);
return retry(function() {
return query.run(sql, bindParameters).finally(function() { return query.run(sql, bindParameters).finally(function() {
if (options.transaction) return; if (options.transaction) return;
return self.connectionManager.releaseConnection(connection); return self.connectionManager.releaseConnection(connection);
}); });
}, Utils._.extend(self.options.retry, options.retry || {}));
}).finally(function () { }).finally(function () {
if (self.test.$trackRunningQueries) { if (self.test.$trackRunningQueries) {
self.test.$runningQueries--; self.test.$runningQueries--;
......
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
"moment": "^2.11.1", "moment": "^2.11.1",
"moment-timezone": "^0.5.0", "moment-timezone": "^0.5.0",
"node-uuid": "~1.4.4", "node-uuid": "~1.4.4",
"retry-as-promised": "^2.0.0",
"semver": "^5.0.1", "semver": "^5.0.1",
"shimmer": "1.1.0", "shimmer": "1.1.0",
"toposort-class": "^1.0.1", "toposort-class": "^1.0.1",
......
...@@ -264,6 +264,46 @@ describe(Support.getTestDialectTeaser('Transaction'), function() { ...@@ -264,6 +264,46 @@ describe(Support.getTestDialectTeaser('Transaction'), function() {
}); });
}); });
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) { 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!