first commit

This commit is contained in:
2023-05-19 00:42:48 +08:00
commit 53de9c6c51
243 changed files with 39485 additions and 0 deletions

122
batch/models/job_base.js Normal file
View File

@@ -0,0 +1,122 @@
'use strict';
var util = require('util');
var uuid = require('node-uuid');
var JobStateMachine = require('./job_state_machine');
var jobStatus = require('../job_status');
var mandatoryProperties = [
'job_id',
'status',
'query',
'created_at',
'updated_at',
'host',
'user'
];
function JobBase(data) {
JobStateMachine.call(this);
var now = new Date().toISOString();
this.data = data;
if (!this.data.job_id) {
this.data.job_id = uuid.v4();
}
if (!this.data.created_at) {
this.data.created_at = now;
}
if (!this.data.updated_at) {
this.data.updated_at = now;
}
}
util.inherits(JobBase, JobStateMachine);
module.exports = JobBase;
// should be implemented by childs
JobBase.prototype.getNextQuery = function () {
throw new Error('Unimplemented method');
};
JobBase.prototype.hasNextQuery = function () {
return !!this.getNextQuery();
};
JobBase.prototype.isPending = function () {
return this.data.status === jobStatus.PENDING;
};
JobBase.prototype.isRunning = function () {
return this.data.status === jobStatus.RUNNING;
};
JobBase.prototype.isDone = function () {
return this.data.status === jobStatus.DONE;
};
JobBase.prototype.isCancelled = function () {
return this.data.status === jobStatus.CANCELLED;
};
JobBase.prototype.isFailed = function () {
return this.data.status === jobStatus.FAILED;
};
JobBase.prototype.isUnknown = function () {
return this.data.status === jobStatus.UNKNOWN;
};
JobBase.prototype.setQuery = function (query) {
var now = new Date().toISOString();
if (!this.isPending()) {
throw new Error('Job is not pending, it cannot be updated');
}
this.data.updated_at = now;
this.data.query = query;
};
JobBase.prototype.setStatus = function (finalStatus, errorMesssage) {
var now = new Date().toISOString();
var initialStatus = this.data.status;
var isValid = this.isValidTransition(initialStatus, finalStatus);
if (!isValid) {
throw new Error('Cannot set status from ' + initialStatus + ' to ' + finalStatus);
}
this.data.updated_at = now;
this.data.status = finalStatus;
if (finalStatus === jobStatus.FAILED && errorMesssage) {
this.data.failed_reason = errorMesssage;
}
};
JobBase.prototype.validate = function () {
for (var i = 0; i < mandatoryProperties.length; i++) {
if (!this.data[mandatoryProperties[i]]) {
throw new Error('property "' + mandatoryProperties[i] + '" is mandatory');
}
}
};
JobBase.prototype.serialize = function () {
var data = JSON.parse(JSON.stringify(this.data));
delete data.host;
delete data.dbuser;
delete data.port;
delete data.dbname;
delete data.pass;
return data;
};
JobBase.prototype.log = function(/*logger*/) {
return false;
};

View File

@@ -0,0 +1,26 @@
'use strict';
var JobSimple = require('./job_simple');
var JobMultiple = require('./job_multiple');
var JobFallback = require('./job_fallback');
var Models = [ JobSimple, JobMultiple, JobFallback ];
function JobFactory() {
}
module.exports = JobFactory;
JobFactory.create = function (data) {
if (!data.query) {
throw new Error('You must indicate a valid SQL');
}
for (var i = 0; i < Models.length; i++) {
if (Models[i].is(data.query)) {
return new Models[i](data);
}
}
throw new Error('there is no job class for the provided query');
};

View File

@@ -0,0 +1,279 @@
'use strict';
var util = require('util');
var JobBase = require('./job_base');
var JobStatus = require('../job_status');
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);
module.exports = JobFallback;
// 1. from user: {
// query: {
// query: [{
// query: 'select ...',
// onsuccess: 'select ..'
// }],
// onerror: 'select ...'
// }
// }
//
// 2. from redis: {
// status: 'pending',
// fallback_status: 'pending'
// query: {
// query: [{
// query: 'select ...',
// onsuccess: 'select ..'
// status: 'pending',
// fallback_status: 'pending',
// }],
// onerror: 'select ...'
// }
// }
JobFallback.is = function (query) {
if (!query.query) {
return false;
}
if (!Array.isArray(query.query)) {
return false;
}
for (var i = 0; i < query.query.length; i++) {
if (!QueryFallback.is(query.query[i])) {
return false;
}
}
return true;
};
JobFallback.prototype.init = function () {
for (var i = 0; i < this.data.query.query.length; i++) {
if (shouldInitStatus(this.data.query.query[i])){
this.data.query.query[i].status = JobStatus.PENDING;
}
if (shouldInitQueryFallbackStatus(this.data.query.query[i])) {
this.data.query.query[i].fallback_status = JobStatus.PENDING;
}
}
if (shouldInitStatus(this.data)) {
this.data.status = JobStatus.PENDING;
}
if (shouldInitFallbackStatus(this.data)) {
this.data.fallback_status = JobStatus.PENDING;
}
};
function shouldInitStatus(jobOrQuery) {
return !jobOrQuery.status;
}
function shouldInitQueryFallbackStatus(query) {
return (query.onsuccess || query.onerror) && !query.fallback_status;
}
function shouldInitFallbackStatus(job) {
return (job.query.onsuccess || job.query.onerror) && !job.fallback_status;
}
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.hasNextQueryFromQueries = function () {
return !!this.getNextQueryFromQueries();
};
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.getNextQueryFromQueries();
if (!query) {
query = this.getNextQueryFromFallback();
}
return query;
};
JobFallback.prototype.setQuery = function (query) {
if (!JobFallback.is(query)) {
throw new Error('You must indicate a valid SQL');
}
JobFallback.super_.prototype.setQuery.call(this, query);
};
JobFallback.prototype.setStatus = function (status, errorMesssage) {
var now = new Date().toISOString();
var hasChanged = this.setQueryStatus(status, this.data, errorMesssage);
hasChanged = this.setJobStatus(status, this.data, hasChanged, errorMesssage);
hasChanged = this.setFallbackStatus(status, this.data, hasChanged);
if (!hasChanged.isValid) {
throw new Error('Cannot set status to ' + status);
}
this.data.updated_at = now;
};
JobFallback.prototype.setQueryStatus = function (status, job, errorMesssage) {
return this.queries.reduce(function (hasChanged, query) {
var result = query.setStatus(status, this.data, hasChanged, errorMesssage);
return result.isValid ? result : hasChanged;
}.bind(this), { isValid: false, appliedToFallback: false });
};
JobFallback.prototype.setJobStatus = function (status, job, hasChanged, errorMesssage) {
var result = {
isValid: false,
appliedToFallback: false
};
status = this.shiftStatus(status, hasChanged);
result.isValid = this.isValidTransition(job.status, status);
if (result.isValid) {
job.status = status;
if (status === JobStatus.FAILED && errorMesssage && !hasChanged.appliedToFallback) {
job.failed_reason = errorMesssage;
}
}
return result.isValid ? result : hasChanged;
};
JobFallback.prototype.setFallbackStatus = function (status, job, hasChanged) {
var result = hasChanged;
if (this.fallback && !this.hasNextQueryFromQueries()) {
result = this.fallback.setStatus(status, job, hasChanged);
}
return result.isValid ? result : hasChanged;
};
JobFallback.prototype.shiftStatus = function (status, hasChanged) {
// jshint maxcomplexity: 7
if (hasChanged.appliedToFallback) {
if (!this.hasNextQueryFromQueries() && (status === JobStatus.DONE || status === JobStatus.FAILED)) {
status = this.getLastFinishedStatus();
} else if (status === JobStatus.DONE || status === JobStatus.FAILED){
status = JobStatus.PENDING;
}
} else if (this.hasNextQueryFromQueries() && status !== JobStatus.RUNNING) {
status = JobStatus.PENDING;
}
return status;
};
JobFallback.prototype.getLastFinishedStatus = function () {
return this.queries.reduce(function (lastFinished, query) {
var status = query.getStatus(this.data);
return this.isFinalStatus(status) ? status : lastFinished;
}.bind(this), JobStatus.DONE);
};
JobFallback.prototype.log = function(logger) {
if (!isFinished(this)) {
return false;
}
var queries = this.data.query.query;
for (var i = 0; i < queries.length; i++) {
var query = queries[i];
var logEntry = {
created: this.data.created_at,
waiting: elapsedTime(this.data.created_at, query.started_at),
time: query.started_at,
endtime: query.ended_at,
username: this.data.user,
dbhost: this.data.host,
job: this.data.job_id,
status: query.status,
elapsed: elapsedTime(query.started_at, query.ended_at)
};
var queryId = query.id;
var tag = 'query';
if (queryId) {
logEntry.query_id = queryId;
var node = parseQueryId(queryId);
if (node) {
logEntry.analysis = node.analysisId;
logEntry.node = node.nodeId;
logEntry.type = node.nodeType;
tag = 'analysis';
}
}
logger.info(logEntry, tag);
}
return true;
};
function isFinished (job) {
return JobStatus.isFinal(job.data.status) &&
(!job.data.fallback_status || JobStatus.isFinal(job.data.fallback_status));
}
function parseQueryId (queryId) {
var data = queryId.split(':');
if (data.length === 3) {
return {
analysisId: data[0],
nodeId: data[1],
nodeType: data[2]
};
}
return null;
}
function elapsedTime (started_at, ended_at) {
if (!started_at || !ended_at) {
return;
}
var start = new Date(started_at);
var end = new Date(ended_at);
return end.getTime() - start.getTime();
}

View File

@@ -0,0 +1,91 @@
'use strict';
var util = require('util');
var JobBase = require('./job_base');
var jobStatus = require('../job_status');
function JobMultiple(jobDefinition) {
JobBase.call(this, jobDefinition);
this.init();
}
util.inherits(JobMultiple, JobBase);
module.exports = JobMultiple;
JobMultiple.is = function (query) {
if (!Array.isArray(query)) {
return false;
}
// 1. From user: ['select * from ...', 'select * from ...']
// 2. From redis: [ { query: 'select * from ...', status: 'pending' },
// { query: 'select * from ...', status: 'pending' } ]
for (var i = 0; i < query.length; i++) {
if (typeof query[i] !== 'string') {
if (typeof query[i].query !== 'string') {
return false;
}
}
}
return true;
};
JobMultiple.prototype.init = function () {
if (!this.data.status) {
this.data.status = jobStatus.PENDING;
}
for (var i = 0; i < this.data.query.length; i++) {
if (!this.data.query[i].query && !this.data.query[i].status) {
this.data.query[i] = {
query: this.data.query[i],
status: jobStatus.PENDING
};
}
}
};
JobMultiple.prototype.getNextQuery = function () {
for (var i = 0; i < this.data.query.length; i++) {
if (this.data.query[i].status === jobStatus.PENDING) {
return this.data.query[i].query;
}
}
};
JobMultiple.prototype.setQuery = function (query) {
if (!JobMultiple.is(query)) {
throw new Error('You must indicate a valid SQL');
}
JobMultiple.super_.prototype.setQuery.call(this, query);
};
JobMultiple.prototype.setStatus = function (finalStatus, errorMesssage) {
var initialStatus = this.data.status;
// if transition is to "done" and there are more queries to run
// then job status must be "pending" instead of "done"
// else job status transition to done (if "running")
if (finalStatus === jobStatus.DONE && this.hasNextQuery()) {
JobMultiple.super_.prototype.setStatus.call(this, jobStatus.PENDING);
} else {
JobMultiple.super_.prototype.setStatus.call(this, finalStatus, errorMesssage);
}
for (var i = 0; i < this.data.query.length; i++) {
var isValid = JobMultiple.super_.prototype.isValidTransition(this.data.query[i].status, finalStatus);
if (isValid) {
this.data.query[i].status = finalStatus;
if (finalStatus === jobStatus.FAILED && errorMesssage) {
this.data.query[i].failed_reason = errorMesssage;
}
return;
}
}
throw new Error('Cannot set status from ' + initialStatus + ' to ' + finalStatus);
};

View File

@@ -0,0 +1,34 @@
'use strict';
var util = require('util');
var JobBase = require('./job_base');
var jobStatus = require('../job_status');
function JobSimple(jobDefinition) {
JobBase.call(this, jobDefinition);
if (!this.data.status) {
this.data.status = jobStatus.PENDING;
}
}
util.inherits(JobSimple, JobBase);
module.exports = JobSimple;
JobSimple.is = function (query) {
return typeof query === 'string';
};
JobSimple.prototype.getNextQuery = function () {
if (this.isPending()) {
return this.data.query;
}
};
JobSimple.prototype.setQuery = function (query) {
if (!JobSimple.is(query)) {
throw new Error('You must indicate a valid SQL');
}
JobSimple.super_.prototype.setQuery.call(this, query);
};

View File

@@ -0,0 +1,39 @@
'use strict';
var assert = require('assert');
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]
];
function JobStateMachine () {
}
module.exports = JobStateMachine;
JobStateMachine.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;
};
JobStateMachine.prototype.isFinalStatus = function (status) {
return JobStatus.isFinal(status);
};

View File

@@ -0,0 +1,78 @@
'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) {
var onsuccessQuery = job.query.query[this.index].onsuccess;
if (onsuccessQuery) {
onsuccessQuery = onsuccessQuery.replace(/<%=\s*job_id\s*%>/g, job.job_id);
}
return onsuccessQuery;
}
};
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) {
var onerrorQuery = job.query.query[this.index].onerror;
if (onerrorQuery) {
onerrorQuery = onerrorQuery.replace(/<%=\s*job_id\s*%>/g, job.job_id);
onerrorQuery = onerrorQuery.replace(/<%=\s*error_message\s*%>/g, job.query.query[this.index].failed_reason);
}
return onerrorQuery;
}
};
Fallback.prototype.hasOnError = function (job) {
return !!this.getOnError(job);
};
Fallback.prototype.setStatus = function (status, job, errorMessage) {
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 && errorMessage) {
job.query.query[this.index].failed_reason = errorMessage;
}
}
return isValid;
};
Fallback.prototype.getStatus = function (job) {
return job.query.query[this.index].fallback_status;
};

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,57 @@
'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) {
var query = {
query: job.query.query[this.index].query
};
if (Number.isFinite(job.query.query[this.index].timeout)) {
query.timeout = job.query.query[this.index].timeout;
}
return 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.RUNNING) {
job.query.query[this.index].started_at = new Date().toISOString();
}
if (this.isFinalStatus(status)) {
job.query.query[this.index].ended_at = new Date().toISOString();
}
if (status === jobStatus.FAILED && errorMesssage) {
job.query.query[this.index].failed_reason = errorMesssage;
}
}
return isValid;
};
Query.prototype.getStatus = function (job) {
return job.query.query[this.index].status;
};

View File

@@ -0,0 +1,31 @@
'use strict';
var util = require('util');
var JobStateMachine = require('../job_state_machine');
function QueryBase(index) {
JobStateMachine.call(this);
this.index = index;
}
util.inherits(QueryBase, JobStateMachine);
module.exports = QueryBase;
// 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.getStatus = function () {
throw new Error('Unimplemented method');
};

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,75 @@
'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 };
};
QueryFallback.prototype.getStatus = function (job) {
return this.query.getStatus(job);
};
module.exports = QueryFallback;