如何使用 $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();
        }
    });