【问题标题】:How to retry failures with $q.all如何使用 $q.all 重试失败
【发布时间】:2016-07-29 22:09:44
【问题描述】:

我有一些代码可以使用 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 请求的情况下可能会更好 - 我也一直在关注:https://stackoverflow.com/a/25730751/150342

【问题讨论】:

  • $q.all 无论如何都会失败。您可以在每个承诺解决之前进行错误捕获并在$q.all 之前处理错误。
  • 我的意思是,不是将一个简单的承诺推入数组,而是推一个尝试 n 次的函数来解决该承诺,如果失败,那么一切都会失败,您应该只处理 $q.all 函数的最终结果。
  • 实际上,我发现$q.all 总是成功 - 这就是为什么我在传递给then 的第一个函数中检查fails.count()。我在其他地方有什么问题吗?
  • 但是推送一个尝试 n 次来解决该承诺的函数仍然对我有用。我该怎么做?
  • 我发布了答案。

标签: promise breeze angular-promise


【解决方案1】:

$q.all() 下游的编排重试是可能的,但确实会非常混乱。在聚合 Promise 之前执行重试要简单得多。

您可以利用闭包和重试计数器,但构建一个捕获链更简洁:

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(...) 返回的promise 推送到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);

这里的进度报告略有不同。稍加思考,它可以更像您自己的答案。

【讨论】:

  • 第二个模式看起来不错,但saveChanges 只被调用了一次。您已将 changes 重命名为 allChanges 但它不是更改数组,它是一个 json 对象。在我的原始版本中,我在一个循环中调用它,而在我的第二个版本中,我使用 map 进行迭代,所以我认为这种模式不太正确。我将编辑我的问题。
  • 该模式的本质是saveChanges是递归的。它用saveChanges(allChanges) 调用一次,然后用return saveChanges(failures), tries + 1); 行调用它自己。同意,我对changes 的使用令人困惑,尽管我认为是正确的。我将编辑代码以使事情更清晰。
  • 好的,我理解得更好了,但是您首先需要将一系列更改传递给saveChanges 方法,我已经对其进行了编辑以展示我是如何做到的。
  • 我决定现在一个接一个地提交我的请求,所以我还没有实际测试过这个解决方案。但我必须说我发现这个练习非常有用,我将来可能会回来使用它。 +1 并被接受为答案
【解决方案2】:

这是一个非常粗略的解决方法。

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) //...

【讨论】:

    【解决方案3】:

    这里有两个有用的答案,但在解决了这个问题后,我得出的结论是,立即重试对我来说真的不起作用。

    我想等待第一批完成,然后如果失败是因为超时,请增加超时允许,然后重试失败。 所以我拿了 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();
            }
        });
    

    【讨论】:

    • 这种方法显然可行,但 (a) 不容易扩展以允许多次重试; (b) 可以避免需要外部变量successesretriesfailedChanges。我即将为我自己的答案添加一个更清洁的解决方案。
    猜你喜欢
    • 2016-09-11
    • 2014-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-05
    • 2016-11-27
    • 2015-09-05
    • 2012-03-15
    相关资源
    最近更新 更多