Bug 1018320 - RequestSync API - patch 4 - sendAsyncMessage used to schedule tasks, r=ehsan
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 13 Jan 2015 09:53:20 +0000
changeset 223539 9706b535ae6483ef4f53b657ed307c69b733b50d
parent 223538 b729cdfd5a8cabafbf6ec894fdfc33b47cf81687
child 223540 d23d32851ae824599af8f598ae50f6c768b33d49
push id53943
push useramarchesini@mozilla.com
push dateTue, 13 Jan 2015 09:53:46 +0000
treeherdermozilla-inbound@f15ef701bdf2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs1018320
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1018320 - RequestSync API - patch 4 - sendAsyncMessage used to schedule tasks, r=ehsan
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) {