如何使用 $q.all 重试失败
How to retry failures with $q.all
我有一些代码可以使用 Breeze 保存数据并报告多次保存的进度,效果相当好。
但是,有时保存会超时,我想自动重试一次。 (目前向用户显示错误,必须手动重试)
我正在努力寻找一种合适的方法来做到这一点,但我对承诺感到困惑,所以我很感激一些帮助。
这是我的代码:
//I'm using Breeze, but because the save takes so long, I
//want to break the changes down into chunks and report progress
//as each chunk is saved....
var surveys = EntityQuery
.from('PropertySurveys')
.using(manager)
.executeLocally();
var promises = [];
var fails = [];
var so = new SaveOptions({ allowConcurrentSaves: false});
var count = 0;
//...so I iterate through the surveys, creating a promise for each survey...
for (var i = 0, len = surveys.length; i < len; i++) {
var query = EntityQuery.from('AnsweredQuestions')
.where('PropertySurveyID', '==', surveys[i].ID)
.expand('ActualAnswers');
var graph = manager.getEntityGraph(query)
var changes = graph.filter(function (entity) {
return !entity.entityAspect.entityState.isUnchanged();
});
if (changes.length > 0) {
promises.push(manager
.saveChanges(changes, so)
.then(function () {
//reporting progress
count++;
logger.info('Uploaded ' + count + ' of ' + promises.length);
},
function () {
//could I retry the fail here?
fails.push(changes);
}
));
}
}
//....then I use $q.all to execute the promises
return $q.all(promises).then(function () {
if (fails.length > 0) {
//could I retry the fails here?
saveFail();
}
else {
saveSuccess();
}
});
编辑
为了澄清为什么我一直在尝试这样做:
我有一个 http 拦截器,可以为所有 http 请求设置超时。当请求超时时,超时会向上调整,并向用户显示一条错误消息,告诉他们如果愿意,可以等待更长的时间重试。
在一个 http 请求中发送所有更改看起来可能需要几分钟,所以我决定将更改分解为多个 http 请求,并在每个请求成功时报告进度。
现在,批处理中的某些请求可能会超时,而有些则不会。
然后我有了一个好主意,我会为 http 请求设置一个低超时,并自动增加它。但是批处理是异步发送的,具有相同的超时设置,并且会针对每次失败调整时间。那可不行。
为了解决这个问题,我想在批处理完成后移动超时调整,然后重试所有请求。
老实说,一开始我不太确定自动超时调整和重试是不是个好主意。即使是这样,在一个接一个地发出 http 请求的情况下可能会更好——我也一直在看:
这是一个非常粗略的解决方法。
var promises = [];
var LIMIT = 3 // 3 tris per promise.
data.forEach(function(chunk) {
promises.push(tryOrFail({
data: chunk,
retries: 0
}));
});
function tryOrFail(data) {
if (data.tries === LIMIT) return $q.reject();
++data.tries;
return processChunk(data.chunk)
.catch(function() {
//Some error handling here
++data.tries;
return tryOrFail(data);
});
}
$q.all(promises) //...
编排 $q.all()
下游的重试是可能的,但确实会非常混乱。在聚合承诺之前执行重试要简单得多。
您可以利用闭包和重试计数器,但构建捕获链更简洁:
function retry(fn, n) {
/*
* Description: perform an arbitrary asynchronous function,
* and, on error, retry up to n times.
* Returns: promise
*/
var p = fn(); // first try
for(var i=0; i<n; i++) {
p = p.catch(function(error) {
// possibly log error here to make it observable
return fn(); // retry
});
}
return p;
}
现在,修改你的 for 循环:
- 使用Function.prototype.bind()将每个保存定义为具有绑定参数的函数。
- 将该函数传递给
retry()
。
- 将
retry().then(...)
返回的承诺推送到 promises
数组。
var query, graph, changes, saveFn;
for (var i = 0, len = surveys.length; i < len; i++) {
query = ...; // as before
graph = ...; // as before
changes = ...; // as before
if (changes.length > 0) {
saveFn = manager.saveChanges.bind(manager, changes, so); // this is what needs to be tried/retried
promises.push(retry(saveFn, 1).then(function() {
// as before
}, function () {
// as before
}));
}
}
return $q.all(promises)... // as before
编辑
不清楚您为什么要重试 $q.all()
的下游。如果需要在重试之前引入一些延迟,最简单的方法就是在上面的模式中进行。
但是,如果重试 $q.all()
的下游是一个严格的要求,这里有一个干净的递归解决方案,允许任意次数的重试,对外部变量的需求最少:
var surveys = //as before
var limit = 2;
function save(changes) {
return manager.saveChanges(changes, so).then(function () {
return true; // true signifies success
}, function (error) {
logger.error('Save Failed');
return changes; // retry (subject to limit)
});
}
function saveChanges(changes_array, tries) {
tries = tries || 0;
if(tries >= limit) {
throw new Error('After ' + tries + ' tries, ' + changes_array.length + ' changes objects were still unsaved.');
}
if(changes_array.length > 0) {
logger.info('Starting try number ' + (tries+1) + ' comprising ' + changes_array.length + ' changes objects');
return $q.all(changes_array.map(save)).then(function(results) {
var successes = results.filter(function() { return item === true; };
var failures = results.filter(function() { return item !== true; }
logger.info('Uploaded ' + successes.length + ' of ' + changes_array.length);
return saveChanges(failures), tries + 1); // recursive call.
});
} else {
return $q(); // return a resolved promise
}
}
//using reduce to populate an array of changes
//the second parameter passed to the reduce method is the initial value
//for memo - in this case an empty array
var changes_array = surveys.reduce(function (memo, survey) {
//memo is the return value from the previous call to the function
var query = EntityQuery.from('AnsweredQuestions')
.where('PropertySurveyID', '==', survey.ID)
.expand('ActualAnswers');
var graph = manager.getEntityGraph(query)
var changes = graph.filter(function (entity) {
return !entity.entityAspect.entityState.isUnchanged();
});
if (changes.length > 0) {
memo.push(changes)
}
return memo;
}, []);
return saveChanges(changes_array).then(saveSuccess, saveFail);
此处的进度报告略有不同。多加思考,可以使它更像您自己的答案。
这里有两个有用的答案,但在解决这个问题后我得出结论,立即重试对我来说真的不起作用。
我想等待第一批完成,然后如果失败是因为超时,增加超时容限,然后再重试失败。
所以我采用了 Juan Stiza 的示例并对其进行了修改以执行我想要的操作。即用 $q.all
重试失败
我的代码现在看起来像这样:
var surveys = //as before
var successes = 0;
var retries = 0;
var failedChanges = [];
//The saveChanges also keeps a track of retries, successes and fails
//it resolves first time through, and rejects second time
//it might be better written as two functions - a save and a retry
function saveChanges(data) {
if (data.retrying) {
retries++;
logger.info('Retrying ' + retries + ' of ' + failedChanges.length);
}
return manager
.saveChanges(data.changes, so)
.then(function () {
successes++;
logger.info('Uploaded ' + successes + ' of ' + promises.length);
},
function (error) {
if (!data.retrying) {
//store the changes and resolve the promise
//so that saveChanges can be called again after the call to $q.all
failedChanges.push(data.changes);
return; //resolved
}
logger.error('Retry Failed');
return $q.reject();
});
}
//using map instead of a for loop to call saveChanges
//and store the returned promises in an array
var promises = surveys.map(function (survey) {
var changes = //as before
return saveChanges({ changes: changes, retrying: false });
});
logger.info('Starting data upload');
return $q.all(promises).then(function () {
if (failedChanges.length > 0) {
var retries = failedChanges.map(function (data) {
return saveChanges({ changes: data, retrying: true });
});
return $q.all(retries).then(saveSuccess, saveFail);
}
else {
saveSuccess();
}
});
我有一些代码可以使用 Breeze 保存数据并报告多次保存的进度,效果相当好。 但是,有时保存会超时,我想自动重试一次。 (目前向用户显示错误,必须手动重试) 我正在努力寻找一种合适的方法来做到这一点,但我对承诺感到困惑,所以我很感激一些帮助。 这是我的代码:
//I'm using Breeze, but because the save takes so long, I
//want to break the changes down into chunks and report progress
//as each chunk is saved....
var surveys = EntityQuery
.from('PropertySurveys')
.using(manager)
.executeLocally();
var promises = [];
var fails = [];
var so = new SaveOptions({ allowConcurrentSaves: false});
var count = 0;
//...so I iterate through the surveys, creating a promise for each survey...
for (var i = 0, len = surveys.length; i < len; i++) {
var query = EntityQuery.from('AnsweredQuestions')
.where('PropertySurveyID', '==', surveys[i].ID)
.expand('ActualAnswers');
var graph = manager.getEntityGraph(query)
var changes = graph.filter(function (entity) {
return !entity.entityAspect.entityState.isUnchanged();
});
if (changes.length > 0) {
promises.push(manager
.saveChanges(changes, so)
.then(function () {
//reporting progress
count++;
logger.info('Uploaded ' + count + ' of ' + promises.length);
},
function () {
//could I retry the fail here?
fails.push(changes);
}
));
}
}
//....then I use $q.all to execute the promises
return $q.all(promises).then(function () {
if (fails.length > 0) {
//could I retry the fails here?
saveFail();
}
else {
saveSuccess();
}
});
编辑 为了澄清为什么我一直在尝试这样做: 我有一个 http 拦截器,可以为所有 http 请求设置超时。当请求超时时,超时会向上调整,并向用户显示一条错误消息,告诉他们如果愿意,可以等待更长的时间重试。
在一个 http 请求中发送所有更改看起来可能需要几分钟,所以我决定将更改分解为多个 http 请求,并在每个请求成功时报告进度。
现在,批处理中的某些请求可能会超时,而有些则不会。
然后我有了一个好主意,我会为 http 请求设置一个低超时,并自动增加它。但是批处理是异步发送的,具有相同的超时设置,并且会针对每次失败调整时间。那可不行。
为了解决这个问题,我想在批处理完成后移动超时调整,然后重试所有请求。
老实说,一开始我不太确定自动超时调整和重试是不是个好主意。即使是这样,在一个接一个地发出 http 请求的情况下可能会更好——我也一直在看:
这是一个非常粗略的解决方法。
var promises = [];
var LIMIT = 3 // 3 tris per promise.
data.forEach(function(chunk) {
promises.push(tryOrFail({
data: chunk,
retries: 0
}));
});
function tryOrFail(data) {
if (data.tries === LIMIT) return $q.reject();
++data.tries;
return processChunk(data.chunk)
.catch(function() {
//Some error handling here
++data.tries;
return tryOrFail(data);
});
}
$q.all(promises) //...
编排 $q.all()
下游的重试是可能的,但确实会非常混乱。在聚合承诺之前执行重试要简单得多。
您可以利用闭包和重试计数器,但构建捕获链更简洁:
function retry(fn, n) {
/*
* Description: perform an arbitrary asynchronous function,
* and, on error, retry up to n times.
* Returns: promise
*/
var p = fn(); // first try
for(var i=0; i<n; i++) {
p = p.catch(function(error) {
// possibly log error here to make it observable
return fn(); // retry
});
}
return p;
}
现在,修改你的 for 循环:
- 使用Function.prototype.bind()将每个保存定义为具有绑定参数的函数。
- 将该函数传递给
retry()
。 - 将
retry().then(...)
返回的承诺推送到promises
数组。
var query, graph, changes, saveFn;
for (var i = 0, len = surveys.length; i < len; i++) {
query = ...; // as before
graph = ...; // as before
changes = ...; // as before
if (changes.length > 0) {
saveFn = manager.saveChanges.bind(manager, changes, so); // this is what needs to be tried/retried
promises.push(retry(saveFn, 1).then(function() {
// as before
}, function () {
// as before
}));
}
}
return $q.all(promises)... // as before
编辑
不清楚您为什么要重试 $q.all()
的下游。如果需要在重试之前引入一些延迟,最简单的方法就是在上面的模式中进行。
但是,如果重试 $q.all()
的下游是一个严格的要求,这里有一个干净的递归解决方案,允许任意次数的重试,对外部变量的需求最少:
var surveys = //as before
var limit = 2;
function save(changes) {
return manager.saveChanges(changes, so).then(function () {
return true; // true signifies success
}, function (error) {
logger.error('Save Failed');
return changes; // retry (subject to limit)
});
}
function saveChanges(changes_array, tries) {
tries = tries || 0;
if(tries >= limit) {
throw new Error('After ' + tries + ' tries, ' + changes_array.length + ' changes objects were still unsaved.');
}
if(changes_array.length > 0) {
logger.info('Starting try number ' + (tries+1) + ' comprising ' + changes_array.length + ' changes objects');
return $q.all(changes_array.map(save)).then(function(results) {
var successes = results.filter(function() { return item === true; };
var failures = results.filter(function() { return item !== true; }
logger.info('Uploaded ' + successes.length + ' of ' + changes_array.length);
return saveChanges(failures), tries + 1); // recursive call.
});
} else {
return $q(); // return a resolved promise
}
}
//using reduce to populate an array of changes
//the second parameter passed to the reduce method is the initial value
//for memo - in this case an empty array
var changes_array = surveys.reduce(function (memo, survey) {
//memo is the return value from the previous call to the function
var query = EntityQuery.from('AnsweredQuestions')
.where('PropertySurveyID', '==', survey.ID)
.expand('ActualAnswers');
var graph = manager.getEntityGraph(query)
var changes = graph.filter(function (entity) {
return !entity.entityAspect.entityState.isUnchanged();
});
if (changes.length > 0) {
memo.push(changes)
}
return memo;
}, []);
return saveChanges(changes_array).then(saveSuccess, saveFail);
此处的进度报告略有不同。多加思考,可以使它更像您自己的答案。
这里有两个有用的答案,但在解决这个问题后我得出结论,立即重试对我来说真的不起作用。
我想等待第一批完成,然后如果失败是因为超时,增加超时容限,然后再重试失败。 所以我采用了 Juan Stiza 的示例并对其进行了修改以执行我想要的操作。即用 $q.all
重试失败我的代码现在看起来像这样:
var surveys = //as before
var successes = 0;
var retries = 0;
var failedChanges = [];
//The saveChanges also keeps a track of retries, successes and fails
//it resolves first time through, and rejects second time
//it might be better written as two functions - a save and a retry
function saveChanges(data) {
if (data.retrying) {
retries++;
logger.info('Retrying ' + retries + ' of ' + failedChanges.length);
}
return manager
.saveChanges(data.changes, so)
.then(function () {
successes++;
logger.info('Uploaded ' + successes + ' of ' + promises.length);
},
function (error) {
if (!data.retrying) {
//store the changes and resolve the promise
//so that saveChanges can be called again after the call to $q.all
failedChanges.push(data.changes);
return; //resolved
}
logger.error('Retry Failed');
return $q.reject();
});
}
//using map instead of a for loop to call saveChanges
//and store the returned promises in an array
var promises = surveys.map(function (survey) {
var changes = //as before
return saveChanges({ changes: changes, retrying: false });
});
logger.info('Starting data upload');
return $q.all(promises).then(function () {
if (failedChanges.length > 0) {
var retries = failedChanges.map(function (data) {
return saveChanges({ changes: data, retrying: true });
});
return $q.all(retries).then(saveSuccess, saveFail);
}
else {
saveSuccess();
}
});