Bug 1150812 - Add Http2 Push service. r=nsm, r=mt
authorDragana Damjanovic <dd.mozilla@gmail.com>
Wed, 03 Jun 2015 08:06:00 -0400
changeset 247025 db9050215b8a3ee9c044b9ffce055a399b7d7b34
parent 247024 036d9abeb00c1de853fc4b04fc43c9c793038cb3
child 247026 4c897e262cd53d5cc043927ae1c75be20f395205
push id28848
push userryanvm@gmail.com
push dateWed, 03 Jun 2015 20:00:13 +0000
treeherdermozilla-central@0920f2325a6d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnsm, mt
bugs1150812
milestone41.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 1150812 - Add Http2 Push service. r=nsm, r=mt
dom/push/PushDB.jsm
dom/push/PushService.jsm
dom/push/PushServiceHttp2.jsm
dom/push/PushServiceWebSocket.jsm
dom/push/moz.build
dom/push/test/xpcshell/test_clearAll_successful.js
dom/push/test/xpcshell/test_notification_duplicate.js
dom/push/test/xpcshell/test_notification_error.js
dom/push/test/xpcshell/test_notification_incomplete.js
dom/push/test/xpcshell/test_notification_version_string.js
dom/push/test/xpcshell/test_register_case.js
dom/push/test/xpcshell/test_register_flush.js
dom/push/test/xpcshell/test_register_invalid_channel.js
dom/push/test/xpcshell/test_register_invalid_endpoint.js
dom/push/test/xpcshell/test_register_success.js
dom/push/test/xpcshell/test_register_timeout.js
dom/push/test/xpcshell/test_unregister_error.js
dom/push/test/xpcshell/test_unregister_invalid_json.js
dom/push/test/xpcshell/test_unregister_success.js
modules/libpref/init/all.js
--- a/dom/push/PushDB.jsm
+++ b/dom/push/PushDB.jsm
@@ -40,54 +40,53 @@ this.PushDB.prototype = {
 
   upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
     if (this._schemaFunction) {
       this._schemaFunction(aTransaction, aDb, aOldVersion, aNewVersion, this);
     }
   },
 
   /*
-   * @param aChannelRecord
+   * @param aRecord
    *        The record to be added.
    */
 
-  put: function(aChannelRecord) {
-    debug("put()" + JSON.stringify(aChannelRecord));
+  put: function(aRecord) {
+    debug("put()" + JSON.stringify(aRecord));
 
     return new Promise((resolve, reject) =>
       this.newTxn(
         "readwrite",
         this._dbStoreName,
         function txnCb(aTxn, aStore) {
-          debug("Going to put " + aChannelRecord.channelID);
-          aStore.put(aChannelRecord).onsuccess = function setTxnResult(aEvent) {
+          aStore.put(aRecord).onsuccess = function setTxnResult(aEvent) {
             debug("Request successful. Updated record ID: " +
                   aEvent.target.result);
           };
         },
         resolve,
         reject
       )
     );
   },
 
   /*
-   * @param aChannelID
+   * @param aKeyID
    *        The ID of record to be deleted.
    */
-  delete: function(aChannelID) {
+  delete: function(aKeyID) {
     debug("delete()");
 
     return new Promise((resolve, reject) =>
       this.newTxn(
         "readwrite",
         this._dbStoreName,
         function txnCb(aTxn, aStore) {
-          debug("Going to delete " + aChannelID);
-          aStore.delete(aChannelID);
+          debug("Going to delete " + aKeyID);
+          aStore.delete(aKeyID);
         },
         resolve,
         reject
       )
     );
   },
 
   clearAll: function clear() {
@@ -122,27 +121,27 @@ this.PushDB.prototype = {
           };
         },
         resolve,
         reject
       )
     );
   },
 
-  getByChannelID: function(aChannelID) {
-    debug("getByChannelID()");
+  getByKeyID: function(aKeyID) {
+    debug("getByKeyID()");
 
     return new Promise((resolve, reject) =>
       this.newTxn(
         "readonly",
         this._dbStoreName,
         function txnCb(aTxn, aStore) {
           aTxn.result = undefined;
 
-          aStore.get(aChannelID).onsuccess = function setTxnResult(aEvent) {
+          aStore.get(aKeyID).onsuccess = function setTxnResult(aEvent) {
             aTxn.result = aEvent.target.result;
             debug("Fetch successful " + aEvent.target.result);
           };
         },
         resolve,
         reject
       )
     );
@@ -166,18 +165,18 @@ this.PushDB.prototype = {
           };
         },
         resolve,
         reject
       )
     );
   },
 
-  getAllChannelIDs: function() {
-    debug("getAllChannelIDs()");
+  getAllKeyIDs: function() {
+    debug("getAllKeyIDs()");
 
     return new Promise((resolve, reject) =>
       this.newTxn(
         "readonly",
         this._dbStoreName,
         function txnCb(aTxn, aStore) {
           aStore.mozGetAll().onsuccess = function(event) {
             aTxn.result = event.target.result;
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -22,19 +22,20 @@ const Cr = Components.results;
 const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 const {PushServiceWebSocket} = Cu.import("resource://gre/modules/PushServiceWebSocket.jsm");
+const {PushServiceHttp2} = Cu.import("resource://gre/modules/PushServiceHttp2.jsm");
 
 // Currently supported protocols: WebSocket.
-const CONNECTION_PROTOCOLS = [PushServiceWebSocket];
+const CONNECTION_PROTOCOLS = [PushServiceWebSocket, PushServiceHttp2];
 
 XPCOMUtils.defineLazyModuleGetter(this, "AlarmService",
                                   "resource://gre/modules/AlarmService.jsm");
 
 this.EXPORTED_SYMBOLS = ["PushService"];
 
 const prefs = new Preferences("dom.push.");
 // Set debug first so that all debugging actually works.
@@ -155,21 +156,21 @@ this.PushService = {
       this._setState(PUSH_SERVICE_ACTIVE_OFFLINE);
     } else {
       if (this._state == PUSH_SERVICE_RUNNING) {
         // PushService was not in the offline state, but got notification to
         // go online (a offline notification has not been sent).
         // Disconnect first.
         this._service.disconnect();
       }
-      this._db.getAllChannelIDs()
-        .then(channelIDs => {
-          if (channelIDs.length > 0) {
+      this._db.getAllKeyIDs()
+        .then(keyIDs => {
+          if (keyIDs.length > 0) {
             // if there are request waiting
-            this._service.connect(channelIDs);
+            this._service.connect(keyIDs);
           }
         });
       this._setState(PUSH_SERVICE_RUNNING);
     }
   },
 
   _changeStateConnectionEnabledEvent: function(enabled) {
     debug("changeStateConnectionEnabledEvent: " + enabled);
@@ -240,17 +241,17 @@ this.PushService = {
         if (!scope) {
           debug("webapps-clear-data: No scope found for " + data.appId);
           return;
         }
 
         this._db.getByScope(scope)
           .then(record =>
             Promise.all([
-              this._db.delete(record.channelID),
+              this._db.delete(this._service.getKeyFromRecord(record)),
               this._sendRequest("unregister", record)
             ])
           ).catch(_ => {
             debug("webapps-clear-data: Error in getByScope(" + scope + ")");
           });
 
         break;
     }
@@ -495,18 +496,21 @@ this.PushService = {
     if (event != CHANGING_SERVICE_EVENT) {
       let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
                    .getService(Ci.nsIMessageBroadcaster);
 
       kCHILD_PROCESS_MESSAGES.forEach(
         msgName => ppmm.removeMessageListener(msgName, this)
       );
     }
+
     this._service.disconnect();
     this._service.uninit();
+    this._service = null;
+    this.stopAlarm();
 
     if (!this._db) {
       return Promise.resolve();
     }
     if (event == UNINIT_EVENT) {
       // If it is uninitialized just close db.
       this._db.close();
       this._db = null;
@@ -571,17 +575,17 @@ this.PushService = {
 
     this._settingAlarm = true;
     AlarmService.add(
       {
         date: new Date(Date.now() + delay),
         ignoreTimezone: true
       },
       () => {
-        if (this._service) {
+        if (this._state > PUSH_SERVICE_ACTIVATING) {
           this._service.onAlarmFired();
         }
       }, (alarmID) => {
         this._alarmID = alarmID;
         debug("Set alarm " + delay + " in the future " + this._alarmID);
         this._settingAlarm = false;
 
         if (this._waitingForAlarmSet) {
@@ -605,17 +609,17 @@ this.PushService = {
       .then(_ => this._db.drop());
   },
 
   // Fires a push-register system message to all applications that have
   // registration.
   _notifyAllAppsRegister: function() {
     debug("notifyAllAppsRegister()");
     // records are objects describing the registration as stored in IndexedDB.
-    return this._db.getAllChannelIDs()
+    return this._db.getAllKeyIDs()
       .then(records => {
         let scopes = new Set();
         for (let record of records) {
           scopes.add(record.scope);
         }
         let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
                          .getService(Ci.nsIMessageListenerManager);
         for (let scope of scopes) {
@@ -631,18 +635,60 @@ this.PushService = {
             scope: scope
           };
 
           globalMM.broadcastAsyncMessage('pushsubscriptionchange', data);
         }
       });
   },
 
+  dropRegistrationAndNotifyApp: function(aKeyId) {
+    return this._db.getByKeyID(aKeyId)
+      .then(record => {
+        let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
+                         .getService(Ci.nsIMessageListenerManager);
+        Services.obs.notifyObservers(
+          null,
+          "push-subscription-change",
+          record.scope
+        );
+
+        let data = {
+          originAttributes: {}, // TODO bug 1166350
+          scope: record.scope
+        };
+
+        globalMM.broadcastAsyncMessage('pushsubscriptionchange', data);
+      })
+      .then(_ => this._db.delete(aKeyId));
+  },
+
+  updateRegistrationAndNotifyApp: function(aOldKey, aRecord) {
+    return this._db.delete(aOldKey)
+      .then(_ => this._db.put(aRecord)
+        .then(record => {
+          let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
+                           .getService(Ci.nsIMessageListenerManager);
+          Services.obs.notifyObservers(
+            null,
+            "push-subscription-change",
+            record.scope
+          );
+
+          let data = {
+            originAttributes: {}, // TODO bug 1166350
+            scope: record.scope
+          };
+
+          globalMM.broadcastAsyncMessage('pushsubscriptionchange', data);
+        }));
+  },
+
   receivedPushMessage: function(aPushRecord, message) {
-    this._updatePushRecord(aPushRecord)
+    this._db.put(aPushRecord)
       .then(_ => this._notifyApp(aPushRecord, message));
   },
 
   _notifyApp: function(aPushRecord, message) {
     if (!aPushRecord || !aPushRecord.scope) {
       debug("notifyApp() something is undefined.  Dropping notification: " +
         JSON.stringify(aPushRecord) );
       return;
@@ -679,27 +725,22 @@ this.PushService = {
       scope: aPushRecord.scope
     };
 
     let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
                  .getService(Ci.nsIMessageListenerManager);
     globalMM.broadcastAsyncMessage('push', data);
   },
 
-  _updatePushRecord: function(aPushRecord) {
-    debug("updatePushRecord()");
-    return this._db.put(aPushRecord);
+  getByKeyID: function(aKeyID) {
+    return this._db.getByKeyID(aKeyID);
   },
 
-  getByChannelID: function(aChannelID) {
-    return this._db.getByChannelID(aChannelID);
-  },
-
-  getAllChannelIDs: function() {
-    return this._db.getAllChannelIDs();
+  getAllKeyIDs: function() {
+    return this._db.getAllKeyIDs();
   },
 
   _sendRequest(action, aRecord) {
     if (this._state == PUSH_SERVICE_CONNECTION_DISABLE) {
       return Promise.reject({state: 0, error: "Service not active"});
     } else if (this._state == PUSH_SERVICE_ACTIVE_OFFLINE) {
       return Promise.reject({state: 0, error: "NetworkError"});
     }
@@ -748,17 +789,17 @@ this.PushService = {
 
   /**
    * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained
    * from _service.request, causing the promise to be rejected instead.
    */
   _onRegisterSuccess: function(aRecord) {
     debug("_onRegisterSuccess()");
 
-    return this._updatePushRecord(aRecord)
+    return this._db.put(aRecord)
       .then(_ => aRecord, error => {
         // Unable to save.
         this._sendRequest("unregister", aRecord);
         throw error;
       });
   },
 
   /**
@@ -788,20 +829,18 @@ this.PushService = {
     this[aMessage.name.slice("Push:".length).toLowerCase()](json, mm);
   },
 
   register: function(aPageRecord, aMessageManager) {
     debug("register(): " + JSON.stringify(aPageRecord));
 
     this._register(aPageRecord)
       .then(pushRecord => {
-        let message = {
-          requestID: aPageRecord.requestID,
-          pushEndpoint: pushRecord.pushEndpoint
-        };
+        let message = this._service.prepareRegister(pushRecord);
+        message.requestID = aPageRecord.requestID;
         aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
       }, error => {
         let message = {
           requestID: aPageRecord.requestID,
           error
         };
         aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
       });
@@ -813,48 +852,49 @@ this.PushService = {
    * Why is the record being deleted from the local database before the server
    * is told?
    *
    * Unregistration is for the benefit of the app and the AppServer
    * so that the AppServer does not keep pinging a channel the UserAgent isn't
    * watching The important part of the transaction in this case is left to the
    * app, to tell its server of the unregistration.  Even if the request to the
    * PushServer were to fail, it would not affect correctness of the protocol,
-   * and the server GC would just clean up the channelID eventually.  Since the
-   * appserver doesn't ping it, no data is lost.
+   * and the server GC would just clean up the channelID/subscription
+   * eventually.  Since the appserver doesn't ping it, no data is lost.
    *
    * If rather we were to unregister at the server and update the database only
    * on success: If the server receives the unregister, and deletes the
-   * channelID, but the response is lost because of network failure, the
-   * application is never informed. In addition the application may retry the
-   * unregister when it fails due to timeout at which point the server will say
-   * it does not know of this unregistration.  We'll have to make the
-   * registration/unregistration phases have retries and attempts to resend
-   * messages from the server, and have the client acknowledge. On a server,
-   * data is cheap, reliable notification is not.
+   * channelID/subscription, but the response is lost because of network
+   * failure, the application is never informed. In addition the application may
+   * retry the unregister when it fails due to timeout (websocket) or any other
+   * reason at which point the server will say it does not know of this
+   * unregistration.  We'll have to make the registration/unregistration phases
+   * have retries and attempts to resend messages from the server, and have the
+   * client acknowledge. On a server, data is cheap, reliable notification is
+   * not.
    */
   _unregister: function(aPageRecord) {
     debug("unregisterWithServer()");
 
     if (!aPageRecord.scope) {
       return Promise.reject({state: 0, error: "NotFoundError"});
     }
 
     return this._checkActivated()
       .then(_ => this._db.getByScope(aPageRecord.scope))
       .then(record => {
         // If the endpoint didn't exist, let's just fail.
         if (record === undefined) {
           throw "NotFoundError";
         }
 
-        // Let's be nice to the server and try to inform it, but we don't care
-        // about the reply.
-        this._sendRequest("unregister", record);
-        this._db.delete(record.channelID);
+        return Promise.all([
+          this._sendRequest("unregister", record),
+          this._db.delete(this._service.getKeyFromRecord(record))
+        ]);
       });
   },
 
   unregister: function(aPageRecord, aMessageManager) {
     debug("unregister() " + JSON.stringify(aPageRecord));
 
     this._unregister(aPageRecord)
       .then(_ =>
@@ -887,22 +927,17 @@ this.PushService = {
     }
 
     return this._checkActivated()
       .then(_ => this._db.getByScope(aPageRecord.scope))
       .then(pushRecord => {
         if (!pushRecord) {
           return null;
         }
-        return {
-          pushEndpoint: pushRecord.pushEndpoint,
-          version: pushRecord.version,
-          lastPush: pushRecord.lastPush,
-          pushCount: pushRecord.pushCount
-        };
+        return this._service.prepareRegistration(pushRecord);
       });
   },
 
   registration: function(aPageRecord, aMessageManager) {
     debug("registration()");
 
     return this._registration(aPageRecord)
       .then(registration =>
new file mode 100644
--- /dev/null
+++ b/dom/push/PushServiceHttp2.jsm
@@ -0,0 +1,820 @@
+/* jshint moz: true, esnext: true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+this.EXPORTED_SYMBOLS = ["PushServiceHttp2"];
+
+const prefs = new Preferences("dom.push.");
+
+// Don't modify this, instead set dom.push.debug.
+// Set debug first so that all debugging actually works.
+var gDebuggingEnabled = prefs.get("debug");
+
+function debug(s) {
+  if (gDebuggingEnabled) {
+    dump("-*- PushServiceHttp2.jsm: " + s + "\n");
+  }
+}
+
+const kPUSHHTTP2DB_DB_NAME = "pushHttp2";
+const kPUSHHTTP2DB_DB_VERSION = 1; // Change this if the IndexedDB format changes
+const kPUSHHTTP2DB_STORE_NAME = "pushHttp2";
+
+/**
+ * A proxy between the PushService and connections listening for incoming push
+ * messages. The PushService can silence messages from the connections by
+ * setting PushSubscriptionListener._pushService to null. This is required
+ * because it can happen that there is an outstanding push message that will
+ * be send on OnStopRequest but the PushService may not be interested in these.
+ * It's easier to stop listening than to have checks at specific points.
+ */
+var PushSubscriptionListener = function(pushService, uri) {
+  debug("Creating a new pushSubscription listener.");
+  this._pushService = pushService;
+  this.uri = uri;
+};
+
+PushSubscriptionListener.prototype = {
+
+  QueryInterface: function (aIID) {
+    if (aIID.equals(Ci.nsIHttpPushListener) ||
+        aIID.equals(Ci.nsIStreamListener)) {
+      return this;
+    }
+    throw Components.results.NS_ERROR_NO_INTERFACE;
+  },
+
+  getInterface: function(aIID) {
+    return this.QueryInterface(aIID);
+  },
+
+  onStartRequest: function(aRequest, aContext) {
+    debug("PushSubscriptionListener onStartRequest()");
+    // We do not do anything here.
+  },
+
+  onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
+    debug("PushSubscriptionListener onDataAvailable()");
+    // Nobody should send data, but just to be sure, otherwise necko will
+    // complain.
+    if (aCount === 0) {
+      return;
+    }
+
+    let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+                        .createInstance(Ci.nsIScriptableInputStream);
+
+    inputStream.init(aStream);
+    var data = inputStream.read(aCount);
+  },
+
+  onStopRequest: function(aRequest, aContext, aStatusCode) {
+    debug("PushSubscriptionListener onStopRequest()");
+    if (!this._pushService) {
+        return;
+    }
+
+    this._pushService.connOnStop(aRequest,
+                                 Components.isSuccessCode(aStatusCode),
+                                 this.uri);
+  },
+
+  onPush: function(associatedChannel, pushChannel) {
+    debug("PushSubscriptionListener onPush()");
+    var pushChannelListener = new PushChannelListener(this);
+    pushChannel.asyncOpen(pushChannelListener, pushChannel);
+  },
+
+  disconnect: function() {
+    this._pushService = null;
+  }
+};
+
+/**
+ * The listener for pushed messages. The message data is collected in
+ * OnDataAvailable and send to the app in OnStopRequest.
+ */
+var PushChannelListener = function(pushSubscriptionListener) {
+  debug("Creating a new push channel listener.");
+  this._mainListener = pushSubscriptionListener;
+};
+
+PushChannelListener.prototype = {
+
+  _message: null,
+  _ackUri: null,
+
+  onStartRequest: function(aRequest, aContext) {
+    this._ackUri = aRequest.URI.spec;
+  },
+
+  onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
+    debug("push channel listener onDataAvailable()");
+
+    if (aCount === 0) {
+      return;
+    }
+
+    let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+                        .createInstance(Ci.nsIScriptableInputStream);
+
+    inputStream.init(aStream);
+    if (!this._message) {
+      this._message = inputStream.read(aCount);
+    } else {
+      this._message.concat(inputStream.read(aCount));
+    }
+  },
+
+  onStopRequest: function(aRequest, aContext, aStatusCode) {
+    debug("push channel listener onStopRequest()  status code:" + aStatusCode);
+    if (Components.isSuccessCode(aStatusCode) &&
+        this._mainListener &&
+        this._mainListener._pushService) {
+      this._mainListener._pushService._pushChannelOnStop(this._mainListener.uri,
+                                                         this._ackUri,
+                                                         this._message);
+    }
+  }
+};
+
+var PushServiceDelete = function(resolve, reject) {
+  this._resolve = resolve;
+  this._reject = reject;
+};
+
+PushServiceDelete.prototype = {
+
+  onStartRequest: function(aRequest, aContext) {},
+
+  onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
+    // Nobody should send data, but just to be sure, otherwise necko will
+    // complain.
+    if (aCount === 0) {
+      return;
+    }
+
+    let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+                        .createInstance(Ci.nsIScriptableInputStream);
+
+    inputStream.init(aStream);
+    var data = inputStream.read(aCount);
+  },
+
+  onStopRequest: function(aRequest, aContext, aStatusCode) {
+
+    if (Components.isSuccessCode(aStatusCode)) {
+       this._resolve();
+    } else {
+       this._reject({status: 0, error: "NetworkError"});
+    }
+  }
+};
+
+var SubscriptionListener = function(aSubInfo, aServerURI, aPushServiceHttp2) {
+  debug("Creating a new subscription listener.");
+  this._subInfo = aSubInfo;
+  this._data = '';
+  this._serverURI = aServerURI;
+  this._service = aPushServiceHttp2;
+};
+
+SubscriptionListener.prototype = {
+
+  onStartRequest: function(aRequest, aContext) {},
+
+  onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
+    debug("subscription listener onDataAvailable()");
+
+    // We do not expect any data, but necko will complain if we do not consume
+    // it.
+    if (aCount === 0) {
+      return;
+    }
+
+    let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+                        .createInstance(Ci.nsIScriptableInputStream);
+
+    inputStream.init(aStream);
+    this._data.concat(inputStream.read(aCount));
+  },
+
+  onStopRequest: function(aRequest, aContext, aStatus) {
+    debug("subscription listener onStopRequest()");
+
+    // Check if pushService is still active.
+    if (!this._service.hasmainPushService()) {
+      this._subInfo.reject({error: "Service deactivated"});
+      return;
+    }
+
+    if (!Components.isSuccessCode(aStatus)) {
+      this._subInfo.reject({error: "Error status" + aStatus});
+      return;
+    }
+
+    var statusCode = aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+
+    if (Math.floor(statusCode / 100) == 5) {
+      if (this._subInfo.retries < prefs.get("http2.maxRetries")) {
+        this._subInfo.retries++;
+        var retryAfter = retryAfterParser(aRequest);
+        setTimeout(this._service.retrySubscription.bind(this._service,
+                                                        this._subInfo),
+                   retryAfter);
+      } else {
+        this._subInfo.reject({error: "Error response code: " + statusCode });
+      }
+      return;
+    } else if (statusCode != 201) {
+      this._subInfo.reject({error: "Error response code: " + statusCode });
+      return;
+    }
+
+    var subscriptionUri;
+    try {
+      subscriptionUri = aRequest.getResponseHeader("location");
+    } catch (err) {
+      this._subInfo.reject({error: "Return code 201, but the answer is bogus"});
+      return;
+    }
+
+    debug("subscriptionUri: " + subscriptionUri);
+
+    var linkList;
+    try {
+      linkList = aRequest.getResponseHeader("link");
+    } catch (err) {
+      this._subInfo.reject({error: "Return code 201, but the answer is bogus"});
+      return;
+    }
+
+    var linkParserResult = linkParser(linkList, this._serverURI);
+    if (linkParserResult.error) {
+      this._subInfo.reject(linkParserResult);
+      return;
+    }
+
+    if (!subscriptionUri) {
+      this._subInfo.reject({error: "Return code 201, but the answer is bogus," +
+                                   " missing subscriptionUri"});
+      return;
+    }
+    try {
+      let uriTry = Services.io.newURI(subscriptionUri, null, null);
+    } catch (e) {
+      debug("Invalid URI " + subscriptionUri);
+      this._subInfo.reject({error: "Return code 201, but URI is bogus. " +
+                                   subscriptionUri});
+      return;
+    }
+
+    var reply = {
+      subscriptionUri: subscriptionUri,
+      pushEndpoint: linkParserResult.pushEndpoint,
+      pushReceiptEndpoint: linkParserResult.pushReceiptEndpoint,
+      pageURL: this._subInfo.record.pageURL,
+      scope: this._subInfo.record.scope,
+      pushCount: 0,
+      lastPush: 0
+    };
+    this._subInfo.resolve(reply);
+  },
+};
+
+function retryAfterParser(aRequest) {
+  var retryAfter = 0;
+  try {
+    var retryField = aRequest.getResponseHeader("retry-after");
+    if (isNaN(retryField)) {
+      retryAfter = Date.parse(retryField) - (new Date().getTime());
+    } else {
+      retryAfter = parseInt(retryField, 10) * 1000;
+    }
+    retryAfter = (retryAfter > 0) ? retryAfter : 0;
+  } catch(e) {}
+
+  return retryAfter;
+}
+
+function linkParser(linkHeader, serverURI) {
+
+  var linkList = linkHeader.split(',');
+  if ((linkList.length < 1)) {
+    return {error: "Return code 201, but the answer is bogus"};
+  }
+
+  var pushEndpoint;
+  var pushReceiptEndpoint;
+
+  linkList.forEach(link => {
+    var linkElems = link.split(';');
+
+    if (linkElems.length == 2) {
+      if (linkElems[1].trim() === 'rel="urn:ietf:params:push"') {
+        pushEndpoint = linkElems[0].substring(linkElems[0].indexOf('<') + 1,
+                                              linkElems[0].indexOf('>'));
+
+      } else if (linkElems[1].trim() === 'rel="urn:ietf:params:push:receipt"') {
+        pushReceiptEndpoint = linkElems[0].substring(linkElems[0].indexOf('<') + 1,
+                                                     linkElems[0].indexOf('>'));
+      }
+    }
+  });
+
+  debug("pushEndpoint: " + pushEndpoint);
+  debug("pushReceiptEndpoint: " + pushReceiptEndpoint);
+  // Missing pushReceiptEndpoint is allowed.
+  if (!pushEndpoint) {
+    return {error: "Return code 201, but the answer is bogus, missing" +
+                   " pushEndpoint"};
+  }
+
+  var uri;
+  var resUri = [];
+  try {
+    [pushEndpoint, pushReceiptEndpoint].forEach(u => {
+      if (u) {
+        uri = u;
+        resUri[u] = Services.io.newURI(uri, null, serverURI);
+      }
+    });
+  } catch (e) {
+    debug("Invalid URI " + uri);
+    return {error: "Return code 201, but URI is bogus. " + uri};
+  }
+
+  return {
+    pushEndpoint: resUri[pushEndpoint].spec,
+    pushReceiptEndpoint: (pushReceiptEndpoint) ? resUri[pushReceiptEndpoint].spec
+                                               : ""
+  };
+}
+
+/**
+ * The implementation of the WebPush.
+ */
+this.PushServiceHttp2 = {
+  _mainPushService: null,
+  _serverURI: null,
+
+  // Keep information about all connections, e.g. the channel, listener...
+  _conns: {},
+  _started: false,
+
+  upgradeSchema: function(aTransaction,
+                          aDb,
+                          aOldVersion,
+                          aNewVersion,
+                          aDbInstance) {
+    debug("upgradeSchemaHttp2()");
+
+    let objectStore = aDb.createObjectStore(aDbInstance._dbStoreName,
+                                            { keyPath: "subscriptionUri" });
+
+    // index to fetch records based on endpoints. used by unregister
+    objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
+
+    // index to fetch records per scope, so we can identify endpoints
+    // associated with an app.
+    objectStore.createIndex("scope", "scope", { unique: true });
+  },
+
+  getKeyFromRecord: function(aRecord) {
+    return aRecord.subscriptionUri;
+  },
+
+  newPushDB: function() {
+    return new PushDB(kPUSHHTTP2DB_DB_NAME,
+                      kPUSHHTTP2DB_DB_VERSION,
+                      kPUSHHTTP2DB_STORE_NAME,
+                      this.upgradeSchema);
+  },
+
+  hasmainPushService: function() {
+    return this._mainPushService !== null;
+  },
+
+  checkServerURI: function(serverURL) {
+    if (!serverURL) {
+      debug("No dom.push.serverURL found!");
+      return;
+    }
+
+    let uri;
+    try {
+      uri = Services.io.newURI(serverURL, null, null);
+    } catch(e) {
+      debug("Error creating valid URI from dom.push.serverURL (" +
+            serverURL + ")");
+      return null;
+    }
+
+    if (uri.scheme !== "https") {
+      debug("Unsupported websocket scheme " + uri.scheme);
+      return null;
+    }
+    return uri;
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic == "nsPref:changed") {
+      if (aData == "dom.push.debug") {
+        gDebuggingEnabled = prefs.get("debug");
+      }
+    }
+  },
+
+  connect: function(subscriptions) {
+    this.startConnections(subscriptions);
+  },
+
+  disconnect: function() {
+    this._shutdownConnections(false);
+  },
+
+  _makeChannel: function(aUri) {
+
+    var ios = Cc["@mozilla.org/network/io-service;1"]
+                .getService(Ci.nsIIOService);
+
+    var chan = ios.newChannel2(aUri,
+                               null,
+                               null,
+                               null,      // aLoadingNode
+                               Services.scriptSecurityManager.getSystemPrincipal(),
+                               null,      // aTriggeringPrincipal
+                               Ci.nsILoadInfo.SEC_NORMAL,
+                               Ci.nsIContentPolicy.TYPE_OTHER)
+                 .QueryInterface(Ci.nsIHttpChannel);
+
+    var loadGroup = Cc["@mozilla.org/network/load-group;1"]
+                      .createInstance(Ci.nsILoadGroup);
+    chan.loadGroup = loadGroup;
+    return chan;
+  },
+
+  /**
+   * Subscribe new resource.
+   */
+  _subscribeResource: function(aRecord) {
+    debug("subscribeResource()");
+
+    return new Promise((resolve, reject) => {
+      this._subscribeResourceInternal({record: aRecord,
+                                       resolve,
+                                       reject,
+                                       retries: 0});
+    })
+    .then(result => {
+      this._conns[result.subscriptionUri] = {channel: null,
+                                             listener: null,
+                                             countUnableToConnect: 0,
+                                             lastStartListening: 0,
+                                             waitingForAlarm: false};
+      this._listenForMsgs(result.subscriptionUri);
+      return result;
+    });
+  },
+
+  _subscribeResourceInternal: function(aSubInfo) {
+    debug("subscribeResource()");
+
+    var listener = new SubscriptionListener(aSubInfo,
+                                            this._serverURI,
+                                            this);
+
+    var chan = this._makeChannel(this._serverURI.spec);
+    chan.requestMethod = "POST";
+    try{
+      chan.asyncOpen(listener, null);
+    } catch(e) {
+      aSubInfo.reject({status: 0, error: "NetworkError"});
+    }
+  },
+
+  retrySubscription: function(aSubInfo) {
+    this._subscribeResourceInternal(aSubInfo);
+  },
+
+  _deleteResource: function(aUri) {
+
+    return new Promise((resolve,reject) => {
+      var chan = this._makeChannel(aUri);
+      chan.requestMethod = "DELETE";
+      try {
+        chan.asyncOpen(new PushServiceDelete(resolve, reject), null);
+      } catch(err) {
+        reject({status: 0, error: "NetworkError"});
+      }
+    });
+  },
+
+  /**
+   * Unsubscribe the resource with a subscription uri aSubscriptionUri.
+   * We can't do anything about it if it fails, so we don't listen for response.
+   */
+  _unsubscribeResource: function(aSubscriptionUri) {
+    debug("unsubscribeResource()");
+
+    return this._deleteResource(aSubscriptionUri);
+  },
+
+  /**
+   * Start listening for messages.
+   */
+  _listenForMsgs: function(aSubscriptionUri) {
+    debug("listenForMsgs() " + aSubscriptionUri);
+    if (!this._conns[aSubscriptionUri]) {
+      debug("We do not have this subscription " + aSubscriptionUri);
+      return;
+    }
+
+    var chan = this._makeChannel(aSubscriptionUri);
+    var conn = {};
+    conn.channel = chan;
+    var listener = new PushSubscriptionListener(this, aSubscriptionUri);
+    conn.listener = listener;
+
+    chan.notificationCallbacks = listener;
+
+    try {
+      chan.asyncOpen(listener, chan);
+    } catch (e) {
+      debug("Error connecting to push server. asyncOpen failed!");
+      conn.listener.disconnect();
+      chan.cancel(Cr.NS_ERROR_ABORT);
+      this._retryAfterBackoff(aSubscriptionUri, -1);
+      return;
+    }
+
+    this._conns[aSubscriptionUri].lastStartListening = Date.now();
+    this._conns[aSubscriptionUri].channel = conn.channel;
+    this._conns[aSubscriptionUri].listener = conn.listener;
+
+  },
+
+  _ackMsgRecv: function(aAckUri) {
+    debug("ackMsgRecv() " + aAckUri);
+    // We can't do anything about it if it fails,
+    // so we don't listen for response.
+    this._deleteResource(aAckUri);
+  },
+
+  init: function(aOptions, aMainPushService, aServerURL) {
+    debug("init()");
+    this._mainPushService = aMainPushService;
+    this._serverURI = aServerURL;
+    gDebuggingEnabled = prefs.get("debug");
+    prefs.observe("debug", this);
+  },
+
+  _retryAfterBackoff: function(aSubscriptionUri, retryAfter) {
+    debug("retryAfterBackoff()");
+
+    var resetRetryCount = prefs.get("http2.reset_retry_count_after_ms");
+    // If it was running for some time, reset retry counter.
+    if ((Date.now() - this._conns[aSubscriptionUri].lastStartListening) >
+        resetRetryCount) {
+      this._conns[aSubscriptionUri].countUnableToConnect = 0;
+    }
+
+    let maxRetries = prefs.get("http2.maxRetries");
+    if (this._conns[aSubscriptionUri].countUnableToConnect >= maxRetries) {
+      this._shutdownSubscription(aSubscriptionUri);
+      this._resubscribe(aSubscriptionUri);
+      return;
+    }
+
+    if (retryAfter !== -1) {
+      // This is a 5xx response.
+      // To respect RetryAfter header, setTimeout is used. setAlarm sets a
+      // cumulative alarm so it will not always respect RetryAfter header.
+      this._conns[aSubscriptionUri].countUnableToConnect++;
+      setTimeout(_ => this._listenForMsgs(aSubscriptionUri), retryAfter);
+      return;
+    }
+
+    // we set just one alarm because most probably all connection will go over
+    // a single TCP connection.
+    retryAfter = prefs.get("http2.retryInterval") *
+      Math.pow(2, this._conns[aSubscriptionUri].countUnableToConnect);
+
+    retryAfter = retryAfter * (0.8 + Math.random() * 0.4); // add +/-20%.
+
+    this._conns[aSubscriptionUri].countUnableToConnect++;
+
+    if (retryAfter === 0) {
+      setTimeout(_ => this._listenForMsgs(aSubscriptionUri), 0);
+    } else {
+      this._conns[aSubscriptionUri].waitingForAlarm = true;
+      this._mainPushService.setAlarm(retryAfter);
+    }
+      debug("Retry in " + retryAfter);
+  },
+
+  // Close connections.
+  _shutdownConnections: function(deleteInfo) {
+    debug("shutdownConnections()");
+
+    for (let subscriptionUri in this._conns) {
+      if (this._conns[subscriptionUri]) {
+        if (this._conns[subscriptionUri].listener) {
+          this._conns[subscriptionUri].listener._pushService = null;
+        }
+
+        if (this._conns[subscriptionUri].channel) {
+          try {
+            this._conns[subscriptionUri].channel.cancel(Cr.NS_ERROR_ABORT);
+          } catch (e) {}
+        }
+        this._conns[subscriptionUri].listener = null;
+        this._conns[subscriptionUri].channel = null;
+        this._conns[subscriptionUri].waitingForAlarm = false;
+        if (deleteInfo) {
+          delete this._conns[subscriptionUri];
+        }
+      }
+    }
+  },
+
+  // Start listening if subscriptions present.
+  startConnections: function(aSubscriptions) {
+    debug("startConnections() " + aSubscriptions.length);
+
+    for (let i = 0; i < aSubscriptions.length; i++) {
+      let record = aSubscriptions[i];
+      if (typeof this._conns[record.subscriptionUri] != "object") {
+        this._conns[record.subscriptionUri] = {channel: null,
+                                               listener: null,
+                                               countUnableToConnect: 0,
+                                               waitingForAlarm: false};
+      }
+      if (!this._conns[record.subscriptionUri].conn) {
+        this._conns[record.subscriptionUri].waitingForAlarm = false;
+        this._listenForMsgs(record.subscriptionUri);
+      }
+    }
+  },
+
+  // Start listening if subscriptions present.
+  _startConnectionsWaitingForAlarm: function() {
+    debug("startConnectionsWaitingForAlarm()");
+    for (let subscriptionUri in this._conns) {
+      if ((this._conns[subscriptionUri]) &&
+          !this._conns[subscriptionUri].conn &&
+          this._conns[subscriptionUri].waitingForAlarm) {
+        this._conns[subscriptionUri].waitingForAlarm = false;
+        this._listenForMsgs(subscriptionUri);
+      }
+    }
+  },
+
+  // Close connection and notify apps that subscription are gone.
+  _shutdownSubscription: function(aSubscriptionUri) {
+    debug("shutdownSubscriptions()");
+
+    if (typeof this._conns[aSubscriptionUri] == "object") {
+      if (this._conns[aSubscriptionUri].listener) {
+        this._conns[aSubscriptionUri].listener._pushService = null;
+      }
+
+      if (this._conns[aSubscriptionUri].channel) {
+        try {
+          this._conns[aSubscriptionUri].channel.cancel(Cr.NS_ERROR_ABORT);
+        } catch (e) {}
+      }
+      delete this._conns[aSubscriptionUri];
+    }
+  },
+
+  uninit: function() {
+    debug("uninit()");
+    this._shutdownConnections(true);
+    this._mainPushService = null;
+  },
+
+
+  request: function(action, aRecord) {
+    switch (action) {
+      case "register":
+        debug("register");
+        return this._subscribeResource(aRecord);
+     case "unregister":
+        this._shutdownSubscription(aRecord.subscriptionUri);
+        return this._unsubscribeResource(aRecord.subscriptionUri);
+    }
+  },
+
+  /** Push server has deleted subscription.
+   *  Re-subscribe - if it succeeds send update db record and send
+   *                 pushsubscriptionchange,
+   *               - on error delete record and send pushsubscriptionchange
+   *  TODO: maybe pushsubscriptionerror will be included.
+   */
+  _resubscribe: function(aSubscriptionUri) {
+    this._mainPushService.getByKeyID(aSubscriptionUri)
+      .then(record => this._subscribeResource(record)
+        .then(recordNew => {
+          if (this._mainPushService) {
+            this._mainPushService.updateRegistrationAndNotifyApp(aSubscriptionUri,
+                                                                 recordNew);
+          }
+        }, error => {
+          if (this._mainPushService) {
+            this._mainPushService.dropRegistrationAndNotifyApp(aSubscriptionUri);
+          }
+        })
+      );
+  },
+
+  connOnStop: function(aRequest, aSuccess,
+                       aSubscriptionUri) {
+    debug("connOnStop() succeeded: " + aSuccess);
+
+    var conn = this._conns[aSubscriptionUri];
+    if (!conn) {
+      // there is no connection description that means that we closed
+      // connection, so do nothing. But we should have already deleted
+      // the listener.
+      return;
+    }
+
+    conn.channel = null;
+    conn.listener = null;
+
+    if (!aSuccess) {
+      this._retryAfterBackoff(aSubscriptionUri, -1);
+
+    } else if (Math.floor(aRequest.responseStatus / 100) == 5) {
+      var retryAfter = retryAfterParser(aRequest);
+      this._retryAfterBackoff(aSubscriptionUri, retryAfter);
+
+    } else if (Math.floor(aRequest.responseStatus / 100) == 4) {
+      this._shutdownSubscription(aSubscriptionUri);
+      this._resubscribe(aSubscriptionUri);
+    } else if (Math.floor(aRequest.responseStatus / 100) == 2) { // This should be 204
+      setTimeout(_ => this._listenForMsgs(aSubscriptionUri), 0);
+    } else {
+      this._retryAfterBackoff(aSubscriptionUri, -1);
+    }
+  },
+
+  _pushChannelOnStop: function(aUri, aAckUri, aMessage) {
+    debug("pushChannelOnStop() ");
+
+    let sendNotification = function(aAckUri, aPushRecord, self) {
+      aPushRecord.pushCount = aPushRecord.pushCount + 1;
+      aPushRecord.lastPush = new Date().getTime();
+      self._mainPushService.receivedPushMessage(aPushRecord, aMessage);
+      self._ackMsgRecv(aAckUri);
+    };
+
+    let recoverNoSuchEndpoint = function() {
+      debug("Could not get push endpoint " + aUri + " from DB");
+    };
+
+    this._mainPushService.getByKeyID(aUri)
+      .then(pushRecord => sendNotification(aAckUri, pushRecord, this),
+            recoverNoSuchEndpoint);
+  },
+
+  onAlarmFired: function() {
+    // Conditions are arranged in decreasing specificity.
+    // i.e. when _waitingForPong is true, other conditions are also true.
+    this._startConnectionsWaitingForAlarm();
+  },
+
+  prepareRegistration: function(aPushRecord) {
+    return {
+      pushEndpoint: aPushRecord.pushEndpoint,
+      pushReceiptEndpoint: aPushRecord.pushReceiptEndpoint,
+      version: aPushRecord.version,
+      lastPush: aPushRecord.lastPush,
+      pushCount: aPushRecord.pushCount
+    };
+  },
+
+  prepareRegister: function(aPushRecord) {
+    return {
+      pushEndpoint: aPushRecord.pushEndpoint,
+      pushReceiptEndpoint: aPushRecord.pushReceiptEndpoint
+    };
+  }
+};
--- a/dom/push/PushServiceWebSocket.jsm
+++ b/dom/push/PushServiceWebSocket.jsm
@@ -138,16 +138,20 @@ this.PushServiceWebSocket = {
     // index to fetch records based on endpoints. used by unregister
     objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
 
     // index to fetch records per scope, so we can identify endpoints
     // associated with an app.
     objectStore.createIndex("scope", "scope", { unique: true });
   },
 
+  getKeyFromRecord: function(aRecord) {
+    return aRecord.channelID;
+  },
+
   newPushDB: function() {
     return new PushDB(kPUSHWSDB_DB_NAME,
                       kPUSHWSDB_DB_VERSION,
                       kPUSHWSDB_STORE_NAME,
                       this.upgradeSchema);
   },
 
   disconnect: function() {
@@ -947,24 +951,29 @@ this.PushServiceWebSocket = {
     }
 
     if (action == "register") {
       record.channelID = this._generateID();
     }
     var data = {channelID: record.channelID,
                 messageType: action};
 
-    return new Promise((resolve, reject) => {
+    var p = new Promise((resolve, reject) => {
       this._pendingRequests[data.channelID] = {record: record,
                                                resolve: resolve,
                                                reject: reject,
                                                ctime: Date.now()
                                               };
       this._queueRequest(data);
     });
+    if (action == "unregister") {
+      return Promise.resolve();
+    } else {
+      return p;
+    }
   },
 
   _queueStart: Promise.resolve(),
   _notifyRequestQueue: null,
   _queue: null,
   _enqueue: function(op, errop) {
     debug("enqueue");
     if (!this._queue) {
@@ -1038,17 +1047,17 @@ this.PushServiceWebSocket = {
         debug("No significant version change: " + aLatestVersion);
       }
     };
 
     let recoverNoSuchChannelID = function(aChannelIDFromServer) {
       debug("Could not get channelID " + aChannelIDFromServer + " from DB");
     };
 
-    this._mainPushService.getByChannelID(aChannelID)
+    this._mainPushService.getByKeyID(aChannelID)
       .then(compareRecordVersionAndNotify.bind(this),
             err => recoverNoSuchChannelID(err));
   },
 
   // begin Push protocol handshake
   _wsOnStart: function(context) {
     debug("wsOnStart()");
     this._releaseWakeLock();
@@ -1091,17 +1100,17 @@ this.PushServiceWebSocket = {
 
         data.mobilenetwork = {
           mcc: networkState.mcc,
           mnc: networkState.mnc,
           netid: networkState.netid
         };
       }
 
-      this._mainPushService.getAllChannelIDs()
+      this._mainPushService.getAllKeyIDs()
         .then(sendHelloMessage.bind(this),
               sendHelloMessage.bind(this));
     });
   },
 
   /**
    * This statusCode is not the websocket protocol status code, but the TCP
    * connection close status code.
@@ -1268,16 +1277,31 @@ this.PushServiceWebSocket = {
    *
    * If this happens, we reconnect the WebSocket to not miss out on
    * notifications.
    */
   onStopListening: function(aServ, aStatus) {
     debug("UDP Server socket was shutdown. Status: " + aStatus);
     this._udpServer = undefined;
     this._beginWSSetup();
+  },
+
+  prepareRegistration: function(aPushRecord) {
+    return {
+      pushEndpoint: aPushRecord.pushEndpoint,
+      version: aPushRecord.version,
+      lastPush: aPushRecord.lastPush,
+      pushCount: aPushRecord.pushCount
+    };
+  },
+
+  prepareRegister: function(aPushRecord) {
+    return {
+      pushEndpoint: aPushRecord.pushEndpoint
+    };
   }
 };
 
 let PushNetworkInfo = {
   /**
    * Returns information about MCC-MNC and the IP of the current connection.
    */
   getNetworkInformation: function() {
--- a/dom/push/moz.build
+++ b/dom/push/moz.build
@@ -7,16 +7,17 @@ EXTRA_COMPONENTS += [
     'Push.js',
     'Push.manifest',
     'PushNotificationService.js',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'PushDB.jsm',
     'PushService.jsm',
+    'PushServiceHttp2.jsm',
     'PushServiceWebSocket.jsm',
 ]
 
 MOCHITEST_MANIFESTS += [
     'test/mochitest.ini',
 ]
 
 XPCSHELL_TESTS_MANIFESTS += [
--- a/dom/push/test/xpcshell/test_clearAll_successful.js
+++ b/dom/push/test/xpcshell/test_clearAll_successful.js
@@ -37,11 +37,11 @@ add_task(function* test_unregister_succe
             uaid: 'fbe865a6-aeb8-446f-873c-aeebdb8d493c'
           }));
         }
       });
     }
   });
 
   yield PushNotificationService.clearAll();
-  let record = yield db.getByChannelID(channelID);
+  let record = yield db.getByKeyID(channelID);
   ok(!record, 'Unregister did not remove record');
 });
--- a/dom/push/test/xpcshell/test_notification_duplicate.js
+++ b/dom/push/test/xpcshell/test_notification_duplicate.js
@@ -67,16 +67,16 @@ add_task(function* test_notification_dup
     }
   });
 
   yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
     'Timed out waiting for notifications');
   yield waitForPromise(ackDefer.promise, DEFAULT_TIMEOUT,
     'Timed out waiting for stale acknowledgement');
 
-  let staleRecord = yield db.getByChannelID(
+  let staleRecord = yield db.getByKeyID(
     '8d2d9400-3597-4c5a-8a38-c546b0043bcc');
   strictEqual(staleRecord.version, 2, 'Wrong stale record version');
 
-  let updatedRecord = yield db.getByChannelID(
+  let updatedRecord = yield db.getByKeyID(
     '27d1e393-03ef-4c72-a5e6-9e890dfccad0');
   strictEqual(updatedRecord.version, 3, 'Wrong updated record version');
 });
--- a/dom/push/test/xpcshell/test_notification_error.js
+++ b/dom/push/test/xpcshell/test_notification_error.js
@@ -51,17 +51,17 @@ add_task(function* test_notification_err
   ]);
 
   let ackDefer = Promise.defer();
   let ackDone = after(records.length, ackDefer.resolve);
   PushService.init({
     serverURI: "wss://push.example.org/",
     networkInfo: new MockDesktopNetworkInfo(),
     db: makeStub(db, {
-      getByChannelID(prev, channelID) {
+      getByKeyID(prev, channelID) {
         if (channelID == '3c3930ba-44de-40dc-a7ca-8a133ec1a866') {
           return Promise.reject('splines not reticulated');
         }
         return prev.call(this, channelID);
       }
     }),
     makeWebSocket(uri) {
       return new MockWebSocket(uri, {
--- a/dom/push/test/xpcshell/test_notification_incomplete.js
+++ b/dom/push/test/xpcshell/test_notification_incomplete.js
@@ -97,17 +97,17 @@ add_task(function* test_notification_inc
         }
       });
     }
   });
 
   yield waitForPromise(notificationDefer.promise, DEFAULT_TIMEOUT,
     'Timed out waiting for incomplete notifications');
 
-  let storeRecords = yield db.getAllChannelIDs();
+  let storeRecords = yield db.getAllKeyIDs();
   storeRecords.sort(({pushEndpoint: a}, {pushEndpoint: b}) =>
     compareAscending(a, b));
   recordsAreEqual(records, storeRecords);
 });
 
 function recordIsEqual(a, b) {
   strictEqual(a.channelID, b.channelID, 'Wrong channel ID in record');
   strictEqual(a.pushEndpoint, b.pushEndpoint, 'Wrong push endpoint in record');
--- a/dom/push/test/xpcshell/test_notification_version_string.js
+++ b/dom/push/test/xpcshell/test_notification_version_string.js
@@ -61,12 +61,12 @@ add_task(function* test_notification_ver
   equal(scope, 'https://example.com/page/1', 'Wrong scope');
   equal(message.pushEndpoint, 'https://example.org/updates/1',
     'Wrong push endpoint');
   strictEqual(message.version, 4, 'Wrong version');
 
   yield waitForPromise(ackDefer.promise, DEFAULT_TIMEOUT,
     'Timed out waiting for string acknowledgement');
 
-  let storeRecord = yield db.getByChannelID(
+  let storeRecord = yield db.getByKeyID(
     '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b');
   strictEqual(storeRecord.version, 4, 'Wrong record version');
 });
--- a/dom/push/test/xpcshell/test_register_case.js
+++ b/dom/push/test/xpcshell/test_register_case.js
@@ -51,14 +51,14 @@ add_task(function* test_register_case() 
     DEFAULT_TIMEOUT,
     'Mixed-case register response timed out'
   );
   equal(newRecord.pushEndpoint, 'https://example.com/update/case',
     'Wrong push endpoint in registration record');
   equal(newRecord.scope, 'https://example.net/case',
     'Wrong scope in registration record');
 
-  let record = yield db.getByChannelID(newRecord.channelID);
+  let record = yield db.getByKeyID(newRecord.channelID);
   equal(record.pushEndpoint, 'https://example.com/update/case',
     'Wrong push endpoint in database record');
   equal(record.scope, 'https://example.net/case',
     'Wrong scope in database record');
 });
--- a/dom/push/test/xpcshell/test_register_flush.js
+++ b/dom/push/test/xpcshell/test_register_flush.js
@@ -84,20 +84,20 @@ add_task(function* test_register_flush()
 
   let {data: scope} = yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
     'Timed out waiting for notification');
   equal(scope, 'https://example.com/page/1', 'Wrong notification scope');
 
   yield waitForPromise(ackDefer.promise, DEFAULT_TIMEOUT,
      'Timed out waiting for acknowledgements');
 
-  let prevRecord = yield db.getByChannelID(
+  let prevRecord = yield db.getByKeyID(
     '9bcc7efb-86c7-4457-93ea-e24e6eb59b74');
   equal(prevRecord.pushEndpoint, 'https://example.org/update/1',
     'Wrong existing push endpoint');
   strictEqual(prevRecord.version, 3,
     'Should record version updates sent before register responses');
 
-  let registeredRecord = yield db.getByChannelID(newRecord.channelID);
+  let registeredRecord = yield db.getByKeyID(newRecord.channelID);
   equal(registeredRecord.pushEndpoint, 'https://example.org/update/2',
     'Wrong new push endpoint');
   ok(!registeredRecord.version, 'Should not record premature updates');
 });
--- a/dom/push/test/xpcshell/test_register_invalid_channel.js
+++ b/dom/push/test/xpcshell/test_register_invalid_channel.js
@@ -50,11 +50,11 @@ add_task(function* test_register_invalid
   yield rejects(
     PushNotificationService.register('https://example.com/invalid-channel'),
     function(error) {
       return error == 'Invalid channel ID';
     },
     'Wrong error for invalid channel ID'
   );
 
-  let record = yield db.getByChannelID(channelID);
+  let record = yield db.getByKeyID(channelID);
   ok(!record, 'Should not store records for error responses');
 });
--- a/dom/push/test/xpcshell/test_register_invalid_endpoint.js
+++ b/dom/push/test/xpcshell/test_register_invalid_endpoint.js
@@ -52,11 +52,11 @@ add_task(function* test_register_invalid
     PushNotificationService.register(
       'https://example.net/page/invalid-endpoint'),
     function(error) {
       return error && error.includes('Invalid pushEndpoint');
     },
     'Wrong error for invalid endpoint'
   );
 
-  let record = yield db.getByChannelID(channelID);
+  let record = yield db.getByKeyID(channelID);
   ok(!record, 'Should not store records with invalid endpoints');
 });
--- a/dom/push/test/xpcshell/test_register_success.js
+++ b/dom/push/test/xpcshell/test_register_success.js
@@ -61,16 +61,16 @@ add_task(function* test_register_success
   );
   equal(newRecord.channelID, channelID,
     'Wrong channel ID in registration record');
   equal(newRecord.pushEndpoint, 'https://example.com/update/1',
     'Wrong push endpoint in registration record');
   equal(newRecord.scope, 'https://example.org/1',
     'Wrong scope in registration record');
 
-  let record = yield db.getByChannelID(channelID);
+  let record = yield db.getByKeyID(channelID);
   equal(record.channelID, channelID,
     'Wrong channel ID in database record');
   equal(record.pushEndpoint, 'https://example.com/update/1',
     'Wrong push endpoint in database record');
   equal(record.scope, 'https://example.org/1',
     'Wrong scope in database record');
 });
--- a/dom/push/test/xpcshell/test_register_timeout.js
+++ b/dom/push/test/xpcshell/test_register_timeout.js
@@ -85,17 +85,17 @@ add_task(function* test_register_timeout
   yield rejects(
     PushNotificationService.register('https://example.net/page/timeout'),
     function(error) {
       return error == 'TimeoutError';
     },
     'Wrong error for request timeout'
   );
 
-  let record = yield db.getByChannelID(channelID);
+  let record = yield db.getByKeyID(channelID);
   ok(!record, 'Should not store records for timed-out responses');
 
   yield waitForPromise(
     timeoutDefer.promise,
     DEFAULT_TIMEOUT,
     'Reconnect timed out'
   );
   equal(registers, 1, 'Should not handle timed-out register requests');
--- a/dom/push/test/xpcshell/test_unregister_error.js
+++ b/dom/push/test/xpcshell/test_unregister_error.js
@@ -51,15 +51,15 @@ add_task(function* test_unregister_error
         }
       });
     }
   });
 
   yield PushNotificationService.unregister(
     'https://example.net/page/failure');
 
-  let result = yield db.getByChannelID(channelID);
+  let result = yield db.getByKeyID(channelID);
   ok(!result, 'Deleted push record exists');
 
   // Make sure we send a request to the server.
   yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
     'Timed out waiting for unregister');
 });
--- a/dom/push/test/xpcshell/test_unregister_invalid_json.js
+++ b/dom/push/test/xpcshell/test_unregister_invalid_json.js
@@ -57,22 +57,22 @@ add_task(function* test_unregister_inval
       });
     }
   });
 
   // "unregister" is fire-and-forget: it's sent via _send(), not
   // _sendRequest().
   yield PushNotificationService.unregister(
     'https://example.edu/page/1');
-  let record = yield db.getByChannelID(
+  let record = yield db.getByKeyID(
     '87902e90-c57e-4d18-8354-013f4a556559');
   ok(!record, 'Failed to delete unregistered record');
 
   yield PushNotificationService.unregister(
     'https://example.net/page/1');
-  record = yield db.getByChannelID(
+  record = yield db.getByKeyID(
     '057caa8f-9b99-47ff-891c-adad18ce603e');
   ok(!record,
     'Failed to delete unregistered record after receiving invalid JSON');
 
   yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
     'Timed out waiting for unregister');
 });
--- a/dom/push/test/xpcshell/test_unregister_success.js
+++ b/dom/push/test/xpcshell/test_unregister_success.js
@@ -47,14 +47,14 @@ add_task(function* test_unregister_succe
           unregisterDefer.resolve();
         }
       });
     }
   });
 
   yield PushNotificationService.unregister(
     'https://example.com/page/unregister-success');
-  let record = yield db.getByChannelID(channelID);
+  let record = yield db.getByKeyID(channelID);
   ok(!record, 'Unregister did not remove record');
 
   yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
     'Timed out waiting for unregister');
 });
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4412,16 +4412,21 @@ pref("dom.push.adaptive.lastGoodPingInte
 // Valid gap between the biggest good ping and the bad ping
 pref("dom.push.adaptive.gap", 60000); // 1 minute
 // We limit the ping to this maximum value
 pref("dom.push.adaptive.upperLimit", 1740000); // 29 min
 
 // enable udp wakeup support
 pref("dom.push.udp.wakeupEnabled", false);
 
+// WebPush prefs:
+pref("dom.push.http2.reset_retry_count_after_ms", 60000);
+pref("dom.push.http2.maxRetries", 2);
+pref("dom.push.http2.retryInterval", 5000);
+
 // WebNetworkStats
 pref("dom.mozNetworkStats.enabled", false);
 
 // WebSettings
 pref("dom.mozSettings.enabled", false);
 pref("dom.mozPermissionSettings.enabled", false);
 
 // W3C touch events