如何使用$ q.all

时间:2016-07-25 11:27:34

标签: promise breeze angular-promise

我有一些使用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

3 个答案:

答案 0 :(得分:2)

$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);

进度报告在这里略有不同。有了更多的想法,它可以更像你自己的答案。

答案 1 :(得分:1)

这是如何解决它的一个非常粗略的想法。

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

答案 2 :(得分:0)

这里有两个有用的答案,但经过这一切,我得出的结论是,立即重试并不适合我。

我想等待第一批完成,然后如果失败是由于超时,请在重试失败之前增加超时限额。 所以我拿了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();
        }
    });