UNPKG

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
'use strict'; 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;