Bug 1036440 - Refactor and use async/await and withConnectionWrapper/promiseDBConnection in PlacesDBUtils, r?mak draft
authormilindl <i.milind.luthra@gmail.com>
Fri, 19 May 2017 23:45:40 +0530
changeset 584954 3dfda9c48ec2a6a285ad7fe1b8e4b348b27e1760
parent 584877 5d6fe59a9a5dfd2d381d2cdfdc968d3305b417cf
child 584955 dbcae503b6205e10ee419487a9563ae799033dd1
push id60953
push userbmo:i.milind.luthra@gmail.com
push dateFri, 26 May 2017 09:07:32 +0000
reviewersmak
bugs1036440
milestone55.0a1
Bug 1036440 - Refactor and use async/await and withConnectionWrapper/promiseDBConnection in PlacesDBUtils, r?mak Convert functions to async/await, and all DBConns to withConnectionWrapper to make PlacesDBUtils as asynchronous as possible. Replaces the `Tasks` object and `_executeTasks` method with a simpler mechanism of waiting on the various (async) functions. The entire system of text based logging using the prefixes ">", "+", "-" has been revamped so that `runTasks`, 'maintenanceOnIdle`and `checkAndFixDatabase` return a Map. A method `getLegacyLog` is provided to convert this Map to the legacy logs. The logs are structured slightly differently from the legacy logs, but the prefix system is essentially the same. The current consumers of do not suffer due to this change, while any new consumers should not use the method at all. MozReview-Commit-ID: 9Fm7cFyFID3
toolkit/components/places/PlacesDBUtils.jsm
--- a/toolkit/components/places/PlacesDBUtils.jsm
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -6,841 +6,890 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
 
 this.EXPORTED_SYMBOLS = [ "PlacesDBUtils" ];
 
 // Constants
 
-const FINISHED_MAINTENANCE_TOPIC = "places-maintenance-finished";
-
 const BYTES_PER_MEBIBYTE = 1048576;
 
-// Smart getters
-
-XPCOMUtils.defineLazyGetter(this, "DBConn", function() {
-  return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
-});
-
-// PlacesDBUtils
-
 this.PlacesDBUtils = {
+
   /**
-   * Executes a list of maintenance tasks.
-   * Once finished it will pass a array log to the callback attached to tasks.
-   * FINISHED_MAINTENANCE_TOPIC is notified through observer service on finish.
-   *
-   * @param aTasks
-   *        Tasks object to execute.
+   * Converts the `Map` returned by `runTasks` to an array of messages (legacy).
+   * @param taskStatusMap
+   *        The Map[String -> Object] returned by `runTasks`.
+   * @return an array of log messages.
    */
-  _executeTasks: function PDBU__executeTasks(aTasks) {
-    if (PlacesDBUtils._isShuttingDown) {
-      aTasks.log("- We are shutting down. Will not schedule the tasks.");
-      aTasks.clear();
+  getLegacyLog(taskStatusMap) {
+    let logs = [];
+    for (let [key, value] of taskStatusMap) {
+      logs.push(`> Task: ${key}`);
+      let prefix = value.succeeded ? "+ " : "- ";
+      logs = logs.concat(value.logs.map(m => `${prefix}${m}`));
     }
-
-    let task = aTasks.pop();
-    if (task) {
-      task.call(PlacesDBUtils, aTasks);
-    } else {
-      // All tasks have been completed.
-      // Telemetry the time it took for maintenance, if a start time exists.
-      if (aTasks._telemetryStart) {
-        Services.telemetry.getHistogramById("PLACES_IDLE_MAINTENANCE_TIME_MS")
-                          .add(Date.now() - aTasks._telemetryStart);
-        aTasks._telemetryStart = 0;
-      }
-
-      if (aTasks.callback) {
-        let scope = aTasks.scope || Cu.getGlobalForObject(aTasks.callback);
-        aTasks.callback.call(scope, aTasks.messages);
-      }
-
-      // Notify observers that maintenance finished.
-      Services.obs.notifyObservers(null, FINISHED_MAINTENANCE_TOPIC);
-    }
+    return logs;
   },
 
   _isShuttingDown: false,
-  shutdown: function PDBU_shutdown() {
+  shutdown() {
     PlacesDBUtils._isShuttingDown = true;
   },
 
+  _clearTaskQueue: false,
+  clearPendingTasks() {
+    PlacesDBUtils._clearTaskQueue = true;
+  },
+
   /**
    * Executes integrity check and common maintenance tasks.
    *
-   * @param [optional] aCallback
-   *        Callback to be invoked when done.  The callback will get a array
-   *        of log messages.
-   * @param [optional] aScope
-   *        Scope for the callback.
+   * @return a Map[taskName(String) -> Object]. The Object has the following properties:
+   *         - succeeded: boolean
+   *         - logs: an array of strings containing the messages logged by the task.
    */
-  maintenanceOnIdle: function PDBU_maintenanceOnIdle(aCallback, aScope) {
-    let tasks = new Tasks([
-      this.checkIntegrity
-    , this.checkCoherence
-    , this._refreshUI
-    ]);
-    tasks._telemetryStart = Date.now();
-    tasks.callback = function() {
-      Services.prefs.setIntPref("places.database.lastMaintenance",
-                                parseInt(Date.now() / 1000));
-      if (aCallback)
-        aCallback();
-    }
-    tasks.scope = aScope;
-    this._executeTasks(tasks);
+  async maintenanceOnIdle() {
+    let tasks = [
+      this.checkIntegrity,
+      this.checkCoherence,
+      this._refreshUI
+    ];
+    let telemetryStartTime = Date.now();
+    let taskStatusMap = await PlacesDBUtils.runTasks(tasks);
+
+    Services.prefs.setIntPref("places.database.lastMaintenance",
+                               parseInt(Date.now() / 1000));
+    Services.telemetry.getHistogramById("PLACES_IDLE_MAINTENANCE_TIME_MS")
+                      .add(Date.now() - telemetryStartTime);
+    return taskStatusMap;
   },
 
   /**
    * Executes integrity check, common and advanced maintenance tasks (like
    * expiration and vacuum).  Will also collect statistics on the database.
    *
-   * @param [optional] aCallback
-   *        Callback to be invoked when done.  The callback will get a array
-   *        of log messages.
-   * @param [optional] aScope
-   *        Scope for the callback.
+   * @return a Map[taskName(String) -> Object]. The Object has the following properties:
+   *         - succeeded: boolean
+   *         - logs: an array of strings containing the messages logged by the task.
    */
-  checkAndFixDatabase: function PDBU_checkAndFixDatabase(aCallback, aScope) {
-    let tasks = new Tasks([
-      this.checkIntegrity
-    , this.checkCoherence
-    , this.expire
-    , this.vacuum
-    , this.stats
-    , this._refreshUI
-    ]);
-    tasks.callback = aCallback;
-    tasks.scope = aScope;
-    this._executeTasks(tasks);
+  async checkAndFixDatabase() {
+    let tasks = [
+      this.checkIntegrity,
+      this.checkCoherence,
+      this.expire,
+      this.vacuum,
+      this.stats,
+      this._refreshUI,
+    ];
+    return PlacesDBUtils.runTasks(tasks);
   },
 
   /**
    * Forces a full refresh of Places views.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
+   * @return {Promise} resolved when refresh is complete.
+   * @resolves to an array of logs for this task.
    */
-  _refreshUI: function PDBU__refreshUI(aTasks) {
-    let tasks = new Tasks(aTasks);
-
+  async _refreshUI() {
     // Send batch update notifications to update the UI.
-    PlacesUtils.history.runInBatchMode({
-      runBatched(aUserData) {}
-    }, null);
-    PlacesDBUtils._executeTasks(tasks);
-  },
-
-  _handleError: function PDBU__handleError(aError) {
-    Cu.reportError("Async statement execution returned with '" +
-                   aError.result + "', '" + aError.message + "'");
+    let observers = PlacesUtils.history.getObservers();
+    for (let observer of observers) {
+      observer.onBeginUpdateBatch();
+      observer.onEndUpdateBatch();
+    }
+    return [];
   },
 
   /**
    * Tries to execute a REINDEX on the database.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
+   * @return {Promise} resolved when reindex is complete.
+   * @resolves to an array of logs for this task.
+   * @rejects if we're unable to reindex the database.
    */
-  reindex: function PDBU_reindex(aTasks) {
-    let tasks = new Tasks(aTasks);
-    tasks.log("> Reindex");
-
-    let stmt = DBConn.createAsyncStatement("REINDEX");
-    stmt.executeAsync({
-      handleError: PlacesDBUtils._handleError,
-      handleResult() {},
-
-      handleCompletion(aReason) {
-        if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
-          tasks.log("+ The database has been reindexed");
-        } else {
-          tasks.log("- Unable to reindex database");
-        }
-
-        PlacesDBUtils._executeTasks(tasks);
-      }
-    });
-    stmt.finalize();
+  async reindex() {
+    try {
+      let logs = [];
+      await PlacesUtils.withConnectionWrapper(
+        "PlacesDBUtils: Reindex the database",
+        async (db) => {
+          let query = "REINDEX";
+          await db.execute(query);
+          logs.push("The database has been re indexed");
+        });
+      return logs;
+    } catch (ex) {
+      throw new Error("Unable to reindex the database.");
+    }
   },
 
   /**
    * Checks integrity but does not try to fix the database through a reindex.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
+   * @return {Promise} resolves if database is sane.
+   * @resolves to an array of logs for this task.
+   * @rejects if we're unable to fix corruption or unable to check status.
    */
-  _checkIntegritySkipReindex: function PDBU__checkIntegritySkipReindex(aTasks) {
-    return this.checkIntegrity(aTasks, true);
+  async _checkIntegritySkipReindex() {
+    return this.checkIntegrity(true);
   },
 
   /**
    * Checks integrity and tries to fix the database through a reindex.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
-   * @param [optional] aSkipdReindex
+   * @param [optional] skipReindex
    *        Whether to try to reindex database or not.
+   *
+   * @return {Promise} resolves if database is sane or is made sane.
+   * @resolves to an array of logs for this task.
+   * @rejects if we're unable to fix corruption or unable to check status.
    */
-  checkIntegrity: function PDBU_checkIntegrity(aTasks, aSkipReindex) {
-    let tasks = new Tasks(aTasks);
-    tasks.log("> Integrity check");
-
-    // Run a integrity check, but stop at the first error.
-    let stmt = DBConn.createAsyncStatement("PRAGMA integrity_check(1)");
-    stmt.executeAsync({
-      handleError: PlacesDBUtils._handleError,
-
-      _corrupt: false,
-      handleResult(aResultSet) {
-        let row = aResultSet.getNextRow();
-        this._corrupt = row.getResultByIndex(0) != "ok";
-      },
+  async checkIntegrity(skipReindex) {
+    let logs = [];
 
-      handleCompletion(aReason) {
-        if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
-          if (this._corrupt) {
-            tasks.log("- The database is corrupt");
-            if (aSkipReindex) {
-              tasks.log("- Unable to fix corruption, database will be replaced on next startup");
-              Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
-              tasks.clear();
-            } else {
-              // Try to reindex, this often fixed simple indices corruption.
-              // We insert from the top of the queue, they will run inverse.
-              tasks.push(PlacesDBUtils._checkIntegritySkipReindex);
-              tasks.push(PlacesDBUtils.reindex);
-            }
+    try {
+      // Run a integrity check, but stop at the first error.
+      await PlacesUtils.withConnectionWrapper("PlacesDBUtils: check the integrity", async (db) => {
+        let row;
+        await db.execute(
+          "PRAGMA integrity_check",
+          null,
+          r => {
+            row = r;
+            throw StopIteration;
+          });
+        if (row.getResultByIndex(0) === "ok") {
+          logs.push("The database is sane");
+        } else {
+          // We stopped due to an integrity corruption, try to fix if possible.
+          logs.push("The database is corrupt");
+          if (skipReindex) {
+            Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
+            PlacesDBUtils.clearPendingTasks();
+            throw new Error("Unable to fix corruption, database will be replaced on next startup");
           } else {
-            tasks.log("+ The database is sane");
+            // Try to reindex, this often fixes simple indices corruption.
+            let reindexLogs = await PlacesDBUtils.reindex();
+            let checkLogs = await PlacesDBUtils._checkIntegritySkipReindex();
+            logs = logs.concat(reindexLogs).concat(checkLogs);
           }
-        } else {
-          tasks.log("- Unable to check database status");
-          tasks.clear();
         }
-
-        PlacesDBUtils._executeTasks(tasks);
+      });
+    } catch (ex) {
+      if (ex.message.indexOf("Unable to fix corruption") !== 0) {
+        // There was some other error, so throw.
+        PlacesDBUtils.clearPendingTasks();
+        throw new Error("Unable to check database integrity");
       }
-    });
-    stmt.finalize();
+    }
+    return logs;
   },
 
   /**
    * Checks data coherence and tries to fix most common errors.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
+   * @return {Promise} resolves when coherence is checked.
+   * @resolves to an array of logs for this task.
+   * @rejects if database is not coherent.
    */
-  checkCoherence: function PDBU_checkCoherence(aTasks) {
-    let tasks = new Tasks(aTasks);
-    tasks.log("> Coherence check");
-
-    let stmts = PlacesDBUtils._getBoundCoherenceStatements();
-    DBConn.executeAsync(stmts, stmts.length, {
-      handleError: PlacesDBUtils._handleError,
-      handleResult() {},
+  async checkCoherence() {
+    let logs = [];
 
-      handleCompletion(aReason) {
-        if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
-          tasks.log("+ The database is coherent");
-        } else {
-          tasks.log("- Unable to check database coherence");
-          tasks.clear();
+    let stmts = await PlacesDBUtils._getBoundCoherenceStatements();
+    let allStatementsPromises = [];
+    let coherenceCheck = true;
+    await PlacesUtils.withConnectionWrapper(
+      "PlacesDBUtils: coherence check:",
+      db => db.executeTransaction(async () => {
+        for (let {query, params} of stmts) {
+          params = params ? params : null;
+          allStatementsPromises.push(db.execute(query, params).catch(ex => {
+            Cu.reportError(ex);
+            coherenceCheck = false;
+          }));
         }
+      })
+    );
 
-        PlacesDBUtils._executeTasks(tasks);
-      }
-    });
-    stmts.forEach(aStmt => aStmt.finalize());
+    await Promise.all(allStatementsPromises);
+    if (coherenceCheck) {
+      logs.push("The database is coherent");
+    } else {
+      PlacesDBUtils.clearPendingTasks();
+      throw new Error("Unable to check database coherence");
+    }
+    return logs;
   },
 
-  _getBoundCoherenceStatements: function PDBU__getBoundCoherenceStatements() {
+  async _getBoundCoherenceStatements() {
     let cleanupStatements = [];
 
     // MOZ_ANNO_ATTRIBUTES
     // A.1 remove obsolete annotations from moz_annos.
     // The 'weave0' idiom exploits character ordering (0 follows /) to
     // efficiently select all annos with a 'weave/' prefix.
-    let deleteObsoleteAnnos = DBConn.createAsyncStatement(
+    let deleteObsoleteAnnos = {
+      query:
       `DELETE FROM moz_annos
        WHERE type = 4
           OR anno_attribute_id IN (
          SELECT id FROM moz_anno_attributes
          WHERE name = 'downloads/destinationFileName' OR
                name BETWEEN 'weave/' AND 'weave0'
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteObsoleteAnnos);
 
     // A.2 remove obsolete annotations from moz_items_annos.
-    let deleteObsoleteItemsAnnos = DBConn.createAsyncStatement(
+    let deleteObsoleteItemsAnnos = {
+      query:
       `DELETE FROM moz_items_annos
        WHERE type = 4
           OR anno_attribute_id IN (
          SELECT id FROM moz_anno_attributes
          WHERE name = 'sync/children'
             OR name = 'placesInternal/GUID'
             OR name BETWEEN 'weave/' AND 'weave0'
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteObsoleteItemsAnnos);
 
     // A.3 remove unused attributes.
-    let deleteUnusedAnnoAttributes = DBConn.createAsyncStatement(
+    let deleteUnusedAnnoAttributes = {
+      query:
       `DELETE FROM moz_anno_attributes WHERE id IN (
          SELECT id FROM moz_anno_attributes n
          WHERE NOT EXISTS
              (SELECT id FROM moz_annos WHERE anno_attribute_id = n.id LIMIT 1)
            AND NOT EXISTS
              (SELECT id FROM moz_items_annos WHERE anno_attribute_id = n.id LIMIT 1)
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteUnusedAnnoAttributes);
 
     // MOZ_ANNOS
     // B.1 remove annos with an invalid attribute
-    let deleteInvalidAttributeAnnos = DBConn.createAsyncStatement(
+    let deleteInvalidAttributeAnnos = {
+      query:
       `DELETE FROM moz_annos WHERE id IN (
          SELECT id FROM moz_annos a
          WHERE NOT EXISTS
            (SELECT id FROM moz_anno_attributes
              WHERE id = a.anno_attribute_id LIMIT 1)
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteInvalidAttributeAnnos);
 
     // B.2 remove orphan annos
-    let deleteOrphanAnnos = DBConn.createAsyncStatement(
+    let deleteOrphanAnnos = {
+      query:
       `DELETE FROM moz_annos WHERE id IN (
          SELECT id FROM moz_annos a
          WHERE NOT EXISTS
            (SELECT id FROM moz_places WHERE id = a.place_id LIMIT 1)
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteOrphanAnnos);
 
     // Bookmarks roots
     // C.1 fix missing Places root
     //     Bug 477739 shows a case where the root could be wrongly removed
     //     due to an endianness issue.  We try to fix broken roots here.
-    let selectPlacesRoot = DBConn.createStatement(
-      "SELECT id FROM moz_bookmarks WHERE id = :places_root");
+    let selectPlacesRoot = {
+      query: "SELECT id FROM moz_bookmarks WHERE id = :places_root",
+      params: {}
+    };
     selectPlacesRoot.params["places_root"] = PlacesUtils.placesRootId;
-    if (!selectPlacesRoot.executeStep()) {
+    let db = await PlacesUtils.promiseDBConnection();
+    let rows = await db.execute(selectPlacesRoot.query, selectPlacesRoot.params);
+    if (rows.length === 0) {
       // We are missing the root, try to recreate it.
-      let createPlacesRoot = DBConn.createAsyncStatement(
-        `INSERT INTO moz_bookmarks (id, type, fk, parent, position, title,
-                                    guid)
-         VALUES (:places_root, 2, NULL, 0, 0, :title, :guid)`);
+      let createPlacesRoot = {
+        query:
+        `INSERT INTO moz_bookmarks (id, type, fk, parent, position, title, guid)
+           VALUES (:places_root, 2, NULL, 0, 0, :title, :guid)`,
+        params: {}
+      };
       createPlacesRoot.params["places_root"] = PlacesUtils.placesRootId;
       createPlacesRoot.params["title"] = "";
       createPlacesRoot.params["guid"] = PlacesUtils.bookmarks.rootGuid;
       cleanupStatements.push(createPlacesRoot);
 
       // Now ensure that other roots are children of Places root.
-      let fixPlacesRootChildren = DBConn.createAsyncStatement(
+      let fixPlacesRootChildren = {
+        query:
         `UPDATE moz_bookmarks SET parent = :places_root WHERE guid IN
-           ( :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid )`);
+             ( :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid )`,
+        params: {}
+      };
       fixPlacesRootChildren.params["places_root"] = PlacesUtils.placesRootId;
       fixPlacesRootChildren.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
       fixPlacesRootChildren.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
       fixPlacesRootChildren.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
       fixPlacesRootChildren.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
       cleanupStatements.push(fixPlacesRootChildren);
     }
-    selectPlacesRoot.finalize();
-
     // C.2 fix roots titles
     //     some alpha version has wrong roots title, and this also fixes them if
     //     locale has changed.
     let updateRootTitleSql = `UPDATE moz_bookmarks SET title = :title
                               WHERE id = :root_id AND title <> :title`;
     // root
-    let fixPlacesRootTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+    let fixPlacesRootTitle = {
+      query: updateRootTitleSql,
+      params: {}
+    };
     fixPlacesRootTitle.params["root_id"] = PlacesUtils.placesRootId;
     fixPlacesRootTitle.params["title"] = "";
     cleanupStatements.push(fixPlacesRootTitle);
     // bookmarks menu
-    let fixBookmarksMenuTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+    let fixBookmarksMenuTitle = {
+      query: updateRootTitleSql,
+      params: {}
+    };
     fixBookmarksMenuTitle.params["root_id"] = PlacesUtils.bookmarksMenuFolderId;
     fixBookmarksMenuTitle.params["title"] =
       PlacesUtils.getString("BookmarksMenuFolderTitle");
     cleanupStatements.push(fixBookmarksMenuTitle);
     // bookmarks toolbar
-    let fixBookmarksToolbarTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+    let fixBookmarksToolbarTitle = {
+      query: updateRootTitleSql,
+      params: {}
+    };
     fixBookmarksToolbarTitle.params["root_id"] = PlacesUtils.toolbarFolderId;
     fixBookmarksToolbarTitle.params["title"] =
       PlacesUtils.getString("BookmarksToolbarFolderTitle");
     cleanupStatements.push(fixBookmarksToolbarTitle);
     // unsorted bookmarks
-    let fixUnsortedBookmarksTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+    let fixUnsortedBookmarksTitle = {
+      query: updateRootTitleSql,
+      params: {}
+    };
     fixUnsortedBookmarksTitle.params["root_id"] = PlacesUtils.unfiledBookmarksFolderId;
     fixUnsortedBookmarksTitle.params["title"] =
       PlacesUtils.getString("OtherBookmarksFolderTitle");
     cleanupStatements.push(fixUnsortedBookmarksTitle);
     // tags
-    let fixTagsRootTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+    let fixTagsRootTitle = {
+      query: updateRootTitleSql,
+      params: {}
+    };
     fixTagsRootTitle.params["root_id"] = PlacesUtils.tagsFolderId;
     fixTagsRootTitle.params["title"] =
       PlacesUtils.getString("TagsFolderTitle");
     cleanupStatements.push(fixTagsRootTitle);
 
     // MOZ_BOOKMARKS
     // D.1 remove items without a valid place
     // if fk IS NULL we fix them in D.7
-    let deleteNoPlaceItems = DBConn.createAsyncStatement(
+    let deleteNoPlaceItems = {
+      query:
       `DELETE FROM moz_bookmarks WHERE guid NOT IN (
          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
        ) AND id IN (
          SELECT b.id FROM moz_bookmarks b
          WHERE fk NOT NULL AND b.type = :bookmark_type
            AND NOT EXISTS (SELECT url FROM moz_places WHERE id = b.fk LIMIT 1)
-       )`);
+       )`,
+      params: {}
+    };
     deleteNoPlaceItems.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
     deleteNoPlaceItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
     deleteNoPlaceItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
     deleteNoPlaceItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
     deleteNoPlaceItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
     deleteNoPlaceItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
     cleanupStatements.push(deleteNoPlaceItems);
 
     // D.2 remove items that are not uri bookmarks from tag containers
-    let deleteBogusTagChildren = DBConn.createAsyncStatement(
+    let deleteBogusTagChildren = {
+      query:
       `DELETE FROM moz_bookmarks WHERE guid NOT IN (
          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
        ) AND id IN (
          SELECT b.id FROM moz_bookmarks b
          WHERE b.parent IN
            (SELECT id FROM moz_bookmarks WHERE parent = :tags_folder)
            AND b.type <> :bookmark_type
-       )`);
+       )`,
+      params: {}
+    };
     deleteBogusTagChildren.params["tags_folder"] = PlacesUtils.tagsFolderId;
     deleteBogusTagChildren.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
     deleteBogusTagChildren.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
     deleteBogusTagChildren.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
     deleteBogusTagChildren.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
     deleteBogusTagChildren.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
     deleteBogusTagChildren.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
     cleanupStatements.push(deleteBogusTagChildren);
 
     // D.3 remove empty tags
-    let deleteEmptyTags = DBConn.createAsyncStatement(
+    let deleteEmptyTags = {
+      query:
       `DELETE FROM moz_bookmarks WHERE guid NOT IN (
          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
        ) AND id IN (
          SELECT b.id FROM moz_bookmarks b
          WHERE b.id IN
            (SELECT id FROM moz_bookmarks WHERE parent = :tags_folder)
            AND NOT EXISTS
              (SELECT id from moz_bookmarks WHERE parent = b.id LIMIT 1)
-       )`);
+       )`,
+      params: {}
+    };
     deleteEmptyTags.params["tags_folder"] = PlacesUtils.tagsFolderId;
     deleteEmptyTags.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
     deleteEmptyTags.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
     deleteEmptyTags.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
     deleteEmptyTags.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
     deleteEmptyTags.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
     cleanupStatements.push(deleteEmptyTags);
 
     // D.4 move orphan items to unsorted folder
-    let fixOrphanItems = DBConn.createAsyncStatement(
+    let fixOrphanItems = {
+      query:
       `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN (
          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
        ) AND id IN (
          SELECT b.id FROM moz_bookmarks b
          WHERE NOT EXISTS
            (SELECT id FROM moz_bookmarks WHERE id = b.parent LIMIT 1)
-       )`);
+       )`,
+      params: {}
+    };
     fixOrphanItems.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId;
     fixOrphanItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
     fixOrphanItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
     fixOrphanItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
     fixOrphanItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
     fixOrphanItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
     cleanupStatements.push(fixOrphanItems);
 
     // D.6 fix wrong item types
     //     Folders and separators should not have an fk.
     //     If they have a valid fk convert them to bookmarks. Later in D.9 we
     //     will move eventual children to unsorted bookmarks.
-    let fixBookmarksAsFolders = DBConn.createAsyncStatement(
+    let fixBookmarksAsFolders = {
+      query:
       `UPDATE moz_bookmarks SET type = :bookmark_type WHERE guid NOT IN (
          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
        ) AND id IN (
          SELECT id FROM moz_bookmarks b
          WHERE type IN (:folder_type, :separator_type)
            AND fk NOTNULL
-       )`);
+       )`,
+      params: {}
+    };
     fixBookmarksAsFolders.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
     fixBookmarksAsFolders.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER;
     fixBookmarksAsFolders.params["separator_type"] = PlacesUtils.bookmarks.TYPE_SEPARATOR;
     fixBookmarksAsFolders.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
     fixBookmarksAsFolders.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
     fixBookmarksAsFolders.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
     fixBookmarksAsFolders.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
     fixBookmarksAsFolders.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
     cleanupStatements.push(fixBookmarksAsFolders);
 
     // D.7 fix wrong item types
     //     Bookmarks should have an fk, if they don't have any, convert them to
     //     folders.
-    let fixFoldersAsBookmarks = DBConn.createAsyncStatement(
+    let fixFoldersAsBookmarks = {
+      query:
       `UPDATE moz_bookmarks SET type = :folder_type WHERE guid NOT IN (
          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
        ) AND id IN (
          SELECT id FROM moz_bookmarks b
          WHERE type = :bookmark_type
            AND fk IS NULL
-       )`);
+       )`,
+      params: {}
+    };
     fixFoldersAsBookmarks.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
     fixFoldersAsBookmarks.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER;
     fixFoldersAsBookmarks.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
     fixFoldersAsBookmarks.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
     fixFoldersAsBookmarks.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
     fixFoldersAsBookmarks.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
     fixFoldersAsBookmarks.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
     cleanupStatements.push(fixFoldersAsBookmarks);
 
     // D.9 fix wrong parents
     //     Items cannot have separators or other bookmarks
     //     as parent, if they have bad parent move them to unsorted bookmarks.
-    let fixInvalidParents = DBConn.createAsyncStatement(
+    let fixInvalidParents = {
+      query:
       `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN (
          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
        ) AND id IN (
          SELECT id FROM moz_bookmarks b
          WHERE EXISTS
            (SELECT id FROM moz_bookmarks WHERE id = b.parent
              AND type IN (:bookmark_type, :separator_type)
              LIMIT 1)
-       )`);
+       )`,
+      params: {}
+    };
     fixInvalidParents.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId;
     fixInvalidParents.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
     fixInvalidParents.params["separator_type"] = PlacesUtils.bookmarks.TYPE_SEPARATOR;
     fixInvalidParents.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
     fixInvalidParents.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
     fixInvalidParents.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
     fixInvalidParents.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
     fixInvalidParents.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
     cleanupStatements.push(fixInvalidParents);
 
     // D.10 recalculate positions
     //      This requires multiple related statements.
     //      We can detect a folder with bad position values comparing the sum of
     //      all distinct position values (+1 since position is 0-based) with the
     //      triangular numbers obtained by the number of children (n).
     //      SUM(DISTINCT position + 1) == (n * (n + 1) / 2).
-    cleanupStatements.push(DBConn.createAsyncStatement(
+    cleanupStatements.push({
+      query:
       `CREATE TEMP TABLE IF NOT EXISTS moz_bm_reindex_temp (
          id INTEGER PRIMARY_KEY
        , parent INTEGER
        , position INTEGER
        )`
-    ));
-    cleanupStatements.push(DBConn.createAsyncStatement(
+    });
+    cleanupStatements.push({
+      query:
       `INSERT INTO moz_bm_reindex_temp
        SELECT id, parent, 0
        FROM moz_bookmarks b
        WHERE parent IN (
          SELECT parent
          FROM moz_bookmarks
          GROUP BY parent
          HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0
        )
        ORDER BY parent ASC, position ASC, ROWID ASC`
-    ));
-    cleanupStatements.push(DBConn.createAsyncStatement(
+    });
+    cleanupStatements.push({
+      query:
       `CREATE INDEX IF NOT EXISTS moz_bm_reindex_temp_index
        ON moz_bm_reindex_temp(parent)`
-    ));
-    cleanupStatements.push(DBConn.createAsyncStatement(
+    });
+    cleanupStatements.push({
+      query:
       `UPDATE moz_bm_reindex_temp SET position = (
          ROWID - (SELECT MIN(t.ROWID) FROM moz_bm_reindex_temp t
                   WHERE t.parent = moz_bm_reindex_temp.parent)
        )`
-    ));
-    cleanupStatements.push(DBConn.createAsyncStatement(
+    });
+    cleanupStatements.push({
+      query:
       `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_reindex_temp_trigger
        BEFORE DELETE ON moz_bm_reindex_temp
        FOR EACH ROW
        BEGIN
          UPDATE moz_bookmarks SET position = OLD.position WHERE id = OLD.id;
        END`
-    ));
-    cleanupStatements.push(DBConn.createAsyncStatement(
-      "DELETE FROM moz_bm_reindex_temp "
-    ));
-    cleanupStatements.push(DBConn.createAsyncStatement(
-      "DROP INDEX moz_bm_reindex_temp_index "
-    ));
-    cleanupStatements.push(DBConn.createAsyncStatement(
-      "DROP TRIGGER moz_bm_reindex_temp_trigger "
-    ));
-    cleanupStatements.push(DBConn.createAsyncStatement(
-      "DROP TABLE moz_bm_reindex_temp "
-    ));
+    });
+    cleanupStatements.push({
+      query: "DELETE FROM moz_bm_reindex_temp "
+    });
+    cleanupStatements.push({
+      query: "DROP INDEX moz_bm_reindex_temp_index "
+    });
+    cleanupStatements.push({
+      query: "DROP TRIGGER moz_bm_reindex_temp_trigger "
+    });
+    cleanupStatements.push({
+      query: "DROP TABLE moz_bm_reindex_temp "
+    });
 
     // D.12 Fix empty-named tags.
     //      Tags were allowed to have empty names due to a UI bug.  Fix them
     //      replacing their title with "(notitle)".
-    let fixEmptyNamedTags = DBConn.createAsyncStatement(
+    let fixEmptyNamedTags = {
+      query:
       `UPDATE moz_bookmarks SET title = :empty_title
        WHERE length(title) = 0 AND type = :folder_type
-         AND parent = :tags_folder`
-    );
+         AND parent = :tags_folder`,
+      params: {}
+    };
     fixEmptyNamedTags.params["empty_title"] = "(notitle)";
     fixEmptyNamedTags.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER;
     fixEmptyNamedTags.params["tags_folder"] = PlacesUtils.tagsFolderId;
     cleanupStatements.push(fixEmptyNamedTags);
 
     // MOZ_ICONS
     // E.1 remove orphan icon entries.
-    let deleteOrphanIconPages = DBConn.createAsyncStatement(
+    let deleteOrphanIconPages = {
+      query:
       `DELETE FROM moz_pages_w_icons WHERE page_url_hash NOT IN (
          SELECT url_hash FROM moz_places
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteOrphanIconPages);
 
-    let deleteOrphanIcons = DBConn.createAsyncStatement(
+    let deleteOrphanIcons = {
+      query:
       `DELETE FROM moz_icons WHERE root = 0 AND id NOT IN (
          SELECT icon_id FROM moz_icons_to_pages
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteOrphanIcons);
 
     // MOZ_HISTORYVISITS
     // F.1 remove orphan visits
-    let deleteOrphanVisits = DBConn.createAsyncStatement(
+    let deleteOrphanVisits = {
+      query:
       `DELETE FROM moz_historyvisits WHERE id IN (
          SELECT id FROM moz_historyvisits v
          WHERE NOT EXISTS
            (SELECT id FROM moz_places WHERE id = v.place_id LIMIT 1)
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteOrphanVisits);
 
     // MOZ_INPUTHISTORY
     // G.1 remove orphan input history
-    let deleteOrphanInputHistory = DBConn.createAsyncStatement(
+    let deleteOrphanInputHistory = {
+      query:
       `DELETE FROM moz_inputhistory WHERE place_id IN (
          SELECT place_id FROM moz_inputhistory i
          WHERE NOT EXISTS
            (SELECT id FROM moz_places WHERE id = i.place_id LIMIT 1)
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteOrphanInputHistory);
 
     // MOZ_ITEMS_ANNOS
     // H.1 remove item annos with an invalid attribute
-    let deleteInvalidAttributeItemsAnnos = DBConn.createAsyncStatement(
+    let deleteInvalidAttributeItemsAnnos = {
+      query:
       `DELETE FROM moz_items_annos WHERE id IN (
          SELECT id FROM moz_items_annos t
          WHERE NOT EXISTS
            (SELECT id FROM moz_anno_attributes
              WHERE id = t.anno_attribute_id LIMIT 1)
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteInvalidAttributeItemsAnnos);
 
     // H.2 remove orphan item annos
-    let deleteOrphanItemsAnnos = DBConn.createAsyncStatement(
+    let deleteOrphanItemsAnnos = {
+      query:
       `DELETE FROM moz_items_annos WHERE id IN (
          SELECT id FROM moz_items_annos t
          WHERE NOT EXISTS
            (SELECT id FROM moz_bookmarks WHERE id = t.item_id LIMIT 1)
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteOrphanItemsAnnos);
 
     // MOZ_KEYWORDS
     // I.1 remove unused keywords
-    let deleteUnusedKeywords = DBConn.createAsyncStatement(
+    let deleteUnusedKeywords = {
+      query:
       `DELETE FROM moz_keywords WHERE id IN (
          SELECT id FROM moz_keywords k
          WHERE NOT EXISTS
            (SELECT 1 FROM moz_places h WHERE k.place_id = h.id)
-       )`);
+       )`
+    };
     cleanupStatements.push(deleteUnusedKeywords);
 
     // MOZ_PLACES
     // L.2 recalculate visit_count and last_visit_date
-    let fixVisitStats = DBConn.createAsyncStatement(
+    let fixVisitStats = {
+      query:
       `UPDATE moz_places
        SET visit_count = (SELECT count(*) FROM moz_historyvisits
                           WHERE place_id = moz_places.id AND visit_type NOT IN (0,4,7,8,9)),
            last_visit_date = (SELECT MAX(visit_date) FROM moz_historyvisits
                               WHERE place_id = moz_places.id)
        WHERE id IN (
          SELECT h.id FROM moz_places h
          WHERE visit_count <> (SELECT count(*) FROM moz_historyvisits v
                                WHERE v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9))
             OR last_visit_date <> (SELECT MAX(visit_date) FROM moz_historyvisits v
                                    WHERE v.place_id = h.id)
-       )`);
+       )`
+    };
     cleanupStatements.push(fixVisitStats);
 
     // L.3 recalculate hidden for redirects.
-    let fixRedirectsHidden = DBConn.createAsyncStatement(
+    let fixRedirectsHidden = {
+      query:
       `UPDATE moz_places
        SET hidden = 1
        WHERE id IN (
          SELECT h.id FROM moz_places h
          JOIN moz_historyvisits src ON src.place_id = h.id
          JOIN moz_historyvisits dst ON dst.from_visit = src.id AND dst.visit_type IN (5,6)
          LEFT JOIN moz_bookmarks on fk = h.id AND fk ISNULL
          GROUP BY src.place_id HAVING count(*) = visit_count
-       )`);
+       )`
+    };
     cleanupStatements.push(fixRedirectsHidden);
 
     // L.4 recalculate foreign_count.
-    let fixForeignCount = DBConn.createAsyncStatement(
+    let fixForeignCount = {
+      query:
       `UPDATE moz_places SET foreign_count =
          (SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id ) +
-         (SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id )`);
+         (SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id )`
+    };
     cleanupStatements.push(fixForeignCount);
 
     // L.5 recalculate missing hashes.
-    let fixMissingHashes = DBConn.createAsyncStatement(
-      `UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0`);
+    let fixMissingHashes = {
+      query: `UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0`
+    };
     cleanupStatements.push(fixMissingHashes);
 
     // MAINTENANCE STATEMENTS SHOULD GO ABOVE THIS POINT!
 
     return cleanupStatements;
   },
 
   /**
    * Tries to vacuum the database.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
+   * @return {Promise} resolves when database is vacuumed.
+   * @resolves to an array of logs for this task.
+   * @rejects if we are unable to vacuum database.
    */
-  vacuum: function PDBU_vacuum(aTasks) {
-    let tasks = new Tasks(aTasks);
-    tasks.log("> Vacuum");
-
+  async vacuum() {
+    let logs = [];
     let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
     DBFile.append("places.sqlite");
-    tasks.log("Initial database size is " +
-              parseInt(DBFile.fileSize / 1024) + " KiB");
-
-    let stmt = DBConn.createAsyncStatement("VACUUM");
-    stmt.executeAsync({
-      handleError: PlacesDBUtils._handleError,
-      handleResult() {},
-
-      handleCompletion(aReason) {
-        if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
-          tasks.log("+ The database has been vacuumed");
-          let vacuumedDBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
-          vacuumedDBFile.append("places.sqlite");
-          tasks.log("Final database size is " +
-                    parseInt(vacuumedDBFile.fileSize / 1024) + " KiB");
-        } else {
-          tasks.log("- Unable to vacuum database");
-          tasks.clear();
-        }
-
-        PlacesDBUtils._executeTasks(tasks);
-      }
-    });
-    stmt.finalize();
+    logs.push("Initial database size is " +
+                parseInt(DBFile.fileSize / 1024) + " KiB");
+    return PlacesUtils.withConnectionWrapper(
+      "PlacesDBUtils: vacuum",
+      async (db) => {
+        await db.execute("VACUUM");
+      }).then(() => {
+        logs.push("The database has been vacuumed");
+        let vacuumedDBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+        vacuumedDBFile.append("places.sqlite");
+        logs.push("Final database size is " +
+                   parseInt(vacuumedDBFile.fileSize / 1024) + " KiB");
+        return logs;
+      }).catch(() => {
+        PlacesDBUtils.clearPendingTasks();
+        throw new Error("Unable to vacuum database");
+      });
   },
 
   /**
    * Forces a full expiration on the database.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
+   * @return {Promise} resolves when the database in cleaned up.
+   * @resolves to an array of logs for this task.
    */
-  expire: function PDBU_expire(aTasks) {
-    let tasks = new Tasks(aTasks);
-    tasks.log("> Orphans expiration");
+  async expire() {
+    let logs = [];
+
+    let expiration = Cc["@mozilla.org/places/expiration;1"]
+                       .getService(Ci.nsIObserver);
 
-    let expiration = Cc["@mozilla.org/places/expiration;1"].
-                     getService(Ci.nsIObserver);
-
-    Services.obs.addObserver(function(aSubject, aTopic, aData) {
-      Services.obs.removeObserver(arguments.callee, aTopic);
-      tasks.log("+ Database cleaned up");
-      PlacesDBUtils._executeTasks(tasks);
-    }, PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+    let returnPromise = new Promise(res => {
+      let observer = (subject, topic, data) => {
+        Services.obs.removeObserver(observer, topic);
+        logs.push("Database cleaned up");
+        res(logs);
+      };
+      Services.obs.addObserver(observer, PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+    });
 
     // Force an orphans expiration step.
     expiration.observe(null, "places-debug-start-expiration", 0);
+    return returnPromise;
   },
 
   /**
    * Collects statistical data on the database.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
+   * @return {Promise} resolves when statistics are collected.
+   * @resolves to an array of logs for this task.
+   * @rejects if we are unable to collect stats for some reason.
    */
-  stats: function PDBU_stats(aTasks) {
-    let tasks = new Tasks(aTasks);
-    tasks.log("> Statistics");
-
+  async stats() {
+    let logs = [];
     let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
     DBFile.append("places.sqlite");
-    tasks.log("Database size is " + parseInt(DBFile.fileSize / 1024) + " KiB");
+    logs.push("Database size is " + parseInt(DBFile.fileSize / 1024) + " KiB");
 
-    [ "user_version"
-    , "page_size"
-    , "cache_size"
-    , "journal_mode"
-    , "synchronous"
-    ].forEach(function(aPragma) {
-      let stmt = DBConn.createStatement("PRAGMA " + aPragma);
-      stmt.executeStep();
-      tasks.log(aPragma + " is " + stmt.getString(0));
-      stmt.finalize();
-    });
+    // Execute each step async.
+    let pragmas = [ "user_version",
+                    "page_size",
+                    "cache_size",
+                    "journal_mode",
+                    "synchronous"
+                  ].map(p => `pragma_${p}`);
+    let pragmaQuery = `SELECT * FROM ${ pragmas.join(", ") }`;
+    await PlacesUtils.withConnectionWrapper(
+      "PlacesDBUtils: pragma for stats",
+      async (db) => {
+        let row = (await db.execute(pragmaQuery))[0];
+        for (let i = 0; i != pragmas.length; i++) {
+          logs.push(`${ pragmas[i] } is ${ row.getResultByIndex(i) }`);
+        }
+      }).catch(() => {
+        logs.push("Could not set pragma for stat collection");
+      });
 
     // Get maximum number of unique URIs.
     try {
       let limitURIs = Services.prefs.getIntPref(
         "places.history.expiration.transient_current_max_pages");
-      tasks.log("History can store a maximum of " + limitURIs + " unique pages");
+      logs.push("History can store a maximum of " + limitURIs + " unique pages");
     } catch (ex) {}
 
-    let stmt = DBConn.createStatement(
-      "SELECT name FROM sqlite_master WHERE type = :type");
-    stmt.params.type = "table";
-    while (stmt.executeStep()) {
-      let tableName = stmt.getString(0);
-      let countStmt = DBConn.createStatement(
-        `SELECT count(*) FROM ${tableName}`);
-      countStmt.executeStep();
-      tasks.log("Table " + tableName + " has " + countStmt.getInt32(0) + " records");
-      countStmt.finalize();
-    }
-    stmt.reset();
+    let query = "SELECT name FROM sqlite_master WHERE type = :type";
+    let params = {};
+    let _getTableCount = async (tableName) => {
+      let db = await PlacesUtils.promiseDBConnection();
+      let rows = await db.execute(`SELECT count(*) FROM ${tableName}`);
+      logs.push(`Table ${tableName} has ${rows[0].getResultByIndex(0)} records`);
+    };
+
+    try {
+      params.type = "table";
+      let db = await PlacesUtils.promiseDBConnection();
+      await db.execute(query, params,
+                       r => _getTableCount(r.getResultByIndex(0)));
 
-    stmt.params.type = "index";
-    while (stmt.executeStep()) {
-      tasks.log("Index " + stmt.getString(0));
-    }
-    stmt.reset();
+      params.type = "index";
+      await db.execute(query, params, r => {
+        logs.push(`Index ${r.getResultByIndex(0)}`);
+      });
 
-    stmt.params.type = "trigger";
-    while (stmt.executeStep()) {
-      tasks.log("Trigger " + stmt.getString(0));
+      params.type = "trigger";
+      await db.execute(query, params, r => {
+        logs.push(`Trigger ${r.getResultByIndex(0)}`);
+      });
+
+    } catch (ex) {
+      throw new Error("Unable to collect stats.");
     }
-    stmt.finalize();
 
-    PlacesDBUtils._executeTasks(tasks);
+    return logs;
   },
 
   /**
    * Collects telemetry data and reports it to Telemetry.
    *
-   * @param [optional] aTasks
-   *        Tasks object to execute.
    */
-  telemetry: function PDBU_telemetry(aTasks) {
-    let tasks = new Tasks(aTasks);
-
+  async telemetry() {
     // This will be populated with one integer property for each probe result,
     // using the histogram name as key.
     let probeValues = {};
 
     // The following array contains an ordered list of entries that are
     // processed to collect telemetry data.  Each entry has these properties:
     //
     //  histogram: Name of the telemetry histogram to update.
@@ -950,151 +999,68 @@ this.PlacesDBUtils = {
       let probe = probes[i];
 
       let promiseDone = new Promise((resolve, reject) => {
         if (!("query" in probe)) {
           resolve([probe]);
           return;
         }
 
-        let stmt = DBConn.createAsyncStatement(probe.query);
-        for (let param in params) {
-          if (probe.query.indexOf(":" + param) > 0) {
-            stmt.params[param] = params[param];
+        let filteredParams = {};
+        for (let p in params) {
+          if (probe.query.includes(`:${p}`)) {
+            filteredParams[p] = params[p];
           }
         }
-
-        try {
-          stmt.executeAsync({
-            handleError: reject,
-            handleResult(aResultSet) {
-              let row = aResultSet.getNextRow();
-              resolve([probe, row.getResultByIndex(0)]);
-            },
-            handleCompletion() {}
-          });
-        } finally {
-          stmt.finalize();
-        }
+        PlacesUtils.promiseDBConnection()
+          .then(db => db.execute(probe.query, filteredParams))
+          .then(rows => resolve([probe, rows[0].getResultByIndex(0)]))
+          .catch(ex => reject(new Error("Unable to get telemetry from database.")));
       });
-
       // Report the result of the probe through Telemetry.
       // The resulting promise cannot reject.
-      promiseDone.then(
-        // On success
-        ([aProbe, aValue]) => {
-          let value = aValue;
-          try {
-            if ("callback" in aProbe) {
-              value = aProbe.callback(value);
-            }
-            probeValues[aProbe.histogram] = value;
-            Services.telemetry.getHistogramById(aProbe.histogram).add(value);
-          } catch (ex) {
-            Components.utils.reportError("Error adding value " + value +
-                                         " to histogram " + aProbe.histogram +
-                                         ": " + ex);
-          }
-        },
-        // On failure
-        this._handleError);
+      promiseDone.then(([aProbe, aValue]) => {
+        let value = aValue;
+        if ("callback" in aProbe) {
+          value = aProbe.callback(value);
+        }
+        probeValues[aProbe.histogram] = value;
+        Services.telemetry.getHistogramById(aProbe.histogram).add(value);
+      }).catch(Cu.reportError);
     }
-
-    PlacesDBUtils._executeTasks(tasks);
   },
 
   /**
-   * Runs a list of tasks, notifying log messages to the callback.
+   * Runs a list of tasks, returning a Map when done.
    *
-   * @param aTasks
+   * @param tasks
    *        Array of tasks to be executed, in form of pointers to methods in
    *        this module.
-   * @param [optional] aCallback
-   *        Callback to be invoked when done.  It will receive an array of
-   *        log messages.
+   * @return a Map[taskName(String) -> Object]. The Object has the following properties:
+   *         - succeeded: boolean
+   *         - logs: an array of strings containing the messages logged by the task
    */
-  runTasks: function PDBU_runTasks(aTasks, aCallback) {
-    let tasks = new Tasks(aTasks);
-    tasks.callback = aCallback;
-    PlacesDBUtils._executeTasks(tasks);
+  async runTasks(tasks) {
+    PlacesDBUtils._clearTaskQueue = false;
+    let tasksMap = new Map();
+    for (let task of tasks) {
+      if (PlacesDBUtils._isShuttingDown) {
+        tasksMap.set(
+          task.name,
+          { succeeded: false, logs: ["Shutting down, will now schedule the task."] });
+        continue;
+      }
+
+      if (PlacesDBUtils._clearTaskQueue) {
+        tasksMap.set(
+          task.name,
+          { succeeded: false, logs: ["The task queue was cleared by an error in another task."] });
+        continue;
+      }
+
+      let result =
+          await task().then(logs => { return { succeeded: true, logs }; })
+                      .catch(err => { return { succeeded: false, logs: [err.message] }; });
+      tasksMap.set(task.name, result);
+    }
+    return tasksMap;
   }
 };
-
-/**
- * LIFO tasks stack.
- *
- * @param [optional] aTasks
- *        Array of tasks or another Tasks object to clone.
- */
-function Tasks(aTasks) {
-  if (aTasks) {
-    if (Array.isArray(aTasks)) {
-      this._list = aTasks.slice(0, aTasks.length);
-    } else if (typeof(aTasks) == "object" &&
-               (Tasks instanceof Tasks || "list" in aTasks)) {
-      // This supports passing in a Tasks-like object, with a "list" property,
-      // for compatibility reasons.
-      this._list = aTasks.list;
-      this._log = aTasks.messages;
-      this.callback = aTasks.callback;
-      this.scope = aTasks.scope;
-      this._telemetryStart = aTasks._telemetryStart;
-    }
-  }
-}
-
-Tasks.prototype = {
-  _list: [],
-  _log: [],
-  callback: null,
-  scope: null,
-  _telemetryStart: 0,
-
-  /**
-   * Adds a task to the top of the list.
-   *
-   * @param aNewElt
-   *        Task to be added.
-   */
-  push: function T_push(aNewElt) {
-    this._list.unshift(aNewElt);
-  },
-
-  /**
-   * Returns and consumes next task.
-   *
-   * @return next task or undefined if no task is left.
-   */
-  pop: function T_pop() {
-    return this._list.shift();
-  },
-
-  /**
-   * Removes all tasks.
-   */
-  clear: function T_clear() {
-    this._list.length = 0;
-  },
-
-  /**
-   * Returns array of tasks ordered from the next to be run to the latest.
-   */
-  get list() {
-    return this._list.slice(0, this._list.length);
-  },
-
-  /**
-   * Adds a message to the log.
-   *
-   * @param aMsg
-   *        String message to be added.
-   */
-  log: function T_log(aMsg) {
-    this._log.push(aMsg);
-  },
-
-  /**
-   * Returns array of log messages ordered from oldest to newest.
-   */
-  get messages() {
-    return this._log.slice(0, this._log.length);
-  },
-}