2016-07-25 26 views
0

Ich habe einen Code, der Daten mit Breeze speichert und den Fortschritt über mehrere Speichervorgänge meldet, der einigermaßen gut funktioniert. Manchmal wird ein Speicher jedoch Timeout, und ich möchte es einmal automatisch versuchen. (Zur Zeit wird dem Benutzer ein Fehler angezeigt und er muss es erneut manuell versuchen) Ich habe Mühe, einen geeigneten Weg zu finden, aber ich bin verwirrt von Versprechungen, also würde ich mich über Hilfe freuen. Hier ist mein Code:

//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(); 
    } 
}); 

bearbeiten zu klären, warum ich dies versucht haben: ich einen http-Abfangjäger, die ein Timeout auf alle HTTP-Anfragen setzt. Wenn eine Anforderung eine Zeitüberschreitung aufweist, wird das Zeitlimit nach oben angepasst. Dem Benutzer wird eine Fehlernachricht angezeigt, in der er ihnen mit einer längeren Wartezeit erneut versuchen kann, falls sie dies wünschen.

Das Senden aller Änderungen in einer HTTP-Anfrage sieht so aus, als könnte es einige Minuten dauern, also habe ich beschlossen, die Änderungen in mehrere HTTP-Anfragen zu zerlegen und den Fortschritt zu melden, wenn jede Anfrage erfolgreich ist.

Jetzt können einige Anforderungen im Batch Timeout sein und einige nicht.

Dann hatte ich die helle Idee, dass ich eine niedrige Zeitüberschreitung für die HTTP-Anfrage festlegen würde, um mit zu beginnen und es automatisch zu erhöhen. Der Stapel wird jedoch asynchron mit derselben Zeitüberschreitungseinstellung gesendet, und die Zeit wird für jeden Fehler angepasst. Das ist nicht gut.

Um dies zu lösen, wollte ich die Timeout-Einstellung verschieben, nachdem der Stapel abgeschlossen ist, dann auch alle Anfragen wiederholen.

Um ehrlich zu sein, bin ich nicht so sicher, dass eine automatische Timeout-Anpassung und Wiederholung ist so eine großartige Idee an erster Stelle. Und selbst wenn es so wäre, wäre es wahrscheinlich besser in einer Situation, in der http-Anfragen nacheinander gemacht wurden - was ich auch gesehen habe: https://stackoverflow.com/a/25730751/150342

+0

'$ q.all' wird scheitern ganz egal was passiert. Sie könnten den Fehler abfangen, bevor jedes Versprechen gelöst wird und die Fehler vor dem '$ q.all' behandelt werden. –

+0

Was ich meine, ist, anstatt eine einfache Versprechen in das Array zu schieben, eine Funktion, die * n * Mal versucht, dieses Versprechen zu lösen, wenn das fehlschlägt, dann alles scheitert, sollten Sie nur das Endergebnis auf dem $ q behandeln .all' Funktion. –

+0

Tatsächlich habe ich festgestellt, dass '$ q.all' immer erfolgreich ist - deshalb überprüfe ich' fails.count() 'in der ersten Funktion, die an' then' übergeben wird. Habe ich irgendwo anders was falsch gemacht? – Colin

Antwort

2

Orchestrierung Wiederholungen hinter $q.all() möglich ist, aber wäre in der Tat sehr chaotisch sein. Es ist viel einfacher, Wiederholungen durchzuführen, bevor die Versprechungen zusammengefasst werden.

Sie könnten Verschlüsse nutzen und versuchen Sie es erneut Zähler, aber es ist sauberer einen Haken Kette zu bauen:

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

Nun ändern Sie Ihre for-Schleife:

  • Verwendung Function.prototype.bind() jeweils definieren als eine Funktion speichern mit eingebundenen Parametern.
  • übergeben Sie diese Funktion an retry().
  • drücken Sie das Versprechen von retry().then(...) auf die promises Array zurückgegeben.
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

Es ist nicht klar, warum Sie downs von $q.all() wiederholen möchten.Wenn es darum geht, eine gewisse Verzögerung vor dem erneuten Versuch einzuführen, wäre der einfachste Weg, das obige Muster zu verwenden.

Wenn jedoch erneut versucht hinter $q.all() eine feste Voraussetzung ist, ist hier eine cleanish rekursive Lösung, die eine beliebige Anzahl von Wiederholungen erlaubt, mit minimalem Bedarf an äußeren Vars:

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

Fortschritt Berichterstattung hier etwas anders. Mit etwas mehr Nachdenken könnte es mehr wie in Ihrer eigenen Antwort gemacht werden.

+0

Dieses zweite Muster sieht gut aus, aber 'saveChanges' wird nur einmal aufgerufen. Sie haben 'changes' in' allChanges' umbenannt, aber es ist kein Array von Änderungen, es ist ein json-Objekt. In meinem Original habe ich es innerhalb einer Schleife aufgerufen und in meiner zweiten Version habe ich map verwendet, um zu iterieren, also glaube ich nicht, dass das Muster richtig ist. Ich werde meine Frage bearbeiten. – Colin

+0

Die Essenz des Musters ist, dass 'saveChanges' rekursiv ist. Es wird einmal mit 'saveChanges (allChanges)' aufgerufen und ruft sich selbst mit der Zeile 'return saveChanges (failures), tryes + 1);'. Einverstanden, meine Verwendung von 'Änderungen' ist verwirrend, obwohl ich richtig glaube. Ich werde den Code bearbeiten, um die Dinge klarer zu machen. –

+0

Ok, ich verstehe es besser, aber Sie müssen eine Reihe von Änderungen an der 'saveChanges' Methode an erster Stelle übergeben, ich habe es bearbeitet, um zu zeigen, wie ich das mache. – Colin

1

Dies ist eine sehr grobe Idee, wie man es löst.

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

Zwei nützliche Antworten hier, aber nachdem ich das durchgearbeitet habe, bin ich zu dem Schluss gekommen, dass sofortige Wiederholungen nicht wirklich funktionieren werden.

Ich möchte warten, bis der erste Batch abgeschlossen ist. Wenn die Fehler aufgrund von Timeouts auftreten, erhöhen Sie die Zeitlimitüberschreitung, bevor Sie Fehler erneut versuchen. Also nahm ich Juan Stiza Beispiel und änderte es, um zu tun, was ich will. wiederholen heißt Ausfälle mit $ q.all

jetzt Ihr Code wie folgt aussieht:

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

Dieser Ansatz wird klar funktionieren, aber (a) ist nicht leicht erweiterbar, um mehr als einen Wiederholungsversuch zuzulassen; (b) die Notwendigkeit für äußere Variablen, "Erfolge", "Wiederholungen", "fehlgeschlagene Änderungen" können vermieden werden. Ich bin dabei, meiner eigenen Antwort eine sauberere Lösung hinzuzufügen. –