Improve error handling in replication

Deprecate `Change.handleError`, it was used inconsistenly for a subset
of possible errors only. Rework all `Change` methods to always report
all errors to the caller via the callback.

Rework `PersistedModel` to report change-tracking errors via the
existing method `PersistedModel.handleChangeError`. This method
can be customized on a per-model basis to provide different error
handling.

The default implementation emits `error` event on the model class,
users can attach an event listener that can provide a custom error
handler.

NOTE: Unhandled `error` events crash the application by default.
This commit is contained in:
Miroslav Bajtoš 2015-03-26 17:14:41 +01:00
parent 6640f8a082
commit 63e2f4b134
2 changed files with 44 additions and 21 deletions

View File

@ -9,6 +9,7 @@ var CJSON = {stringify: require('canonical-json')};
var async = require('async'); var async = require('async');
var assert = require('assert'); var assert = require('assert');
var debug = require('debug')('loopback:change'); var debug = require('debug')('loopback:change');
var deprecate = require('depd')('loopback');
/** /**
* Change list entry. * Change list entry.
@ -73,18 +74,43 @@ module.exports = function(Change) {
*/ */
Change.rectifyModelChanges = function(modelName, modelIds, callback) { Change.rectifyModelChanges = function(modelName, modelIds, callback) {
var tasks = [];
var Change = this; var Change = this;
var errors = [];
modelIds.forEach(function(id) { var tasks = modelIds.map(function(id) {
tasks.push(function(cb) { return function(cb) {
Change.findOrCreateChange(modelName, id, function(err, change) { Change.findOrCreateChange(modelName, id, function(err, change) {
if (err) return Change.handleError(err, cb); if (err) return next(err);
change.rectify(cb); change.rectify(next);
}); });
});
function next(err) {
if (err) {
err.modelName = modelName;
err.modelId = id;
errors.push(err);
}
cb();
}
};
});
async.parallel(tasks, function(err) {
if (err) return callback(err);
if (errors.length) {
var desc = errors
.map(function(e) {
return '#' + e.modelId + ' - ' + e.toString();
})
.join('\n');
var msg = 'Cannot rectify ' + modelName + ' changes:\n' + desc;
err = new Error(msg);
err.details = { errors: errors };
return callback(err);
}
callback();
}); });
async.parallel(tasks, callback);
}; };
/** /**
@ -217,7 +243,7 @@ module.exports = function(Change) {
var model = this.getModelCtor(); var model = this.getModelCtor();
var id = this.getModelId(); var id = this.getModelId();
model.findById(id, function(err, inst) { model.findById(id, function(err, inst) {
if (err) return Change.handleError(err, cb); if (err) return cb(err);
if (inst) { if (inst) {
cb(null, Change.revisionForInst(inst)); cb(null, Change.revisionForInst(inst));
} else { } else {
@ -459,6 +485,9 @@ module.exports = function(Change) {
}; };
Change.handleError = function(err) { Change.handleError = function(err) {
deprecate('Change.handleError is deprecated, ' +
'you should pass errors to your callback instead.');
if (!this.settings.ignoreErrors) { if (!this.settings.ignoreErrors) {
throw err; throw err;
} }

View File

@ -1038,9 +1038,8 @@ PersistedModel.createUpdates = function(deltas, cb) {
Model.findById(change.modelId, function(err, inst) { Model.findById(change.modelId, function(err, inst) {
if (err) return cb(err); if (err) return cb(err);
if (!inst) { if (!inst) {
console.error('missing data for change:', change);
return cb && return cb &&
cb(new Error('missing data for change: ' + change.modelId)); cb(new Error('Missing data for change: ' + change.modelId));
} }
if (inst.toObject) { if (inst.toObject) {
update.data = inst.toObject(); update.data = inst.toObject();
@ -1343,8 +1342,7 @@ PersistedModel.enableChangeTracking = function() {
function cleanup() { function cleanup() {
Model.rectifyAllChanges(function(err) { Model.rectifyAllChanges(function(err) {
if (err) { if (err) {
console.error(Model.modelName + ' Change Cleanup Error:'); Model.handleChangeError(err, 'cleanup');
console.error(err);
} }
}); });
} }
@ -1359,8 +1357,7 @@ function rectifyOnSave(ctx, next) {
function reportErrorAndNext(err) { function reportErrorAndNext(err) {
if (err) { if (err) {
console.error( ctx.Model.handleChangeError(err, 'after save');
ctx.Model.modelName + '.rectifyChange(s) after save failed:' + err);
} }
next(); next();
} }
@ -1378,8 +1375,7 @@ function rectifyOnDelete(ctx, next) {
function reportErrorAndNext(err) { function reportErrorAndNext(err) {
if (err) { if (err) {
console.error( ctx.Model.handleChangeError(err, 'after delete');
ctx.Model.modelName + '.rectifyChange(s) after delete failed:' + err);
} }
next(); next();
} }
@ -1426,11 +1422,9 @@ PersistedModel.rectifyAllChanges = function(callback) {
* @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
*/ */
PersistedModel.handleChangeError = function(err) { PersistedModel.handleChangeError = function(err, operationName) {
if (err) { if (!err) return;
console.error(Model.modelName + ' Change Tracking Error:'); this.emit('error', err, operationName);
console.error(err);
}
}; };
/** /**