2016-07-25 2 views
0

Breeze를 사용하여 데이터를 저장하고 합리적으로 잘 작동하는 여러 저장 과정에 대한 진행 상황을보고하는 코드가 있습니다. 그러나 때로는 저장 시간이 초과되어 자동으로 한 번 다시 시도하고 싶습니다. (현재 사용자에게 오류가 표시되고 수동으로 다시 시도해야합니다.) 적절한 방법을 찾기 위해 고심하고 있지만 약속으로 인해 혼란 스럽습니다. 나는 모든 HTTP 요청에 제한 시간을 설정하는 HTTP 인터셉터가 :이 시도 된 이유 명확히하기

//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 요청이 차례로 진행되는 상황에서 더 나을 것입니다.이 또한 제가 살펴 봤습니다. https://stackoverflow.com/a/25730751/150342

+0

'$의 q.all'은 모두 상관없이 실패하지 않습니다. 각 약속이 해결되고'$ q.all '전에 오류를 처리하기 전에 잡기 오류를 잡을 수 있습니다. –

+0

내가 의미하는 바는 간단한 약속을 배열에 푸는 대신 * n * 번 시도하여 그 약속을 해결하는 함수를 푸시합니다. 실패하면 모든 것이 실패하고'$ q .all' 기능. –

+0

사실,'$ q.all '이 항상 성공한다는 것을 발견했습니다. 그래서'then '에 전달 된 첫 번째 함수에서'fails.count()'를 확인합니다. 내가 다른 곳에서 뭔가 잘못 됐어? – Colin

답변

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

을 개정하여 루프 : 정의

  • 사용 Function.prototype.bind() 각 함수로 저장 bound-in 매개 변수를 사용합니다.
  • 이 함수를 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()의 downsteam을 다시 시도 할 수 있습니다 이유는 분명하지 않다.다시 시도하기 전에 약간의 지연을 도입하는 문제라면, 가장 간단한 방법은 위의 패턴 내에서 수행하는 것입니다. $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 (failure), tries + 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

두 가지 유용한 답변이 있지만이 작업을 통해 즉각적인 재 시도가 실제로 저에게 효과가 없다고 결론을 냈습니다.

첫 번째 일괄 처리가 완료 될 때까지 기다리고 싶습니다. 그러면 시간 초과로 인해 실패한 경우 실패를 다시 시도하기 전에 시간 초과 한도를 늘리십시오. 그래서 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(); 
     } 
    }); 
+0

이 방법은 분명히 효과적 일 수 있지만 (a) 두 번 이상 재 시도 할 수 있도록 쉽게 확장 할 수는 없습니다. (b) 외부 덩어리의 필요성, '성공', '재시도', '실패한 변화'는 피할 수 있습니다. 내 자신의 대답에 깨끗한 솔루션을 추가하려고합니다. –

관련 문제