Bug 887889 - Migrate ContentPrefService2 to Sqlite.jsm r=mak
authorDoug Thayer <dothayer@mozilla.com>
Mon, 12 Mar 2018 14:41:42 -0700
changeset 415934 29598c587f7fb07c434df30534bdf489f987562d
parent 415933 fec4af27b939fe5cf2a5d6ab6290e1feaa302a18
child 415935 744a7b9fe0abd1095d0f5ba464161d51a97a33b7
push id33911
push usercsabou@mozilla.com
push dateFri, 27 Apr 2018 10:01:39 +0000
treeherdermozilla-central@822936017145 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs887889
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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.");
 }