Bug 1018320 - RequestSync API - Patch 4 - sendAsyncMessage used to schedule tasks. r=ehsan, a=bajaj
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 13 Jan 2015 09:53:20 +0000
changeset 237138 dda5de2f1e9fdf270f6cb79b0c60ed69b239e08f
parent 237137 0fd5710ce15e3c46b228a6e4c47dc3277923508a
child 237139 bfc99f8c73b62e51334c2cfd5e952f34e37e7573
push id213
push userryanvm@gmail.com
push dateTue, 24 Feb 2015 00:59:48 +0000
treeherdermozilla-b2g37_v2_2@b5a532c7f606 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, bajaj
bugs1018320
milestone37.0
Bug 1018320 - RequestSync API - Patch 4 - sendAsyncMessage used to schedule tasks. r=ehsan, a=bajaj
dom/requestsync/RequestSyncService.jsm
--- a/dom/requestsync/RequestSyncService.jsm
+++ b/dom/requestsync/RequestSyncService.jsm
@@ -9,16 +9,18 @@ const {classes: Cc, interfaces: Ci, util
 function debug(s) {
   //dump('DEBUG RequestSyncService: ' + s + '\n');
 }
 
 const RSYNCDB_VERSION = 1;
 const RSYNCDB_NAME = "requestSync";
 const RSYNC_MIN_INTERVAL = 100;
 
+const RSYNC_OPERATION_TIMEOUT = 120000 // 2 minutes
+
 Cu.import('resource://gre/modules/IndexedDBHelper.jsm');
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.importGlobalProperties(["indexedDB"]);
 
 
 XPCOMUtils.defineLazyServiceGetter(this, "appsService",
                                    "@mozilla.org/AppsService;1",
@@ -51,16 +53,19 @@ this.RequestSyncService = {
 
   _pendingOperation: false,
   _pendingMessages: [],
 
   _registrations: {},
 
   _wifi: false,
 
+  _activeTask: null,
+  _queuedTasks: [],
+
   // Initialization of the RequestSyncService.
   init: function() {
     debug("init");
 
     this._messages.forEach((function(msgName) {
       ppmm.addMessageListener(msgName, this);
     }).bind(this));
 
@@ -227,29 +232,44 @@ this.RequestSyncService = {
 
     this.removeRegistrationInternal(aTaskName, aKey);
     return true;
   },
 
   removeRegistrationInternal: function(aTaskName, aKey) {
     debug('removeRegistrationInternal');
 
-    if (this._registrations[aKey][aTaskName].timer) {
-      this._registrations[aKey][aTaskName].timer.cancel();
+    let obj = this._registrations[aKey][aTaskName];
+    if (obj.timer) {
+      obj.timer.cancel();
     }
 
+    // It can be that this task has been already schedulated.
+    this.removeTaskFromQueue(obj);
+
+    // It can be that this object is already in scheduled, or in the queue of a
+    // iDB transacation. In order to avoid rescheduling it, we must disable it.
+    obj.active = false;
+
     delete this._registrations[aKey][aTaskName];
 
     // Lets remove the key in case there are not tasks registered.
     for (var key in this._registrations[aKey]) {
       return;
     }
     delete this._registrations[aKey];
   },
 
+  removeTaskFromQueue: function(aObj) {
+    let pos = this._queuedTasks.indexOf(aObj);
+    if (pos != -1) {
+      this._queuedTasks.splice(pos, 1);
+    }
+  },
+
   // The communication from the exposed objects and the service is done using
   // messages. This function receives and processes them.
   receiveMessage: function(aMessage) {
     debug("receiveMessage");
 
     // We cannot process this request now.
     if (this._pendingOperation) {
       this._pendingMessages.push(aMessage);
@@ -505,78 +525,140 @@ this.RequestSyncService = {
     aObj.timer.initWithCallback(function() { self.timeout(aObj); },
                                 aObj.data.minInterval * 1000,
                                 Ci.nsITimer.TYPE_ONE_SHOT);
   },
 
   timeout: function(aObj) {
     debug("timeout");
 
+    if (this._activeTask) {
+      debug("queueing tasks");
+      // We have an active task, let's queue this as next task.
+      if (this._queuedTasks.indexOf(aObj) == -1) {
+        this._queuedTasks.push(aObj);
+      }
+      return;
+    }
+
     let app = appsService.getAppByLocalId(aObj.principal.appId);
     if (!app) {
       dump("ERROR!! RequestSyncService - Failed to retrieve app data from a principal.\n");
       aObj.active = false;
       this.updateObjectInDB(aObj);
       return;
     }
 
     let manifestURL = Services.io.newURI(app.manifestURL, null, null);
     let pageURL = Services.io.newURI(aObj.data.wakeUpPage, null, aObj.principal.URI);
 
     // Maybe need to be rescheduled?
-    if (this.needRescheduling('request-sync', manifestURL, pageURL)) {
+    if (this.hasPendingMessages('request-sync', manifestURL, pageURL)) {
       this.scheduleTimer(aObj);
       return;
     }
 
     aObj.timer = null;
+    this._activeTask = aObj;
 
     if (!manifestURL || !pageURL) {
       dump("ERROR!! RequestSyncService - Failed to create URI for the page or the manifest\n");
       aObj.active = false;
       this.updateObjectInDB(aObj);
       return;
     }
 
-    // Sending the message.
-    systemMessenger.sendMessage('request-sync',
-                                this.createPartialTaskObject(aObj.data),
-                                pageURL, manifestURL);
+    // We don't want to run more than 1 task at the same time. We do this using
+    // the promise created by sendMessage(). But if the task takes more than
+    // RSYNC_OPERATION_TIMEOUT millisecs, we have to ignore the promise and
+    // continue processing other tasks.
+
+    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 
-    // One shot? Then this is not active.
-    aObj.active = !aObj.data.oneShot;
-    aObj.data.lastSync = new Date();
+    let done = false;
+    let self = this;
+    function taskCompleted() {
+      debug("promise or timeout for task calls taskCompleted");
+
+      if (!done) {
+        done = true;
+        self.operationCompleted();
+      }
 
-    let self = this;
-    this.updateObjectInDB(aObj, function() {
-      // SchedulerTimer creates a timer and a nsITimer cannot be cloned. This
-      // is the reason why this operation has to be done after storing the aObj
-      // into IDB.
-      if (!aObj.data.oneShot) {
-        self.scheduleTimer(aObj);
-      }
+      timer.cancel();
+      timer = null;
+    }
+
+    timer.initWithCallback(function() {
+      debug("Task is taking too much, let's ignore the promise.");
+      taskCompleted();
+    }, RSYNC_OPERATION_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
+
+    // Sending the message.
+    let promise =
+      systemMessenger.sendMessage('request-sync',
+                                  this.createPartialTaskObject(aObj.data),
+                                  pageURL, manifestURL);
+
+    promise.then(function() {
+      debug("promise resolved");
+      taskCompleted();
+    }, function() {
+      debug("promise rejected");
+      taskCompleted();
     });
   },
 
-  needRescheduling: function(aMessageName, aManifestURL, aPageURL) {
+  operationCompleted: function() {
+    debug("operationCompleted");
+
+    if (!this._activeTask) {
+      dump("ERROR!! RequestSyncService - OperationCompleted called without an active task\n");
+      return;
+    }
+
+    // One shot? Then this is not active.
+    this._activeTask.active = !this._activeTask.data.oneShot;
+    this._activeTask.data.lastSync = new Date();
+
+    let self = this;
+    this.updateObjectInDB(this._activeTask, function() {
+      // SchedulerTimer creates a timer and a nsITimer cannot be cloned. This
+      // is the reason why this operation has to be done after storing the task
+      // into IDB.
+      if (!self._activeTask.data.oneShot) {
+        self.scheduleTimer(self._activeTask);
+      }
+
+      self.processNextTask();
+    });
+  },
+
+  processNextTask: function() {
+    debug("processNextTask");
+
+    this._activeTask = null;
+
+    if (this._queuedTasks.length == 0) {
+      return;
+    }
+
+    let task = this._queuedTasks.shift();
+    this.timeout(task);
+  },
+
+  hasPendingMessages: function(aMessageName, aManifestURL, aPageURL) {
     let hasPendingMessages =
       cpmm.sendSyncMessage("SystemMessageManager:HasPendingMessages",
                            { type: aMessageName,
                              pageURL: aPageURL.spec,
                              manifestURL: aManifestURL.spec })[0];
 
     debug("Pending messages: " + hasPendingMessages);
-
-    if (hasPendingMessages) {
-      return true;
-    }
-
-    // FIXME: other reasons?
-
-    return false;
+    return hasPendingMessages;
   },
 
   // Update the object into the database.
   updateObjectInDB: function(aObj, aCb) {
     debug("updateObjectInDB");
 
     this.dbTxn('readwrite', function(aStore) {
       aStore.put(aObj, aObj.dbKey);
@@ -644,20 +726,24 @@ this.RequestSyncService = {
   },
 
   wifiStateChanged: function(aEnabled) {
     debug("onWifiStateChanged");
     this._wifi = aEnabled;
 
     if (!this._wifi) {
       // Disable all the wifiOnly tasks.
+      let self = this;
       this.forEachRegistration(function(aObj) {
         if (aObj.data.wifiOnly && aObj.timer) {
           aObj.timer.cancel();
           aObj.timer = null;
+
+          // It can be that this task has been already schedulated.
+          self.removeTaskFromQueue(aObj);
         }
       });
       return;
     }
 
     // Enable all the tasks.
     let self = this;
     this.forEachRegistration(function(aObj) {