Bug 887889 - Migrate ContentPrefService2 to Sqlite.jsm r?mak draft
authorDoug Thayer <dothayer@mozilla.com>
Mon, 12 Mar 2018 14:41:42 -0700
changeset 788702 5accd7d51883a69e2ac9c50cda87796d009b9e17
parent 788701 93051649414dca637dc95a04bf304452e453090f
child 788703 0ecc1626fd6593f6d4d6945434ce3862e1d19527
push id108067
push userbmo:dothayer@mozilla.com
push dateThu, 26 Apr 2018 21:12:12 +0000
reviewersmak
bugs887889
milestone61.0a1
Bug 887889 - Migrate ContentPrefService2 to Sqlite.jsm r?mak - I kept the xpcom-shutdown observer around even though it's not doing much and it could be satisfied by doing a little more work in the Sqlite.shutdown blocker. I wasn't sure which to use since it seems like the Sqlite.shutdown blocker is intended to be used to cleanup connection-related things. Thoughts on this are welcome. MozReview-Commit-ID: CqcGHBFaJsZ
toolkit/components/contentprefs/ContentPrefService2.js
--- a/toolkit/components/contentprefs/ContentPrefService2.js
+++ b/toolkit/components/contentprefs/ContentPrefService2.js
@@ -1,42 +1,40 @@
 /* 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/. */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/ContentPrefUtils.jsm");
 ChromeUtils.import("resource://gre/modules/ContentPrefStore.jsm");
+ChromeUtils.defineModuleGetter(this, "OS",
+                               "resource://gre/modules/osfile.jsm");
+ChromeUtils.defineModuleGetter(this, "Sqlite",
+                               "resource://gre/modules/Sqlite.jsm");
 
 const CACHE_MAX_GROUP_ENTRIES = 100;
 
 const GROUP_CLAUSE = `
   SELECT id
   FROM groups
   WHERE name = :group OR
         (:includeSubdomains AND name LIKE :pattern ESCAPE '/')
 `;
 
 function ContentPrefService2() {
   if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
     return ChromeUtils.import("resource://gre/modules/ContentPrefServiceChild.jsm")
              .ContentPrefServiceChild;
   }
 
-  // If this throws an exception, it causes the getService call to fail,
-  // but the next time a consumer tries to retrieve the service, we'll try
-  // to initialize the database again, which might work if the failure
-  // was due to a temporary condition (like being out of disk space).
-  this._dbInit();
-
   Services.obs.addObserver(this, "last-pb-context-exited");
 
   // Observe shutdown so we can shut down the database connection.
-  Services.obs.addObserver(this, "xpcom-shutdown");
+  Services.obs.addObserver(this, "profile-before-change");
 }
 
 const cache = new ContentPrefStore();
 cache.set = function CPS_cache_set(group, name, val) {
   Object.getPrototypeOf(this).set.apply(this, arguments);
   let groupCount = this._groups.size;
   if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
     // Clean half of the entries
@@ -53,41 +51,54 @@ const privModeStorage = new ContentPrefS
 
 ContentPrefService2.prototype = {
   // XPCOM Plumbing
 
   classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
 
   // Destruction
 
-  _destroy: function ContentPrefService__destroy() {
-    Services.obs.removeObserver(this, "xpcom-shutdown");
+  _destroy: function CPS2__destroy() {
+    Services.obs.removeObserver(this, "profile-before-change");
     Services.obs.removeObserver(this, "last-pb-context-exited");
 
-    this.destroy();
-
-    this._dbConnection.asyncClose(() => {
-      Services.obs.notifyObservers(null, "content-prefs-db-closed");
-    });
-
     // Delete references to XPCOM components to make sure we don't leak them
     // (although we haven't observed leakage in tests).  Also delete references
     // in _observers and _genericObservers to avoid cycles with those that
     // refer to us and don't remove themselves from those observer pools.
     delete this._observers;
     delete this._genericObservers;
     delete this.__grouper;
   },
 
 
   // in-memory cache and private-browsing stores
 
   _cache: cache,
   _pbStore: privModeStorage,
 
+  _connPromise: null,
+
+  get conn() {
+    if (this._connPromise) {
+      return this._connPromise;
+    }
+
+    return this._connPromise = new Promise(async (resolve, reject) => {
+      let conn;
+      try {
+        conn = await this._getConnection();
+      } catch (e) {
+        this.log("Failed to establish database connection: " + e);
+        reject(e);
+      }
+      resolve(conn);
+    });
+  },
+
   // nsIContentPrefService
 
   getByName: function CPS2_getByName(name, context, callback) {
     checkNameArg(name);
     checkCallbackArg(callback, true);
 
     // Some prefs may be in both the database and the private browsing store.
     // Notify the caller of such prefs only once, using the values from private
@@ -114,32 +125,32 @@ ContentPrefService2.prototype = {
       SELECT NULL AS grp, prefs.value AS value
       FROM prefs
       JOIN settings ON settings.id = prefs.settingID
       WHERE settings.name = :name AND prefs.groupID ISNULL
     `);
     stmt2.params.name = name;
 
     this._execStmts([stmt1, stmt2], {
-      onRow: function onRow(row) {
+      onRow: row => {
         let grp = row.getResultByName("grp");
         let val = row.getResultByName("value");
         this._cache.set(grp, name, val);
         if (!pbPrefs.has(grp, name))
           cbHandleResult(callback, new ContentPref(grp, name, val));
       },
-      onDone: function onDone(reason, ok, gotRow) {
+      onDone: (reason, ok, gotRow) => {
         if (ok) {
           for (let [pbGroup, pbName, pbVal] of pbPrefs) {
             cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
           }
         }
         cbHandleCompletion(callback, reason);
       },
-      onError: function onError(nsresult) {
+      onError: nsresult => {
         cbHandleError(callback, nsresult);
       }
     });
   },
 
   getByDomainAndName: function CPS2_getByDomainAndName(group, name, context,
                                                        callback) {
     checkGroupArg(group);
@@ -169,40 +180,42 @@ ContentPrefService2.prototype = {
     if (context && context.usePrivateBrowsing) {
       for (let [sgroup, val] of
              this._pbStore.match(group, name, includeSubdomains)) {
         pbPrefs.set(sgroup, name, val);
       }
     }
 
     this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], {
-      onRow: function onRow(row) {
+      onRow: row => {
         let grp = row.getResultByName("grp");
         let val = row.getResultByName("value");
         this._cache.set(grp, name, val);
         if (!pbPrefs.has(group, name))
           cbHandleResult(callback, new ContentPref(grp, name, val));
       },
-      onDone: function onDone(reason, ok, gotRow) {
+      onDone: (reason, ok, gotRow) => {
         if (ok) {
           if (!gotRow)
             this._cache.set(group, name, undefined);
           for (let [pbGroup, pbName, pbVal] of pbPrefs) {
             cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
           }
         }
         cbHandleCompletion(callback, reason);
       },
-      onError: function onError(nsresult) {
+      onError: nsresult => {
         cbHandleError(callback, nsresult);
       }
     });
   },
 
-  _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) {
+  _commonGetStmt: function CPS2__commonGetStmt(group,
+                                               name,
+                                               includeSubdomains) {
     let stmt = group ?
       this._stmtWithGroupClause(group, includeSubdomains, `
         SELECT groups.name AS grp, prefs.value AS value
         FROM prefs
         JOIN settings ON settings.id = prefs.settingID
         JOIN groups ON groups.id = prefs.groupID
         WHERE settings.name = :name AND prefs.groupID IN (${GROUP_CLAUSE})
       `) :
@@ -214,20 +227,21 @@ ContentPrefService2.prototype = {
       `);
     stmt.params.name = name;
     return stmt;
   },
 
   _stmtWithGroupClause: function CPS2__stmtWithGroupClause(group,
                                                            includeSubdomains,
                                                            sql) {
-    let stmt = this._stmt(sql);
+    let stmt = this._stmt(sql, false);
     stmt.params.group = group;
     stmt.params.includeSubdomains = includeSubdomains || false;
-    stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/");
+    stmt.params.pattern = "%." + (group == null ? null :
+      group.replace(/\/|%|_/g, "/$&"));
     return stmt;
   },
 
   getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(group,
                                                                    name,
                                                                    context) {
     checkGroupArg(group);
     let prefs = this._getCached(group, name, false, context);
@@ -354,24 +368,24 @@ ContentPrefService2.prototype = {
       `);
     }
     stmt.params.name = name;
     stmt.params.value = value;
     stmt.params.now = Date.now() / 1000;
     stmts.push(stmt);
 
     this._execStmts(stmts, {
-      onDone: function onDone(reason, ok) {
+      onDone: (reason, ok) => {
         if (ok)
           this._cache.setWithCast(group, name, value);
         cbHandleCompletion(callback, reason);
         if (ok)
           this._notifyPrefSet(group, name, value, context && context.usePrivateBrowsing);
       },
-      onError: function onError(nsresult) {
+      onError: nsresult => {
         cbHandleError(callback, nsresult);
       }
     });
   },
 
   removeByDomainAndName: function CPS2_removeByDomainAndName(group, name,
                                                              context,
                                                              callback) {
@@ -420,22 +434,22 @@ ContentPrefService2.prototype = {
     stmts.push(stmt);
 
     stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
 
     let prefs = new ContentPrefStore();
 
     let isPrivate = context && context.usePrivateBrowsing;
     this._execStmts(stmts, {
-      onRow: function onRow(row) {
+      onRow: row => {
         let grp = row.getResultByName("grp");
         prefs.set(grp, name, undefined);
         this._cache.set(grp, name, undefined);
       },
-      onDone: function onDone(reason, ok) {
+      onDone: (reason, ok) => {
         if (ok) {
           this._cache.set(group, name, undefined);
           if (isPrivate) {
             for (let [sgroup, ] of
                    this._pbStore.match(group, name, includeSubdomains)) {
               prefs.set(sgroup, name, undefined);
               this._pbStore.remove(sgroup, name);
             }
@@ -443,17 +457,17 @@ ContentPrefService2.prototype = {
         }
         cbHandleCompletion(callback, reason);
         if (ok) {
           for (let [sgroup, , ] of prefs) {
             this._notifyPrefRemoved(sgroup, name, isPrivate);
           }
         }
       },
-      onError: function onError(nsresult) {
+      onError: nsresult => {
         cbHandleError(callback, nsresult);
       }
     });
   },
 
   // Deletes settings and groups that are no longer used.
   _settingsAndGroupsCleanupStmts() {
     // The NOTNULL term in the subquery of the second statment is needed because of
@@ -532,23 +546,23 @@ ContentPrefService2.prototype = {
       DELETE FROM settings
       WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
     `));
 
     let prefs = new ContentPrefStore();
 
     let isPrivate = context && context.usePrivateBrowsing;
     this._execStmts(stmts, {
-      onRow: function onRow(row) {
+      onRow: row => {
         let grp = row.getResultByName("grp");
         let name = row.getResultByName("name");
         prefs.set(grp, name, undefined);
         this._cache.set(grp, name, undefined);
       },
-      onDone: function onDone(reason, ok) {
+      onDone: (reason, ok) => {
         if (ok && isPrivate) {
           for (let [sgroup, sname, ] of this._pbStore) {
             if (!group ||
                 (!includeSubdomains && group == sgroup) ||
                 (includeSubdomains && sgroup && this._pbStore.groupsMatchIncludingSubdomains(group, sgroup))) {
               prefs.set(sgroup, sname, undefined);
               this._pbStore.remove(sgroup, sname);
             }
@@ -556,17 +570,17 @@ ContentPrefService2.prototype = {
         }
         cbHandleCompletion(callback, reason);
         if (ok) {
           for (let [sgroup, sname, ] of prefs) {
             this._notifyPrefRemoved(sgroup, sname, isPrivate);
           }
         }
       },
-      onError: function onError(nsresult) {
+      onError: nsresult => {
         cbHandleError(callback, nsresult);
       }
     });
   },
 
   _removeAllDomainsSince: function CPS2__removeAllDomainsSince(since, context, callback) {
     checkCallbackArg(callback, false);
 
@@ -598,23 +612,23 @@ ContentPrefService2.prototype = {
     stmts.push(stmt);
 
     // Cleanup no longer used values.
     stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
 
     let prefs = new ContentPrefStore();
     let isPrivate = context && context.usePrivateBrowsing;
     this._execStmts(stmts, {
-      onRow: function onRow(row) {
+      onRow: row => {
         let grp = row.getResultByName("grp");
         let name = row.getResultByName("name");
         prefs.set(grp, name, undefined);
         this._cache.set(grp, name, undefined);
       },
-      onDone: function onDone(reason, ok) {
+      onDone: (reason, ok) => {
         // This nukes all the groups in _pbStore since we don't have their timestamp
         // information.
         if (ok && isPrivate) {
           for (let [sgroup, sname, ] of this._pbStore) {
             if (sgroup) {
               prefs.set(sgroup, sname, undefined);
             }
           }
@@ -622,17 +636,17 @@ ContentPrefService2.prototype = {
         }
         cbHandleCompletion(callback, reason);
         if (ok) {
           for (let [sgroup, sname, ] of prefs) {
             this._notifyPrefRemoved(sgroup, sname, isPrivate);
           }
         }
       },
-      onError: function onError(nsresult) {
+      onError: nsresult => {
         cbHandleError(callback, nsresult);
       }
     });
   },
 
   removeAllDomainsSince: function CPS2_removeAllDomainsSince(since, context, callback) {
     this._removeAllDomainsSince(since, context, callback);
   },
@@ -690,65 +704,56 @@ ContentPrefService2.prototype = {
         SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
       )
     `));
 
     let prefs = new ContentPrefStore();
     let isPrivate = context && context.usePrivateBrowsing;
 
     this._execStmts(stmts, {
-      onRow: function onRow(row) {
+      onRow: row => {
         let grp = row.getResultByName("grp");
         prefs.set(grp, name, undefined);
         this._cache.set(grp, name, undefined);
       },
-      onDone: function onDone(reason, ok) {
+      onDone: (reason, ok) => {
         if (ok && isPrivate) {
           for (let [sgroup, sname, ] of this._pbStore) {
             if (sname === name) {
               prefs.set(sgroup, name, undefined);
               this._pbStore.remove(sgroup, name);
             }
           }
         }
         cbHandleCompletion(callback, reason);
         if (ok) {
           for (let [sgroup, , ] of prefs) {
             this._notifyPrefRemoved(sgroup, name, isPrivate);
           }
         }
       },
-      onError: function onError(nsresult) {
+      onError: nsresult => {
         cbHandleError(callback, nsresult);
       }
     });
   },
 
-  destroy: function CPS2_destroy() {
-    if (this._statements) {
-      for (let sql in this._statements) {
-        let stmt = this._statements[sql];
-        stmt.finalize();
-      }
-    }
-  },
-
   /**
    * Returns the cached mozIStorageAsyncStatement for the given SQL.  If no such
    * statement is cached, one is created and cached.
    *
    * @param sql  The SQL query string.
    * @return     The cached, possibly new, statement.
    */
-  _stmt: function CPS2__stmt(sql) {
-    if (!this._statements)
-      this._statements = {};
-    if (!this._statements[sql])
-      this._statements[sql] = this._dbConnection.createAsyncStatement(sql);
-    return this._statements[sql];
+  _stmt: function CPS2__stmt(sql, cachable = true) {
+    return {
+      sql,
+      cachable,
+      params: {},
+    };
   },
 
   /**
    * Executes some async statements.
    *
    * @param stmts      An array of mozIStorageAsyncStatements.
    * @param callbacks  An object with the following methods:
    *                   onRow(row) (optional)
@@ -758,52 +763,53 @@ ContentPrefService2.prototype = {
    *                     Called when done.
    *                     reason: A nsIContentPrefService2.COMPLETE_* value.
    *                     reasonOK: reason == nsIContentPrefService2.COMPLETE_OK.
    *                     didGetRow: True if onRow was ever called.
    *                   onError(nsresult) (optional)
    *                     Called on error.
    *                     nsresult: The error code.
    */
-  _execStmts: function CPS2__execStmts(stmts, callbacks) {
-    let self = this;
+  _execStmts: async function CPS2__execStmts(stmts, callbacks) {
+    let conn = await this.conn;
+    let ok = true;
     let gotRow = false;
-    this._dbConnection.executeAsync(stmts, stmts.length, {
-      handleResult: function handleResult(results) {
+    let { onRow, onError } = callbacks;
+    await conn.executeTransaction(async () => {
+      for (let {sql, params, cachable} of stmts) {
         try {
-          let row = null;
-          while ((row = results.getNextRow())) {
+          let execute = cachable ? conn.executeCached : conn.execute;
+          await execute.call(conn, sql, params, row => {
             gotRow = true;
-            if (callbacks.onRow)
-              callbacks.onRow.call(self, row);
+            if (onRow) {
+              try {
+                onRow(row);
+              } catch (e) {
+                Cu.reportError(e);
+              }
+            }
+          });
+        } catch (e) {
+          try {
+            onError(Cr.NS_ERROR_FAILURE);
+          } catch (err) {
+            ok = false;
+            Cu.reportError(e);
           }
-        } catch (err) {
-          Cu.reportError(err);
-        }
-      },
-      handleCompletion: function handleCompletion(reason) {
-        try {
-          let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED;
-          callbacks.onDone.call(self,
-                                ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK :
-                                  Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
-                                ok, gotRow);
-        } catch (err) {
-          Cu.reportError(err);
-        }
-      },
-      handleError: function handleError(error) {
-        try {
-          if (callbacks.onError)
-            callbacks.onError.call(self, Cr.NS_ERROR_FAILURE);
-        } catch (err) {
-          Cu.reportError(err);
         }
       }
     });
+
+    try {
+      callbacks.onDone(ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK :
+                       Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
+                       ok, gotRow);
+    } catch (e) {
+      Cu.reportError(e);
+    }
   },
 
   __grouper: null,
   get _grouper() {
     if (!this.__grouper)
       this.__grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"].
                        getService(Ci.nsIContentURIGrouper);
     return this.__grouper;
@@ -916,48 +922,50 @@ ContentPrefService2.prototype = {
    * Tests use this as a backchannel by calling it directly.
    *
    * @param subj   This value depends on topic.
    * @param topic  The backchannel "method" name.
    * @param data   This value depends on topic.
    */
   observe: function CPS2_observe(subj, topic, data) {
     switch (topic) {
-    case "xpcom-shutdown":
+    case "profile-before-change":
       this._destroy();
       break;
     case "last-pb-context-exited":
       this._pbStore.removeAll();
       break;
     case "test:reset":
       let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
       this._reset(fn);
       break;
     case "test:db":
       let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
-      obj.value = this._dbConnection;
+      obj.value = this.conn;
       break;
     }
   },
 
   /**
    * Removes all state from the service.  Used by tests.
    *
    * @param callback  A function that will be called when done.
    */
-  _reset: function CPS2__reset(callback) {
+  async _reset(callback) {
     this._pbStore.removeAll();
     this._cache.removeAll();
 
     this._observers = {};
     this._genericObservers = [];
 
     let tables = ["prefs", "groups", "settings"];
     let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`));
-    this._execStmts(stmts, { onDone: () => callback() });
+    this._execStmts(stmts, { onDone: () => {
+      callback();
+    } });
   },
 
   QueryInterface: function CPS2_QueryInterface(iid) {
     let supportedIIDs = [
       Ci.nsIContentPrefService2,
       Ci.nsIObserver,
       Ci.nsISupports,
     ];
@@ -996,196 +1004,189 @@ ContentPrefService2.prototype = {
       },
       prefs_idx: {
         table: "prefs",
         columns: ["timestamp", "groupID", "settingID"]
       }
     }
   },
 
-  _dbConnection: null,
+  _debugLog: false,
 
-  // _dbInit and the methods it calls (_dbCreate, _dbMigrate, and version-
-  // specific migration methods) must be careful not to call any method
-  // of the service that assumes the database connection has already been
-  // initialized, since it won't be initialized until at the end of _dbInit.
-
-  _dbInit: function ContentPrefService__dbInit() {
-    var dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
-    dbFile.append("content-prefs.sqlite");
+  log: function CPS2_log(aMessage) {
+    if (this._debugLog) {
+      Services.console.logStringMessage("ContentPrefService2: " + aMessage);
+    }
+  },
 
-    var dbConnection;
+  async _getConnection(aAttemptNum = 0) {
+    let path = OS.Path.join(OS.Constants.Path.profileDir, "content-prefs.sqlite");
+    let conn;
+    let resetAndRetry = async e => {
+      if (e.status != Cr.NS_ERROR_FILE_CORRUPTED) {
+        throw e;
+      }
 
-    if (!dbFile.exists())
-      dbConnection = this._dbCreate(dbFile);
-    else {
-      try {
-        dbConnection = Services.storage.openDatabase(dbFile);
-      } catch (e) {
-        // If the connection isn't ready after we open the database, that means
-        // the database has been corrupted, so we back it up and then recreate it.
-        if (e.result != Cr.NS_ERROR_FILE_CORRUPTED)
-          throw e;
-        dbConnection = this._dbBackUpAndRecreate(dbFile, dbConnection);
+      if (aAttemptNum >= this.MAX_ATTEMPTS) {
+        if (conn) {
+          await conn.close();
+        }
+        this.log("Establishing connection failed too many times. Giving up.");
+        throw e;
       }
 
-      // Get the version of the schema in the file.
-      var version = dbConnection.schemaVersion;
+      try {
+        await this._failover(conn, path);
+      } catch (e) {
+        Cu.reportError(e);
+        throw e;
+      }
+      return this._getConnection(++aAttemptNum);
+    };
+    try {
+      conn = await Sqlite.openConnection({ path });
+      Sqlite.shutdown.addBlocker(
+        "Closing ContentPrefService2 connection.",
+        () => conn.close());
+    } catch (e) {
+      Cu.reportError(e);
+      return resetAndRetry(e);
+    }
 
-      // Try to migrate the schema in the database to the current schema used by
-      // the service.  If migration fails, back up the database and recreate it.
-      if (version != this._dbVersion) {
-        try {
-          this._dbMigrate(dbConnection, version, this._dbVersion);
-        } catch (ex) {
-          Cu.reportError("error migrating DB: " + ex + "; backing up and recreating");
-          dbConnection = this._dbBackUpAndRecreate(dbFile, dbConnection);
-        }
-      }
+    try {
+      await this._dbMaybeInit(conn);
+    } catch (e) {
+      Cu.reportError(e);
+      return resetAndRetry(e);
     }
 
     // Turn off disk synchronization checking to reduce disk churn and speed up
     // operations when prefs are changed rapidly (such as when a user repeatedly
     // changes the value of the browser zoom setting for a site).
     //
     // Note: this could cause database corruption if the OS crashes or machine
     // loses power before the data gets written to disk, but this is considered
     // a reasonable risk for the not-so-critical data stored in this database.
     //
     // If you really don't want to take this risk, however, just set the
     // toolkit.storage.synchronous pref to 1 (NORMAL synchronization) or 2
     // (FULL synchronization), in which case mozStorageConnection::Initialize
     // will use that value, and we won't override it here.
     if (!Services.prefs.prefHasUserValue("toolkit.storage.synchronous"))
-      dbConnection.executeSimpleSQL("PRAGMA synchronous = OFF");
+      await conn.execute("PRAGMA synchronous = OFF");
 
-    this._dbConnection = dbConnection;
+    return conn;
   },
 
-  _dbCreate: function ContentPrefService__dbCreate(aDBFile) {
-    var dbConnection = Services.storage.openDatabase(aDBFile);
-
-    try {
-      this._dbCreateSchema(dbConnection);
-      dbConnection.schemaVersion = this._dbVersion;
-    } catch (ex) {
-      // If we failed to create the database (perhaps because the disk ran out
-      // of space), then remove the database file so we don't leave it in some
-      // half-created state from which we won't know how to recover.
-      dbConnection.close();
-      aDBFile.remove(false);
-      throw ex;
+  async _failover(aConn, aPath) {
+    this.log("Cleaning up DB file - close & remove & backup.");
+    if (aConn) {
+      await aConn.close();
     }
-
-    return dbConnection;
+    let backupFile = aPath + ".corrupt";
+    let { file, path: uniquePath } =
+      await OS.File.openUnique(backupFile, { humanReadable: true });
+    await file.close();
+    await OS.File.copy(aPath, uniquePath);
+    await OS.File.remove(aPath);
+    this.log("Completed DB cleanup.");
   },
 
-  _dbCreateSchema: function ContentPrefService__dbCreateSchema(aDBConnection) {
-    this._dbCreateTables(aDBConnection);
-    this._dbCreateIndices(aDBConnection);
-  },
+  _dbMaybeInit: async function CPS2__dbMaybeInit(aConn) {
+    let version = parseInt(await aConn.getSchemaVersion(), 10);
+    this.log("Schema version: " + version);
 
-  _dbCreateTables: function ContentPrefService__dbCreateTables(aDBConnection) {
-    for (let name in this._dbSchema.tables)
-      aDBConnection.createTable(name, this._dbSchema.tables[name]);
-  },
-
-  _dbCreateIndices: function ContentPrefService__dbCreateIndices(aDBConnection) {
-    for (let name in this._dbSchema.indices) {
-      let index = this._dbSchema.indices[name];
-      let statement = `
-        CREATE INDEX IF NOT EXISTS ${name} ON ${index.table}
-        (${index.columns.join(", ")})
-      `;
-      aDBConnection.executeSimpleSQL(statement);
+    if (version == 0) {
+      await this._dbCreateSchema(aConn);
+    } else if (version != this._dbVersion) {
+      await this._dbMigrate(aConn, version, this._dbVersion);
     }
   },
 
-  _dbBackUpAndRecreate: function ContentPrefService__dbBackUpAndRecreate(aDBFile,
-                                                                         aDBConnection) {
-    Services.storage.backupDatabaseFile(aDBFile, "content-prefs.sqlite.corrupt");
+  _createTable: async function CPS2__createTable(aConn, aName) {
+    let tSQL = this._dbSchema.tables[aName];
+    this.log("Creating table " + aName + " with " + tSQL);
+    await aConn.execute(`CREATE TABLE ${aName} (${tSQL})`);
+  },
 
-    // Close the database, ignoring the "already closed" exception, if any.
-    // It'll be open if we're here because of a migration failure but closed
-    // if we're here because of database corruption.
-    try { aDBConnection.close(); } catch (ex) {}
-
-    aDBFile.remove(false);
-
-    let dbConnection = this._dbCreate(aDBFile);
-
-    return dbConnection;
+  _createIndex: async function CPS2__createTable(aConn, aName) {
+    let index = this._dbSchema.indices[aName];
+    let statement = "CREATE INDEX IF NOT EXISTS " + aName + " ON " + index.table +
+                    "(" + index.columns.join(", ") + ")";
+    await aConn.execute(statement);
   },
 
-  _dbMigrate: function ContentPrefService__dbMigrate(aDBConnection, aOldVersion, aNewVersion) {
+  _dbCreateSchema: async function CPS2__dbCreateSchema(aConn) {
+    await aConn.executeTransaction(async () => {
+      this.log("Creating DB -- tables");
+      for (let name in this._dbSchema.tables) {
+        await this._createTable(aConn, name);
+      }
+
+      this.log("Creating DB -- indices");
+      for (let name in this._dbSchema.indices) {
+        await this._createIndex(aConn, name);
+      }
+
+      await aConn.setSchemaVersion(this._dbVersion);
+    });
+  },
+
+  _dbMigrate: async function CPS2__dbMigrate(aConn, aOldVersion, aNewVersion) {
     /**
      * Migrations should follow the template rules in bug 1074817 comment 3 which are:
      * 1. Migration should be incremental and non-breaking.
      * 2. It should be idempotent because one can downgrade an upgrade again.
      * On downgrade:
      * 1. Decrement schema version so that upgrade runs the migrations again.
      */
-    aDBConnection.beginTransaction();
-
-    try {
-       /**
-       * If the schema version is 0, that means it was never set, which means
-       * the database was somehow created without the schema being applied, perhaps
-       * because the system ran out of disk space (although we check for this
-       * in _createDB) or because some other code created the database file without
-       * applying the schema.  In any case, recover by simply reapplying the schema.
-       */
-      if (aOldVersion == 0) {
-        this._dbCreateSchema(aDBConnection);
-      } else {
-        for (let i = aOldVersion; i < aNewVersion; i++) {
-          let migrationName = "_dbMigrate" + i + "To" + (i + 1);
-          if (typeof this[migrationName] != "function") {
-            throw new Error("no migrator function from version " + aOldVersion + " to version " +
-                            aNewVersion);
-          }
-          this[migrationName](aDBConnection);
+    await aConn.executeTransaction(async () => {
+      for (let i = aOldVersion; i < aNewVersion; i++) {
+        let migrationName = "_dbMigrate" + i + "To" + (i + 1);
+        if (typeof this[migrationName] != "function") {
+          throw new Error("no migrator function from version " + aOldVersion + " to version " +
+                          aNewVersion);
         }
+        await this[migrationName](aConn);
       }
-      aDBConnection.schemaVersion = aNewVersion;
-      aDBConnection.commitTransaction();
-    } catch (ex) {
-      aDBConnection.rollbackTransaction();
-      throw ex;
-    }
+      await aConn.setSchemaVersion(aNewVersion);
+    });
   },
 
-  _dbMigrate1To2: function ContentPrefService___dbMigrate1To2(aDBConnection) {
-    aDBConnection.executeSimpleSQL("ALTER TABLE groups RENAME TO groupsOld");
-    aDBConnection.createTable("groups", this._dbSchema.tables.groups);
-    aDBConnection.executeSimpleSQL(`
+  _dbMigrate1To2: async function CPS2___dbMigrate1To2(aConn) {
+    await aConn.execute("ALTER TABLE groups RENAME TO groupsOld");
+    await this._createTable(aConn, "groups");
+    await aConn.execute(`
       INSERT INTO groups (id, name)
       SELECT id, name FROM groupsOld
     `);
 
-    aDBConnection.executeSimpleSQL("DROP TABLE groupers");
-    aDBConnection.executeSimpleSQL("DROP TABLE groupsOld");
-  },
-
-  _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) {
-    this._dbCreateIndices(aDBConnection);
+    await aConn.execute("DROP TABLE groupers");
+    await aConn.execute("DROP TABLE groupsOld");
   },
 
-  _dbMigrate3To4: function ContentPrefService__dbMigrate3To4(aDBConnection) {
+  _dbMigrate2To3: async function CPS2__dbMigrate2To3(aConn) {
+    for (let name in this._dbSchema.indices) {
+      await this._createIndex(aConn, name);
+    }
+  },
+
+  _dbMigrate3To4: async function CPS2__dbMigrate3To4(aConn) {
     // Add timestamp column if it does not exist yet. This operation is idempotent.
     try {
-      let stmt = aDBConnection.createStatement("SELECT timestamp FROM prefs");
-      stmt.finalize();
+      await aConn.execute("SELECT timestamp FROM prefs");
     } catch (e) {
-      aDBConnection.executeSimpleSQL("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0");
+      await aConn.execute("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0");
     }
 
     // To modify prefs_idx drop it and create again.
-    aDBConnection.executeSimpleSQL("DROP INDEX IF EXISTS prefs_idx");
-    this._dbCreateIndices(aDBConnection);
+    await aConn.execute("DROP INDEX IF EXISTS prefs_idx");
+    for (let name in this._dbSchema.indices) {
+      await this._createIndex(aConn, name);
+    }
   },
 };
 
 function checkGroupArg(group) {
   if (!group || typeof(group) != "string")
     throw invalidArg("domain must be nonempty string.");
 }