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

Commit 0cf1911f by Sushant Committed by GitHub

fix(sequelize/query): retry entire query (#8991)

1 parent ba26a857
Showing with 32 additions and 55 deletions
...@@ -163,7 +163,12 @@ class Sequelize { ...@@ -163,7 +163,12 @@ class Sequelize {
pool: {}, pool: {},
quoteIdentifiers: true, quoteIdentifiers: true,
hooks: {}, hooks: {},
retry: {max: 5, match: ['SQLITE_BUSY: database is locked']}, retry: {
max: 5,
match: [
'SQLITE_BUSY: database is locked'
]
},
transactionType: Transaction.TYPES.DEFERRED, transactionType: Transaction.TYPES.DEFERRED,
isolationLevel: null, isolationLevel: null,
databaseVersion: 0, databaseVersion: 0,
...@@ -425,14 +430,14 @@ class Sequelize { ...@@ -425,14 +430,14 @@ class Sequelize {
* @param {Object|Array} [options.bind] Either an object of named bind parameter in the format `_param` or an array of unnamed bind parameter to replace `$1, $2, ...` in your SQL. * @param {Object|Array} [options.bind] Either an object of named bind parameter in the format `_param` or an array of unnamed bind parameter to replace `$1, $2, ...` in your SQL.
* @param {Boolean} [options.useMaster=false] Force the query to use the write pool, regardless of the query type. * @param {Boolean} [options.useMaster=false] Force the query to use the write pool, regardless of the query type.
* @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 {new Model()} [options.instance] A sequelize instance used to build the return instance * @param {new Model()} [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 {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 {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 {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 {Boolean} [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 {Boolean} [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.fieldMap] Map returned fields to arbitrary names for `SELECT` query type. * @param {Object} [options.fieldMap] Map returned fields to arbitrary names for `SELECT` query type.
* *
* @return {Promise} * @return {Promise}
...@@ -441,10 +446,13 @@ class Sequelize { ...@@ -441,10 +446,13 @@ class Sequelize {
*/ */
query(sql, options) { query(sql, options) {
options = _.assign({}, this.options.query, options);
const retryOptions = _.assignIn({}, this.options.retry, options.retry || {});
let bindParameters; let bindParameters;
return Promise.try(() => { return retry(retryParameters => Promise.try(() => {
options = _.assign({}, this.options.query, options); const isFirstTry = retryParameters.current === 1;
if (options.instance && !options.model) { if (options.instance && !options.model) {
options.model = options.instance.constructor; options.model = options.instance.constructor;
...@@ -517,41 +525,47 @@ class Sequelize { ...@@ -517,41 +525,47 @@ class Sequelize {
} }
if (options.transaction && options.transaction.finished) { 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)'); 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; error.sql = sql;
return Promise.reject(error); return Promise.reject(error);
} }
if (this.test._trackRunningQueries) { if (isFirstTry && this.test._trackRunningQueries) {
this.test._runningQueries++; this.test._runningQueries++;
} }
//if dialect doesn't support search_path or dialect option //if dialect doesn't support search_path or dialect option
//to prepend searchPath is not true delete the searchPath option //to prepend searchPath is not true delete the searchPath option
if (!this.dialect.supports.searchPath || !this.options.dialectOptions || !this.options.dialectOptions.prependSearchPath || if (
options.supportsSearchPath === false) { !this.dialect.supports.searchPath ||
!this.options.dialectOptions ||
!this.options.dialectOptions.prependSearchPath ||
options.supportsSearchPath === false
) {
delete options.searchPath; delete options.searchPath;
} else if (!options.searchPath) { } else if (!options.searchPath) {
//if user wants to always prepend searchPath (dialectOptions.preprendSearchPath = true) //if user wants to always prepend searchPath (dialectOptions.preprendSearchPath = true)
//then set to DEFAULT if none is provided //then set to DEFAULT if none is provided
options.searchPath = 'DEFAULT'; options.searchPath = 'DEFAULT';
} }
return options.transaction ? options.transaction.connection : this.connectionManager.getConnection(options);
return options.transaction
? options.transaction.connection
: this.connectionManager.getConnection(options);
}).then(connection => { }).then(connection => {
const query = new this.dialect.Query(connection, this, options); const query = new this.dialect.Query(connection, this, options);
const retryOptions = _.extend(this.options.retry, options.retry || {});
return retry(() => query.run(sql, bindParameters), retryOptions) return query.run(sql, bindParameters)
.finally(() => { .finally(() => {
if (this.test._trackRunningQueries) {
this.test._runningQueries--;
}
if (!options.transaction) { if (!options.transaction) {
return this.connectionManager.releaseConnection(connection); return this.connectionManager.releaseConnection(connection);
} }
}); });
}).finally(() => { }), retryOptions);
if (this.test._trackRunningQueries) {
this.test._runningQueries--;
}
});
} }
/** /**
...@@ -772,7 +786,7 @@ class Sequelize { ...@@ -772,7 +786,7 @@ class Sequelize {
databaseVersion(options) { databaseVersion(options) {
return this.getQueryInterface().databaseVersion(options); return this.getQueryInterface().databaseVersion(options);
} }
/** /**
* Get the fn for random based on the dialect * Get the fn for random based on the dialect
* *
......
'use strict';
const chai = require('chai'),
sinon = require('sinon'),
expect = chai.expect,
Support = require(__dirname + '/support'),
Sequelize = Support.Sequelize,
Promise = Sequelize.Promise,
current = Support.sequelize;
describe('sequelize.query', () => {
it('connection should be released only once when retry fails', () => {
const getConnectionStub = sinon.stub(current.connectionManager, 'getConnection').callsFake(() => {
return Promise.resolve({});
});
const releaseConnectionStub = sinon.stub(current.connectionManager, 'releaseConnection').callsFake(() => {
return Promise.resolve();
});
const queryStub = sinon.stub(current.dialect.Query.prototype, 'run').callsFake(() => {
return Promise.reject(new Error('wrong sql'));
});
return current.query('THIS IS A WRONG SQL', {
retry: {
max: 2,
// retry for all errors
match: null
}
})
.catch(() => {})
.finally(() => {
expect(releaseConnectionStub).have.been.calledOnce;
queryStub.restore();
getConnectionStub.restore();
releaseConnectionStub.restore();
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!