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 269683 db9050215b8a3ee9c044b9ffce055a399b7d7b34
parent 269682 036d9abeb00c1de853fc4b04fc43c9c793038cb3
child 269684 4c897e262cd53d5cc043927ae1c75be20f395205
push id2540
push userwcosta@mozilla.com
push dateWed, 03 Jun 2015 20:55:41 +0000
reviewersnsm, mt
bugs1150812
milestone41.0a1
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