Bug 1012535 - Handle translation service unavailable errors, r=felipe, a=gavin.
authorFlorian Quèze <florian@queze.net>
Mon, 16 Jun 2014 17:50:08 +0200
changeset 208162 ee3af593dcb88bb804f9835b5452375a2a191bd6
parent 208161 011d04986808d73e2d8ef5bb790ad1c3203e89d8
child 208163 883d156210cff8b207538f16531755d6bbb525fe
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe, gavin
bugs1012535
milestone32.0a2
Bug 1012535 - Handle translation service unavailable errors, r=felipe, a=gavin.
browser/components/translation/BingTranslator.jsm
browser/components/translation/Translation.jsm
browser/components/translation/TranslationContentHandler.jsm
browser/components/translation/translation-infobar.xml
--- a/browser/components/translation/BingTranslator.jsm
+++ b/browser/components/translation/BingTranslator.jsm
@@ -41,16 +41,17 @@ const MAX_REQUESTS = 15;
  *                             task is finished.
  */
 this.BingTranslation = function(translationDocument, sourceLanguage, targetLanguage) {
   this.translationDocument = translationDocument;
   this.sourceLanguage = sourceLanguage;
   this.targetLanguage = targetLanguage;
   this._pendingRequests = 0;
   this._partialSuccess = false;
+  this._serviceUnavailable = false;
   this._translatedCharacterCount = 0;
 };
 
 this.BingTranslation.prototype = {
   /**
    * Performs the translation, splitting the document into several chunks
    * respecting the data limits of the API.
    *
@@ -75,59 +76,87 @@ this.BingTranslation.prototype = {
         let request = this._generateNextTranslationRequest(currentIndex);
 
         // Create a real request to the server, and put it on the
         // pending requests list.
         let bingRequest = new BingRequest(request.data,
                                           this.sourceLanguage,
                                           this.targetLanguage);
         this._pendingRequests++;
-        bingRequest.fireRequest().then(this._chunkCompleted.bind(this));
+        bingRequest.fireRequest().then(this._chunkCompleted.bind(this),
+                                       this._chunkFailed.bind(this));
 
         currentIndex = request.lastIndex;
         if (request.finished) {
           break;
         }
       }
 
       return this._onFinishedDeferred.promise;
     }.bind(this));
   },
 
   /**
-   * Function called when a request sent to the server is completed.
-   * This function handles determining if the response was successful or not,
-   * calling the function to parse the result, and resolving the promise
-   * returned by the public `translate()` method when all chunks are completed.
+   * Function called when a request sent to the server completed successfully.
+   * This function handles calling the function to parse the result and the
+   * function to resolve the promise returned by the public `translate()`
+   * method when there's no pending request left.
    *
    * @param   request   The BingRequest sent to the server.
    */
   _chunkCompleted: function(bingRequest) {
-     this._pendingRequests--;
-     if (bingRequest.requestSucceeded &&
-         this._parseChunkResult(bingRequest)) {
-       // error on request
-       this._partialSuccess = true;
-       // Count the number of characters successfully translated.
-       this._translatedCharacterCount += bingRequest.characterCount;
-     }
+    if (this._parseChunkResult(bingRequest)) {
+      this._partialSuccess = true;
+      // Count the number of characters successfully translated.
+      this._translatedCharacterCount += bingRequest.characterCount;
+    }
+
+    this._checkIfFinished();
+  },
 
+  /**
+   * Function called when a request sent to the server has failed.
+   * This function handles deciding if the error is transient or means the
+   * service is unavailable (zero balance on the key) and calling the
+   * function to resolve the promise returned by the public `translate()`
+   * method when there's no pending request left.
+   *
+   * @param   aError   [optional] The RESTRequest that failed.
+   */
+  _chunkFailed: function(aError) {
+    if (aError instanceof RESTRequest &&
+        aError.response.status == 400) {
+      let body = aError.response.body;
+      if (body.contains("TranslateApiException") && body.contains("balance"))
+        this._serviceUnavailable = true;
+    }
+
+    this._checkIfFinished();
+  },
+
+  /**
+   * Function called when a request sent to the server has completed.
+   * This function handles resolving the promise
+   * returned by the public `translate()` method when all chunks are completed.
+   */
+  _checkIfFinished: function() {
     // Check if all pending requests have been
     // completed and then resolves the promise.
     // If at least one chunk was successful, the
     // promise will be resolved positively which will
     // display the "Success" state for the infobar. Otherwise,
     // the "Error" state will appear.
-    if (this._pendingRequests == 0) {
+    if (--this._pendingRequests == 0) {
       if (this._partialSuccess) {
         this._onFinishedDeferred.resolve({
           characterCount: this._translatedCharacterCount
         });
       } else {
-        this._onFinishedDeferred.reject("failure");
+        let error = this._serviceUnavailable ? "unavailable" : "failure";
+        this._onFinishedDeferred.reject(error);
       }
     }
   },
 
   /**
    * This function parses the result returned by Bing's Http.svc API,
    * which is a XML file that contains a number of elements. To our
    * particular interest, the only part of the response that matters
@@ -275,34 +304,26 @@ BingRequest.prototype = {
       requestString += '</Texts>' +
           '<To>' + this.targetLanguage + '</To>' +
         '</TranslateArrayRequest>';
 
       let utf8 = CommonUtils.encodeUTF8(requestString);
 
       let deferred = Promise.defer();
       request.post(utf8, function(err) {
+        if (request.error || !request.response.success)
+          deferred.reject(request);
+
         deferred.resolve(this);
       }.bind(this));
 
       this.networkRequest = request;
       return deferred.promise;
     }.bind(this));
-  },
-
-  /**
-   * Checks if the request succeeded. Only valid
-   * after the request has finished.
-   *
-   * @returns    True if the request succeeded.
-   */
-  get requestSucceeded() {
-    return !this.networkRequest.error &&
-            this.networkRequest.response.success;
-   }
+  }
 };
 
 /**
  * Authentication Token manager for the API
  */
 let BingTokenManager = {
   _currentToken: null,
   _currentExpiryTime: 0,
@@ -354,16 +375,22 @@ let BingTokenManager = {
       BingTokenManager._pendingRequest = null;
 
       if (err) {
         deferred.reject(err);
       }
 
       try {
         let json = JSON.parse(this.response.body);
+
+        if (json.error) {
+          deferred.reject(json.error);
+          return;
+        }
+
         let token = json.access_token;
         let expires_in = json.expires_in;
         BingTokenManager._currentToken = token;
         BingTokenManager._currentExpiryTime = new Date(Date.now() + expires_in * 1000);
         deferred.resolve(token);
       } catch (e) {
         deferred.reject(e);
       }
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -23,16 +23,19 @@ const DAILY_LAST_TEXT_FIELD = {type: Met
 const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
 
 
 this.Translation = {
   STATE_OFFER: 0,
   STATE_TRANSLATING: 1,
   STATE_TRANSLATED: 2,
   STATE_ERROR: 3,
+  STATE_UNAVAILABLE: 4,
+
+  serviceUnavailable: false,
 
   supportedSourceLanguages: ["en", "zh", "ja", "es", "de", "fr", "ru", "ar", "ko", "pt"],
   supportedTargetLanguages: ["en", "pl", "tr", "vi"],
 
   _defaultTargetLanguage: "",
   get defaultTargetLanguage() {
     if (!this._defaultTargetLanguage) {
       this._defaultTargetLanguage = Cc["@mozilla.org/chrome/chrome-registry;1"]
@@ -166,16 +169,21 @@ TranslationUI.prototype = {
     let notificationBox = this.notificationBox;
     let notif = notificationBox.appendNotification("", "translation", null,
                                                    notificationBox.PRIORITY_INFO_HIGH);
     notif.init(this);
     return notif;
   },
 
   shouldShowInfoBar: function(aURI) {
+    // Never show the infobar automatically while the translation
+    // service is temporarily unavailable.
+    if (Translation.serviceUnavailable)
+      return false;
+
     // Check if we should never show the infobar for this language.
     let neverForLangs =
       Services.prefs.getCharPref("browser.translation.neverForLanguages");
     if (neverForLangs.split(",").indexOf(this.detectedLanguage) != -1)
       return false;
 
     // or if we should never show the infobar for this domain.
     let perms = Services.perms;
@@ -205,16 +213,19 @@ TranslationUI.prototype = {
         if (msg.data.success) {
           this.originalShown = false;
           this.state = Translation.STATE_TRANSLATED;
           this.showURLBarIcon();
 
           // Record the number of characters translated.
           TranslationHealthReport.recordTranslation(msg.data.from, msg.data.to,
                                                     msg.data.characterCount);
+        } else if (msg.data.unavailable) {
+          Translation.serviceUnavailable = true;
+          this.state = Translation.STATE_UNAVAILABLE;
         } else {
           this.state = Translation.STATE_ERROR;
         }
         break;
     }
   }
 };
 
--- a/browser/components/translation/TranslationContentHandler.jsm
+++ b/browser/components/translation/TranslationContentHandler.jsm
@@ -137,17 +137,20 @@ TranslationContentHandler.prototype = {
               from: msg.data.from,
               to: msg.data.to,
               success: true
             });
             translationDocument.showTranslation();
           },
           error => {
             translationDocument.translationError = true;
-            this.global.sendAsyncMessage("Translation:Finished", {success: false});
+            let data = {success: false};
+            if (error == "unavailable")
+              data.unavailable = true;
+            this.global.sendAsyncMessage("Translation:Finished", data);
           }
         );
         break;
       }
 
       case "Translation:ShowOriginal":
         this.global.content.translationDocument.showOriginal();
         break;
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -80,16 +80,22 @@
               <xul:label class="translate-infobar-element"
                          value="&translation.errorTranslating.label;"/>
               <xul:button class="translate-infobar-element"
                           label="&translation.tryAgain.button;"
                           anonid="tryAgain"
                           oncommand="document.getBindingParent(this).translate();"/>
             </xul:hbox>
 
+            <!-- unavailable -->
+            <xul:vbox class="translation-unavailable" pack="center">
+              <xul:label class="translate-infobar-element"
+                         value="&translation.serviceUnavailable.label;"/>
+            </xul:vbox>
+
           </xul:deck>
           <xul:spacer flex="1"/>
 
           <xul:button type="menu"
                       class="translate-infobar-element options-menu-button"
                       anonid="options"
                       label="&translation.options.menu;">
             <xul:menupopup onpopupshowing="document.getBindingParent(this).optionsShowing();">
@@ -141,16 +147,21 @@
           ]]>
         </setter>
       </property>
 
       <method name="init">
         <parameter name="aTranslation"/>
         <body>
           <![CDATA[
+            if (Translation.serviceUnavailable) {
+              this.state = Translation.STATE_UNAVAILABLE;
+              return;
+            }
+
             this.translation = aTranslation;
             let bundle = Cc["@mozilla.org/intl/stringbundle;1"]
                            .getService(Ci.nsIStringBundleService)
                            .createBundle("chrome://global/locale/languageNames.properties");
 
             // Fill the lists of supported source languages.
             let detectedLanguage = this._getAnonElt("detectedLanguage");
             let fromLanguage = this._getAnonElt("fromLanguage");