Refactored fallback jobs

This commit is contained in:
Daniel García Aubert 2016-06-01 10:39:01 +02:00
parent 1a0e2b681b
commit 481a82500b
10 changed files with 438 additions and 259 deletions

View File

@ -3,17 +3,7 @@
var assert = require('assert');
var uuid = require('node-uuid');
var jobStatus = require('../job_status');
var validStatusTransitions = [
[jobStatus.PENDING, jobStatus.RUNNING],
[jobStatus.PENDING, jobStatus.CANCELLED],
[jobStatus.PENDING, jobStatus.UNKNOWN],
[jobStatus.PENDING, jobStatus.SKIPPED],
[jobStatus.RUNNING, jobStatus.DONE],
[jobStatus.RUNNING, jobStatus.FAILED],
[jobStatus.RUNNING, jobStatus.CANCELLED],
[jobStatus.RUNNING, jobStatus.PENDING],
[jobStatus.RUNNING, jobStatus.UNKNOWN]
];
var validStatusTransitions = require('./job_status_transitions');
var mandatoryProperties = [
'job_id',
'status',
@ -22,6 +12,12 @@ var mandatoryProperties = [
'updated_at',
'user'
];
var finalStatus = [
jobStatus.CANCELLED,
jobStatus.DONE,
jobStatus.FAILED,
jobStatus.UNKNOWN
];
function JobBase(data) {
var now = new Date().toISOString();
@ -58,6 +54,10 @@ JobBase.prototype.isValidStatusTransition = function (initialStatus, finalStatus
return false;
};
JobBase.prototype.isFinalStatus = function (status) {
return finalStatus.indexOf(status) !== -1;
};
// should be implemented by childs
JobBase.prototype.getNextQuery = function () {
throw new Error('Unimplemented method');

View File

@ -3,28 +3,23 @@
var util = require('util');
var JobBase = require('./job_base');
var jobStatus = require('../job_status');
var breakStatus = [
jobStatus.CANCELLED,
jobStatus.FAILED,
jobStatus.UNKNOWN
];
function isBreakStatus(status) {
return breakStatus.indexOf(status) !== -1;
}
var finalStatus = [
jobStatus.CANCELLED,
jobStatus.DONE,
jobStatus.FAILED,
jobStatus.UNKNOWN
];
function isFinalStatus(status) {
return finalStatus.indexOf(status) !== -1;
}
var QueryFallback = require('./query/query_fallback');
var MainFallback = require('./query/main_fallback');
var QueryFactory = require('./query/query_factory');
function JobFallback(jobDefinition) {
JobBase.call(this, jobDefinition);
this.init();
this.queries = [];
for (var i = 0; i < this.data.query.query.length; i++) {
this.queries[i] = QueryFactory.create(this.data, i);
}
if (MainFallback.is(this.data)) {
this.fallback = new MainFallback();
}
}
util.inherits(JobFallback, JobBase);
@ -63,11 +58,7 @@ JobFallback.is = function (query) {
}
for (var i = 0; i < query.query.length; i++) {
if (!query.query[i].query) {
return false;
}
if (typeof query.query[i].query !== 'string') {
if (!QueryFallback.is(query.query[i])) {
return false;
}
}
@ -76,7 +67,7 @@ JobFallback.is = function (query) {
};
JobFallback.prototype.init = function () {
// jshint maxcomplexity: 8
// jshint maxcomplexity: 9
for (var i = 0; i < this.data.query.query.length; i++) {
if ((this.data.query.query[i].onsuccess || this.data.query.query[i].onerror) &&
!this.data.query.query[i].status) {
@ -87,87 +78,42 @@ JobFallback.prototype.init = function () {
}
}
if ((this.data.query.onsuccess || this.data.query.onerror) && !this.data.status) {
if (!this.data.status) {
this.data.status = jobStatus.PENDING;
this.data.fallback_status = jobStatus.PENDING;
if (this.data.query.onsuccess || this.data.query.onerror) {
this.data.status = jobStatus.PENDING;
this.data.fallback_status = jobStatus.PENDING;
}
} else if (!this.data.status) {
this.data.status = jobStatus.PENDING;
}
};
JobFallback.prototype.getNextQueryFromQueries = function () {
for (var i = 0; i < this.queries.length; i++) {
if (this.queries[i].hasNextQuery(this.data)) {
return this.queries[i].getNextQuery(this.data);
}
}
};
JobFallback.prototype.getNextQueryFromFallback = function () {
if (this.fallback && this.fallback.hasNextQuery(this.data)) {
return this.fallback.getNextQuery(this.data);
}
};
JobFallback.prototype.getNextQuery = function () {
var query = this._getNextQueryFromQuery();
var query = this.getNextQueryFromQueries();
if (!query) {
query = this._getNextQueryFromJobFallback();
query = this.getNextQueryFromFallback();
}
return query;
};
JobFallback.prototype._hasNextQueryFromQuery = function () {
return !!this._getNextQueryFromQuery();
};
JobFallback.prototype._getNextQueryFromQuery = function () {
// jshint maxcomplexity: 8
for (var i = 0; i < this.data.query.query.length; i++) {
if (this.data.query.query[i].fallback_status) {
if (this._isNextQuery(i)) {
return this.data.query.query[i].query;
} else if (this._isNextQueryOnSuccess(i)) {
return this.data.query.query[i].onsuccess;
} else if (this._isNextQueryOnError(i)) {
return this.data.query.query[i].onerror;
} else if (isBreakStatus(this.data.query.query[i].status)) {
return;
}
} else if (this.data.query.query[i].status === jobStatus.PENDING) {
return this.data.query.query[i].query;
}
}
};
JobFallback.prototype._getNextQueryFromJobFallback = function () {
if (this.data.fallback_status) {
if (this._isNextQueryOnSuccessJob()) {
return this.data.query.onsuccess;
} else if (this._isNextQueryOnErrorJob()) {
return this.data.query.onerror;
}
}
};
JobFallback.prototype._isNextQuery = function (index) {
return this.data.query.query[index].status === jobStatus.PENDING;
};
JobFallback.prototype._isNextQueryOnSuccess = function (index) {
return this.data.query.query[index].status === jobStatus.DONE &&
this.data.query.query[index].onsuccess &&
this.data.query.query[index].fallback_status === jobStatus.PENDING;
};
JobFallback.prototype._isNextQueryOnError = function (index) {
return this.data.query.query[index].status === jobStatus.FAILED &&
this.data.query.query[index].onerror &&
this.data.query.query[index].fallback_status === jobStatus.PENDING;
};
JobFallback.prototype._isNextQueryOnSuccessJob = function () {
return this.data.status === jobStatus.DONE &&
this.data.query.onsuccess &&
this.data.fallback_status === jobStatus.PENDING;
};
JobFallback.prototype._isNextQueryOnErrorJob = function () {
return this.data.status === jobStatus.FAILED &&
this.data.query.onerror &&
this.data.fallback_status === jobStatus.PENDING;
};
JobFallback.prototype.setQuery = function (query) {
if (!JobFallback.is(query)) {
throw new Error('You must indicate a valid SQL');
@ -177,99 +123,90 @@ JobFallback.prototype.setQuery = function (query) {
};
JobFallback.prototype.setStatus = function (status, errorMesssage) {
var now = new Date().toISOString();
var resultFromQuery = this._setQueryStatus(status, errorMesssage);
var resultFromJob = this._setJobStatus(status, resultFromQuery.isChangeAppliedToQueryFallback, errorMesssage);
// jshint maxcomplexity: 7
if (!resultFromJob.isValid && !resultFromQuery.isValid) {
throw new Error('Cannot set status from ' + this.data.status + ' to ' + status);
var now = new Date().toISOString();
var hasChanged = {
isValid: false,
appliedToFallback: false
};
var result = {};
for (var i = 0; i < this.queries.length; i++) {
result = this.queries[i].setStatus(status, this.data, hasChanged, errorMesssage);
if (result.isValid) {
hasChanged = result;
}
}
if (!resultFromQuery.isChangeAppliedToQueryFallback || status === jobStatus.CANCELLED) {
this._setSkipped(status);
result = this.setJobStatus(status, this.data, hasChanged, errorMesssage);
if (result.isValid) {
hasChanged = result;
}
if (!this.getNextQueryFromQueries() && this.fallback) {
result = this.fallback.setStatus(status, this.data, hasChanged);
if (result.isValid) {
hasChanged = result;
}
}
if (!hasChanged.isValid) {
throw new Error('Cannot set status to ' + status);
}
this.data.updated_at = now;
};
JobFallback.prototype._setSkipped = function (status) {
this._setSkippedQueryStatus();
this._setSkippedJobStatus();
JobFallback.prototype.setJobStatus = function (status, job, hasChanged, errorMesssage) {
var isValid = false;
if (status === jobStatus.CANCELLED || status === jobStatus.FAILED) {
this._setRestPendingToSkipped(status);
status = this.shiftStatus(status, hasChanged);
isValid = this.isValidStatusTransition(job.status, status);
if (isValid) {
job.status = status;
}
if (status === jobStatus.FAILED && errorMesssage && !hasChanged.appliedToFallback) {
job.failed_reason = errorMesssage;
}
return { isValid: isValid, appliedToFallback: false };
};
JobFallback.prototype._setSkippedQueryStatus = function () {
// jshint maxcomplexity: 8
for (var i = 0; i < this.data.query.query.length; i++) {
if (this.data.query.query[i].status === jobStatus.FAILED && this.data.query.query[i].onsuccess) {
if (this.isValidStatusTransition(this.data.query.query[i].fallback_status, jobStatus.SKIPPED)) {
this.data.query.query[i].fallback_status = jobStatus.SKIPPED;
}
}
if (this.data.query.query[i].status === jobStatus.DONE && this.data.query.query[i].onerror) {
if (this.isValidStatusTransition(this.data.query.query[i].fallback_status, jobStatus.SKIPPED)) {
this.data.query.query[i].fallback_status = jobStatus.SKIPPED;
}
}
if (this.data.query.query[i].status === jobStatus.CANCELLED && this.data.query.query[i].fallback_status) {
if (this.isValidStatusTransition(this.data.query.query[i].fallback_status, jobStatus.SKIPPED)) {
this.data.query.query[i].fallback_status = jobStatus.SKIPPED;
}
}
}
};
JobFallback.prototype._setSkippedJobStatus = function () {
JobFallback.prototype.shiftStatus = function (status, hasChanged) {
// jshint maxcomplexity: 7
if (this.data.status === jobStatus.FAILED && this.data.query.onsuccess) {
if (this.isValidStatusTransition(this.data.fallback_status, jobStatus.SKIPPED)) {
this.data.fallback_status = jobStatus.SKIPPED;
if (hasChanged.appliedToFallback) {
if (!this.getNextQueryFromQueries() && (status === jobStatus.DONE || status === jobStatus.FAILED)) {
status = this._getLastStatusFromFinishedQuery();
} else if (status === jobStatus.DONE || status === jobStatus.FAILED){
status = jobStatus.PENDING;
}
} else if (this.getNextQueryFromQueries() && status !== jobStatus.RUNNING) {
status = jobStatus.PENDING;
}
if (this.data.status === jobStatus.DONE && this.data.query.onerror) {
if (this.isValidStatusTransition(this.data.fallback_status, jobStatus.SKIPPED)) {
this.data.fallback_status = jobStatus.SKIPPED;
}
}
if (this.data.status === jobStatus.CANCELLED && this.data.fallback_status) {
if (this.isValidStatusTransition(this.data.fallback_status, jobStatus.SKIPPED)) {
this.data.fallback_status = jobStatus.SKIPPED;
}
}
return status;
};
JobFallback.prototype._setRestPendingToSkipped = function (status) {
for (var i = 0; i < this.data.query.query.length; i++) {
if (this.data.query.query[i].status === jobStatus.PENDING) {
this.data.query.query[i].status = jobStatus.SKIPPED;
}
if (this.data.query.query[i].status !== status &&
this.data.query.query[i].fallback_status === jobStatus.PENDING) {
this.data.query.query[i].fallback_status = jobStatus.SKIPPED;
}
}
};
JobFallback.prototype._getLastStatusFromFinishedQuery = function () {
var lastStatus = jobStatus.DONE;
for (var i = 0; i < this.data.query.query.length; i++) {
if (this.data.query.query[i].fallback_status) {
if (isFinalStatus(this.data.query.query[i].status)) {
if (this.isFinalStatus(this.data.query.query[i].status)) {
lastStatus = this.data.query.query[i].status;
} else {
break;
}
} else {
if (isFinalStatus(this.data.query.query[i].status)) {
if (this.isFinalStatus(this.data.query.query[i].status)) {
lastStatus = this.data.query.query[i].status;
} else {
break;
@ -279,95 +216,3 @@ JobFallback.prototype._getLastStatusFromFinishedQuery = function () {
return lastStatus;
};
JobFallback.prototype._setJobStatus = function (status, isChangeAppliedToQueryFallback, errorMesssage) {
var isValid = false;
status = this._shiftJobStatus(status, isChangeAppliedToQueryFallback);
isValid = this.isValidStatusTransition(this.data.status, status);
if (isValid) {
this.data.status = status;
} else if (this.data.fallback_status) {
isValid = this.isValidStatusTransition(this.data.fallback_status, status);
if (isValid) {
this.data.fallback_status = status;
}
}
if (status === jobStatus.FAILED && errorMesssage && !isChangeAppliedToQueryFallback) {
this.data.failed_reason = errorMesssage;
}
return {
isValid: isValid
};
};
JobFallback.prototype._shiftJobStatus = function (status, isChangeAppliedToQueryFallback) {
// jshint maxcomplexity: 7
// In some scenarios we have to change the normal flow in order to keep consistency
// between query's status and job's status.
if (isChangeAppliedToQueryFallback) {
if (!this._hasNextQueryFromQuery() && (status === jobStatus.DONE || status === jobStatus.FAILED)) {
status = this._getLastStatusFromFinishedQuery();
} else if (status === jobStatus.DONE || status === jobStatus.FAILED){
status = jobStatus.PENDING;
}
} else if (this._hasNextQueryFromQuery() && status !== jobStatus.RUNNING) {
status = jobStatus.PENDING;
}
return status;
};
JobFallback.prototype._shouldTryToApplyStatusTransitionToQueryFallback = function (index) {
return (this.data.query.query[index].status === jobStatus.DONE && this.data.query.query[index].onsuccess) ||
(this.data.query.query[index].status === jobStatus.FAILED && this.data.query.query[index].onerror);
};
JobFallback.prototype._setQueryStatus = function (status, errorMesssage) {
// jshint maxcomplexity: 8
var isValid = false;
var isChangeAppliedToQueryFallback = false;
for (var i = 0; i < this.data.query.query.length; i++) {
isValid = this.isValidStatusTransition(this.data.query.query[i].status, status);
if (isValid) {
this.data.query.query[i].status = status;
if (status === jobStatus.FAILED && errorMesssage) {
this.data.query.query[i].failed_reason = errorMesssage;
}
break;
}
if (this._shouldTryToApplyStatusTransitionToQueryFallback(i)) {
isValid = this.isValidStatusTransition(this.data.query.query[i].fallback_status, status);
if (isValid) {
this.data.query.query[i].fallback_status = status;
if (status === jobStatus.FAILED && errorMesssage) {
this.data.query.query[i].failed_reason = errorMesssage;
}
isChangeAppliedToQueryFallback = true;
break;
}
}
}
return {
isValid: isValid,
isChangeAppliedToQueryFallback: isChangeAppliedToQueryFallback
};
};

View File

@ -0,0 +1,17 @@
'use strict';
var jobStatus = require('../job_status');
var validStatusTransitions = [
[jobStatus.PENDING, jobStatus.RUNNING],
[jobStatus.PENDING, jobStatus.CANCELLED],
[jobStatus.PENDING, jobStatus.UNKNOWN],
[jobStatus.PENDING, jobStatus.SKIPPED],
[jobStatus.RUNNING, jobStatus.DONE],
[jobStatus.RUNNING, jobStatus.FAILED],
[jobStatus.RUNNING, jobStatus.CANCELLED],
[jobStatus.RUNNING, jobStatus.PENDING],
[jobStatus.RUNNING, jobStatus.UNKNOWN]
];
module.exports = validStatusTransitions;

View File

@ -0,0 +1,65 @@
'use strict';
var util = require('util');
var QueryBase = require('./query_base');
var jobStatus = require('../../job_status');
function Fallback(index) {
QueryBase.call(this, index);
}
util.inherits(Fallback, QueryBase);
module.exports = Fallback;
Fallback.is = function (query) {
if (query.onsuccess || query.onerror) {
return true;
}
return false;
};
Fallback.prototype.getNextQuery = function (job) {
if (this.hasOnSuccess(job)) {
return this.getOnSuccess(job);
}
if (this.hasOnError(job)) {
return this.getOnError(job);
}
};
Fallback.prototype.getOnSuccess = function (job) {
if (job.query.query[this.index].status === jobStatus.DONE &&
job.query.query[this.index].fallback_status === jobStatus.PENDING) {
return job.query.query[this.index].onsuccess;
}
};
Fallback.prototype.hasOnSuccess = function (job) {
return !!this.getOnSuccess(job);
};
Fallback.prototype.getOnError = function (job) {
if (job.query.query[this.index].status === jobStatus.FAILED &&
job.query.query[this.index].fallback_status === jobStatus.PENDING) {
return job.query.query[this.index].onerror;
}
};
Fallback.prototype.hasOnError = function (job) {
return !!this.getOnError(job);
};
Fallback.prototype.setStatus = function (status, job, errorMesssage) {
var isValid = false;
isValid = this.isValidTransition(job.query.query[this.index].fallback_status, status);
if (isValid) {
job.query.query[this.index].fallback_status = status;
if (status === jobStatus.FAILED && errorMesssage) {
job.query.query[this.index].failed_reason = errorMesssage;
}
}
return isValid;
};

View File

@ -0,0 +1,74 @@
'use strict';
var util = require('util');
var QueryBase = require('./query_base');
var jobStatus = require('../../job_status');
function MainFallback() {
QueryBase.call(this);
}
util.inherits(MainFallback, QueryBase);
module.exports = MainFallback;
MainFallback.is = function (job) {
if (job.query.onsuccess || job.query.onerror) {
return true;
}
return false;
};
MainFallback.prototype.getNextQuery = function (job) {
if (this.hasOnSuccess(job)) {
return this.getOnSuccess(job);
}
if (this.hasOnError(job)) {
return this.getOnError(job);
}
};
MainFallback.prototype.getOnSuccess = function (job) {
if (job.status === jobStatus.DONE && job.fallback_status === jobStatus.PENDING) {
return job.query.onsuccess;
}
};
MainFallback.prototype.hasOnSuccess = function (job) {
return !!this.getOnSuccess(job);
};
MainFallback.prototype.getOnError = function (job) {
if (job.status === jobStatus.FAILED && job.fallback_status === jobStatus.PENDING) {
return job.query.onerror;
}
};
MainFallback.prototype.hasOnError = function (job) {
return !!this.getOnError(job);
};
MainFallback.prototype.setStatus = function (status, job, previous) {
var isValid = false;
var appliedToFallback = false;
if (previous.isValid && !previous.appliedToFallback) {
if (this.isFinalStatus(status) && !this.hasNextQuery(job)) {
isValid = this.isValidTransition(job.fallback_status, jobStatus.SKIPPED);
if (isValid) {
job.fallback_status = jobStatus.SKIPPED;
appliedToFallback = true;
}
}
} else if (!previous.isValid) {
isValid = this.isValidTransition(job.fallback_status, status);
if (isValid) {
job.fallback_status = status;
appliedToFallback = true;
}
}
return { isValid: isValid, appliedToFallback: appliedToFallback };
};

View File

@ -0,0 +1,41 @@
'use strict';
var util = require('util');
var QueryBase = require('./query_base');
var jobStatus = require('../../job_status');
function Query(index) {
QueryBase.call(this, index);
}
util.inherits(Query, QueryBase);
module.exports = Query;
Query.is = function (query) {
if (query.query && typeof query.query === 'string') {
return true;
}
return false;
};
Query.prototype.getNextQuery = function (job) {
if (job.query.query[this.index].status === jobStatus.PENDING) {
return job.query.query[this.index].query;
}
};
Query.prototype.setStatus = function (status, job, errorMesssage) {
var isValid = false;
isValid = this.isValidTransition(job.query.query[this.index].status, status);
if (isValid) {
job.query.query[this.index].status = status;
if (status === jobStatus.FAILED && errorMesssage) {
job.query.query[this.index].failed_reason = errorMesssage;
}
}
return isValid;
};

View File

@ -0,0 +1,51 @@
'use strict';
var jobStatus = require('../../job_status');
var assert = require('assert');
var validStatusTransitions = require('../job_status_transitions');
var finalStatus = [
jobStatus.CANCELLED,
jobStatus.DONE,
jobStatus.FAILED,
jobStatus.UNKNOWN
];
function QueryBase(index) {
this.index = index;
}
module.exports = QueryBase;
QueryBase.prototype.isFinalStatus = function (status) {
return finalStatus.indexOf(status) !== -1;
};
// should be implemented
QueryBase.prototype.setStatus = function () {
throw new Error('Unimplemented method');
};
// should be implemented
QueryBase.prototype.getNextQuery = function () {
throw new Error('Unimplemented method');
};
QueryBase.prototype.hasNextQuery = function (job) {
return !!this.getNextQuery(job);
};
QueryBase.prototype.isValidTransition = function (initialStatus, finalStatus) {
var transition = [ initialStatus, finalStatus ];
for (var i = 0; i < validStatusTransitions.length; i++) {
try {
assert.deepEqual(transition, validStatusTransitions[i]);
return true;
} catch (e) {
continue;
}
}
return false;
};

View File

@ -0,0 +1,16 @@
'use strict';
var QueryFallback = require('./query_fallback');
function QueryFactory() {
}
module.exports = QueryFactory;
QueryFactory.create = function (job, index) {
if (QueryFallback.is(job.query.query[index])) {
return new QueryFallback(job, index);
}
throw new Error('there is no query class for the provided query');
};

View File

@ -0,0 +1,71 @@
'use strict';
var util = require('util');
var QueryBase = require('./query_base');
var Query = require('./query');
var Fallback = require('./fallback');
var jobStatus = require('../../job_status');
function QueryFallback(job, index) {
QueryBase.call(this, index);
this.init(job, index);
}
util.inherits(QueryFallback, QueryBase);
QueryFallback.is = function (query) {
if (Query.is(query)) {
return true;
}
return false;
};
QueryFallback.prototype.init = function (job, index) {
this.query = new Query(index);
if (Fallback.is(job.query.query[index])) {
this.fallback = new Fallback(index);
}
};
QueryFallback.prototype.getNextQuery = function (job) {
if (this.query.hasNextQuery(job)) {
return this.query.getNextQuery(job);
}
if (this.fallback && this.fallback.hasNextQuery(job)) {
return this.fallback.getNextQuery(job);
}
};
QueryFallback.prototype.setStatus = function (status, job, previous, errorMesssage) {
// jshint maxcomplexity: 9
var isValid = false;
var appliedToFallback = false;
if (previous.isValid && !previous.appliedToFallback) {
if (status === jobStatus.FAILED || status === jobStatus.CANCELLED) {
this.query.setStatus(jobStatus.SKIPPED, job, errorMesssage);
if (this.fallback) {
this.fallback.setStatus(jobStatus.SKIPPED, job);
}
}
} else if (!previous.isValid) {
isValid = this.query.setStatus(status, job, errorMesssage);
if (this.fallback) {
if (!isValid) {
isValid = this.fallback.setStatus(status, job, errorMesssage);
appliedToFallback = true;
} else if (isValid && this.isFinalStatus(status) && !this.fallback.hasNextQuery(job)) {
this.fallback.setStatus(jobStatus.SKIPPED, job);
}
}
}
return { isValid: isValid, appliedToFallback: appliedToFallback };
};
module.exports = QueryFallback;

View File

@ -31,7 +31,6 @@ describe('Batch API fallback job', function () {
describe('"onsuccess" on first query should be triggered', function () {
var fallbackJob = {};
it('should create a job', function (done) {
assert.response(app, {
url: '/api/v2/sql/job?api_key=1234',
@ -951,7 +950,7 @@ describe('Batch API fallback job', function () {
});
});
describe('"onsuccess" should not be triggered for any query and "skipped"', function () {
describe('"onsuccess" should be "skipped"', function () {
var fallbackJob = {};
it('should create a job', function (done) {