mongo-milestone
Version:
*A life-saving little tool to work around the lack of ACID Transactions in MongoDB*
408 lines (288 loc) • 12.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.walkPath = exports.finishAttempt = exports.createAttempt = exports.runAction = exports.endMilestone = exports.startMilestone = exports.createMilestone = exports.run = exports.spawn = exports.ACTION_NOT_READY = exports.MILESTONE_NOT_FOUND = exports.INVALID_MILESTONE = undefined;
var _q = require('q');
var _q2 = _interopRequireDefault(_q);
var _moment = require('moment');
var _moment2 = _interopRequireDefault(_moment);
var _serializeError = require('serialize-error');
var _serializeError2 = _interopRequireDefault(_serializeError);
var _dispatcher = require('./dispatcher');
var _config = require('./config');
var _Milestone = require('./domain/Milestone');
var _Milestone2 = _interopRequireDefault(_Milestone);
var _Action = require('./domain/Action');
var _Action2 = _interopRequireDefault(_Action);
var _Attempt = require('./domain/Attempt');
var _Attempt2 = _interopRequireDefault(_Attempt);
var _NamedAttempt = require('./domain/NamedAttempt');
var _NamedAttempt2 = _interopRequireDefault(_NamedAttempt);
var _db = require('./db');
var db = _interopRequireWildcard(_db);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const INVALID_MILESTONE = exports.INVALID_MILESTONE = 'The object passed in should be of type Milestone';
const MILESTONE_NOT_FOUND = exports.MILESTONE_NOT_FOUND = 'The passed id is not of a valid Milestone';
const ACTION_NOT_READY = exports.ACTION_NOT_READY = type => `The Action "${ type }" is not yet ready to be retried. Check the retry timespan.`;
const threshold = () => {
return (0, _moment2.default)().subtract((0, _config.getConfig)(_config.RETRY_TIMESPAN), 'minutes').toDate();
};
const getElapsedTime = (start, end = new Date()) => {
const offset = new Date().getTimezoneOffset();
const method = offset > 0 ? 'add' : 'subtract';
const elapsed = (0, _moment2.default)(end.getTime() - start.getTime())[method](offset, 'minutes').format('H:mm"ss\'SSS\\m\\s');
return { start, end, elapsed };
};
const spawn = exports.spawn = () => {
var deferred = _q2.default.defer();
_q2.default.spawn(function* () {
try {
const timespan = (0, _config.getConfig)(_config.RETRY_TIMESPAN);
const nextRun = (0, _moment2.default)().add(timespan, 'minutes').toDate();
const result = yield run();
const currentRunEnd = new Date();
deferred.resolve(result);
const nextRunPassed = (0, _moment2.default)(currentRunEnd).isAfter(nextRun);
if (nextRunPassed) {
return spawn();
}
const millisecondsToNextRun = nextRun.getTime() - currentRunEnd.getTime();
setTimeout(() => {
spawn();
}, millisecondsToNextRun);
} catch (e) {
deferred.resolve(e);
}
});
return deferred.promise;
};
const run = exports.run = () => {
const deferred = _q2.default.defer();
_q2.default.spawn(function* () {
try {
const currentRunStart = new Date();
const found = yield db.milestones.aggregate([{ $match: { state: false } }, { $project: { _id: true, type: true, parameters: true } }, { $sort: { _id: 1 } }]).toArray();
const resolved = [];
const rejected = [];
for (const milestone of found) {
const milestoneStart = new Date();
try {
const output = yield robot.startMilestone(milestone._id);
const { elapsed } = getElapsedTime(milestoneStart);
resolved.push({ milestone, output, elapsed });
} catch (error) {
const { elapsed } = getElapsedTime(milestoneStart);
rejected.push({ milestone, error, elapsed });
}
}
const elapsed = getElapsedTime(currentRunStart);
deferred.resolve({ found, resolved, rejected, elapsed });
} catch (e) {
deferred.reject(e);
}
});
return deferred.promise;
};
const createMilestone = exports.createMilestone = milestone => {
const deferred = _q2.default.defer();
if (!(milestone instanceof _Milestone2.default)) {
throw new Error(INVALID_MILESTONE);
}
_q2.default.spawn(function* () {
try {
yield db.milestones.insertOne(milestone);
deferred.resolve(milestone);
} catch (e) {
deferred.reject(e);
}
});
return deferred.promise;
};
const startMilestone = exports.startMilestone = id => {
var deferred = _q2.default.defer();
_q2.default.spawn(function* () {
try {
let milestone = yield db.milestones.findOne({ _id: id });
if (!milestone) {
return deferred.reject(new Error(MILESTONE_NOT_FOUND));
}
const params = { milestone: milestone.parameters };
const output = yield robot.runAction(milestone, milestone.action, params, 'action');
milestone = yield db.milestones.findOne({ _id: id });
const milestoneOutput = milestone.report.filter(t => t.success).reduce((params, attempt) => Object.assign({}, params, { [attempt.actionName]: attempt.output }), params);
milestone = yield robot.endMilestone(milestone._id, milestoneOutput);
deferred.resolve(milestone);
} catch (e) {
deferred.reject(e);
}
});
return deferred.promise;
};
const endMilestone = exports.endMilestone = (id, output) => {
const deferred = _q2.default.defer();
const query = { _id: id };
_q2.default.spawn(function* () {
try {
const result = yield db.milestones.findOneAndUpdate(query, {
$set: {
state: true,
endDate: new Date(),
output
}
}, { returnOriginal: false });
deferred.resolve(result.value);
} catch (e) {
deferred.reject(e);
}
});
return deferred.promise;
};
const runAction = exports.runAction = (milestone, action, parameters, path) => {
var deferred = _q2.default.defer();
_q2.default.spawn(function* () {
try {
if (!action.state) {
const attemptData = yield robot.createAttempt(milestone, action, parameters, path);
milestone = attemptData.milestone;
const attemptId = attemptData.attemptId;
let methodOutput;
try {
methodOutput = yield (0, _dispatcher.dispatch)(action.method, parameters);
} catch (e) {
methodOutput = e;
} finally {
milestone = yield robot.finishAttempt(milestone._id, methodOutput, path, attemptId);
}
if (methodOutput instanceof Error) {
return deferred.reject(methodOutput);
}
}
const parentSuccessfulOutput = ((walkPath(milestone, `${ path }.report`) || []).find(attempt => attempt.success) || {}).output;
const childrenParameters = Object.assign({}, parameters);
childrenParameters[action.type] = parentSuccessfulOutput;
const allActionsComplete = (action.next || []).map((currentAction, index) => {
if (currentAction.state) {
return _q2.default.fcall(() => {
return { [currentAction.type]: (currentAction.report.find(t => t.success) || {}).output };
});
} else {
const lastAttempt = currentAction.report[0];
let isAbandoned = lastAttempt ? lastAttempt.success === null && lastAttempt.beginDate.getTime() < threshold().getTime() : false;
if (!lastAttempt || lastAttempt.success == false || isAbandoned) {
const childPath = `${ path }.next.${ index }`;
return robot.runAction(milestone, currentAction, childrenParameters, childPath);
} else {
return _q2.default.fcall(() => {
throw new Error(ACTION_NOT_READY(currentAction.type));
});
}
}
});
const allActionsCompleteResult = yield _q2.default.all(allActionsComplete);
const doneParameters = allActionsCompleteResult.reduce((parameters, item) => {
return Object.assign({}, parameters, item);
}, childrenParameters);
let doneResult = {};
if (action.done) {
doneResult = yield robot.runAction(milestone, action.done, doneParameters, `${ path }.done`);
}
deferred.resolve({ [action.type]: parentSuccessfulOutput });
} catch (e) {
deferred.reject(e);
}
});
return deferred.promise;
};
const createAttempt = exports.createAttempt = (milestone, action, parameters, path) => {
var deferred = _q2.default.defer();
const id = milestone._id;
const namedAttempt = new _NamedAttempt2.default(action.type, parameters);
const attempt = new _Attempt2.default(parameters, namedAttempt._id);
_q2.default.spawn(function* () {
try {
const update = {
$push: {
'report': {
$each: [namedAttempt],
$position: 0
}
}
};
update.$push[`${ path }.report`] = {
$each: [attempt],
$position: 0
};
const result = yield db.milestones.findOneAndUpdate({ _id: id }, update, { returnOriginal: false });
deferred.resolve({ milestone: result.value, attemptId: namedAttempt._id });
} catch (e) {
deferred.reject(e);
}
});
return deferred.promise;
};
const finishAttempt = exports.finishAttempt = (id, output, path, attemptId) => {
const deferred = _q2.default.defer();
const query = { _id: id, 'report._id': attemptId };
_q2.default.spawn(function* () {
try {
const isError = output instanceof Error;
const parsedOutput = isError ? (0, _serializeError2.default)(output) : typeof output.toObject === 'function' ? output.toObject() : output;
const completionDate = new Date();
const update = {
$set: {
'report.$.success': !isError,
'report.$.output': parsedOutput,
'report.$.endDate': completionDate
}
};
update.$set[`${ path }.report.0.success`] = !isError;
update.$set[`${ path }.report.0.output`] = parsedOutput;
update.$set[`${ path }.report.0.endDate`] = completionDate;
if (!isError) {
update.$set[`${ path }.state`] = true;
}
const result = yield db.milestones.findOneAndUpdate(query, update, { returnOriginal: false });
deferred.resolve(result.value);
} catch (e) {
deferred.reject(e);
}
});
return deferred.promise;
};
const walkPath = exports.walkPath = (obj, path, value) => {
const keys = path.split('.');
let result = obj;
let i = 0;
for (var key of keys) {
i++;
const parsedKey = !isNaN(parseInt(key)) ? parseInt(key) : key;
if (i == keys.length && value !== undefined) {
result[parsedKey] = value;
}
result = result[parsedKey];
if (!result) {
break;
}
}
return result;
};
// const setPath = (obj, path, value) => {
// const ''
// }
const robot = {
run,
spawn,
createMilestone,
startMilestone,
endMilestone,
runAction,
walkPath,
createAttempt,
finishAttempt,
INVALID_MILESTONE,
MILESTONE_NOT_FOUND,
ACTION_NOT_READY
};
exports.default = robot;