2016-07-25 11 views
0

У меня есть код, который сохраняет данные с помощью 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

+0

'$ q.all' не удастся вообще ни на что. Вы можете сделать ошибку, прежде чем каждое обещание разрешит и обработает ошибки до '$ q.all'. –

+0

То, что я имею в виду, вместо того, чтобы проталкивать простое обещание в массив, нажимать функцию, которая пытается * n * раз решить это обещание, если это не удастся, тогда все провалится, вы должны обрабатывать только конечный результат на '$ q .все' функция. –

+0

На самом деле, я обнаружил, что '$ q.all' всегда преуспевает - вот почему я проверяю' fail.count() 'в первой функции, переданной' then'. У меня что-то случилось в другом месте? – Colin

ответ

2

Orchestrating вниз по течению от повторных попыток $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; 
} 

Теперь изменить ваш цикл:

  • использование 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 

EDIT

Это не понятно, почему вы, возможно, захотите повторить downsteam из $q.all().Если перед повторной попыткой ввести некоторую задержку, самый простой способ - сделать в рамках вышеприведенного шаблона.

Однако, если повторная попытка ниже $q.all() является твердым требованием, вот cleanish рекурсивным решения, которое позволяет любому числу повторных попыток, с минимальной потребностью для внешнего Варса:

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

отчетности Прогресса немного отличается здесь. С немного больше думал, что это может быть больше похоже на ваш собственный ответ.

+0

Этот второй шаблон выглядит отлично, но 'saveChanges' вызывается только один раз. Вы переименовали 'changes' в' allChanges', но это не массив изменений, это json-объект. В моем оригинале я назвал его внутри цикла, и в моей второй версии я использовал карту для итерации, поэтому я не думаю, что шаблон совершенно прав. Я отредактирую свой вопрос. – Colin

+0

Суть шаблона заключается в том, что 'saveChanges' является рекурсивным. Он вызывается один раз с помощью 'saveChanges (allChanges)', затем вызывает себя с помощью строки 'return saveChanges (failures), try + 1);'. Согласившись, мое использование «изменений» сбивает с толку, хотя я считаю правильным. Я отредактирую код, чтобы сделать все более ясным. –

+0

Хорошо, я понимаю это лучше, но вам нужно передать массив изменений в метод 'saveChanges', я отредактировал его, чтобы показать, как я это делаю. – Colin

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

Два полезных ответа здесь, но, проработав это, я пришел к выводу, что немедленные попытки не будут работать для меня.

Я хочу дождаться завершения первой партии, а затем, если сбои из-за тайм-аутов, увеличьте допустимое время ожидания перед повторным сбоем. Итак, я взял пример Хуана Стизы и изменил его, чтобы делать то, что хочу. то есть повторить неудачи с $ 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(); 
     } 
    }); 
+0

Этот подход будет явно работать, но (а) не может быть расширен, чтобы допускать более одного повтора; (б) можно избежать необходимости в внешних варах, «успехах», «попытках», «failChanges». Я собираюсь добавить более чистое решение для своего собственного ответа. –

 Смежные вопросы

  • Нет связанных вопросов^_^