Bug 1244650 - Failure to clear Forms and Search Data on exit. r=yoric draft
authorMarco Bonardo <mbonardo@mozilla.com>
Thu, 04 Feb 2016 13:51:34 +0100
changeset 331378 5cd01a803fbc1f72dc5174dd0c5a5b5aea473fc2
parent 331273 cf16002d74e4a8ae9a8c5e7ef2de7e9876c61f3f
child 514369 889c587c3fb805c46bfdf9f913c04d2bf64c4829
push id10970
push usermak77@bonardo.net
push dateTue, 16 Feb 2016 22:48:56 +0000
reviewersyoric
bugs1244650
milestone47.0a1
Bug 1244650 - Failure to clear Forms and Search Data on exit. r=yoric The problem is due to sanitization happening too late in the shutdown cycle. The Sanitizer depends on Places shutdown, that recently moved to async shutdown. That change caused shutdown to happen completely at profile-before-change, unfortunately during that phase it's impossible to predict which services are already shutdown. The patch restores the previous Places shutdown procedure, thus clients are notified earlier, during profile-change-teardown. Additional meaningful changes: * Fixes FX_SANITIZE_TOTAL telemetry to properly count total time taken by sanitize. * Makes each cleanup operation isolated from other errors to try cleaning up as most as possible. * In case of multiple sanitization sub steps, each step is isolated by a try/catch, the last seen exception is reported upstream. * Makes FX_SANITIZE_HISTORY actually measure history, not other random stuff. * Removes TOPIC_SIMULATE_PLACES_MUST_CLOSE_1 since we can now just use profile-change-teardown for shutdown phase 1. MozReview-Commit-ID: HroLvbi25IC
browser/base/content/sanitize.js
browser/base/content/test/general/browser_sanitize-timespans.js
browser/components/places/tests/unit/test_clearHistory_shutdown.js
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/Shutdown.cpp
toolkit/components/places/Shutdown.h
toolkit/components/places/moz.build
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/tests/head_common.js
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -131,43 +131,53 @@ Sanitizer.prototype = {
     // Cache the range of times to clear
     let range = null;
     // If we ignore timespan, clear everything,
     // otherwise, pick a range.
     if (!this.ignoreTimespan) {
       range = this.range || Sanitizer.getClearRange();
     }
 
+    // For performance reasons we start all the clear tasks at once, then wait
+    // for their promises later.
+    // Some of the clear() calls may raise exceptions (for example bug 265028),
+    // we catch and store them, but continue to sanitize as much as possible.
+    // Callers should check returned errors and give user feedback
+    // about items that could not be sanitized
+    let refObj = {};
+    TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj);
+
+    let annotateError = (name, ex) => {
+      progress[name] = "failed";
+      seenError = true;
+      console.error("Error sanitizing " + name, ex);
+    };
+
+    // Array of objects in form { name, promise }.
+    // Name is the itemName and promise may be a promise, if the sanitization
+    // is asynchronous, or the function return value, if synchronous.
+    let promises = [];
     for (let itemName of itemsToClear) {
       let item = this.items[itemName];
-      if (!("clear" in item)) {
-        progress[itemName] = "`clear` not in item";
-        continue;
-      }
-      item.range = range;
-      // Some of these clear() may raise exceptions (see bug #265028)
-      // to sanitize as much as possible, we catch and store them,
-      // rather than fail fast.
-      // Callers should check returned errors and give user feedback
-      // about items that could not be sanitized
-      let refObj = {};
       try {
-        TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj);
-        yield item.clear();
-        progress[itemName] = "cleared";
-      } catch(er) {
-        progress[itemName] = "failed";
-        seenError = true;
-        console.error("Error sanitizing " + itemName, er);
-      } finally {
-        TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
+        // Note we need to catch errors here, otherwise Promise.all would stop
+        // at the first rejection.
+        promises.push(item.clear(range)
+                          .then(() => progress[itemName] = "cleared",
+                                ex => annotateError(itemName, ex)));
+      } catch (ex) {
+        annotateError(itemName, ex);
       }
     }
+    yield Promise.all(promises);
 
     // Sanitization is complete.
+    TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
+    // Reset the inProgress preference since we were not killed during
+    // sanitization.
     Preferences.reset(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
     progress = {};
     if (seenError) {
       throw new Error("Error sanitizing");
     }
   }),
 
   // Time span only makes sense in certain cases.  Consumers who want
@@ -175,95 +185,121 @@ Sanitizer.prototype = {
   // and can optionally specify a specific range.  If timespan is not ignored,
   // and range is not set, sanitize() will use the value of the timespan
   // pref to determine a range
   ignoreTimespan : true,
   range : null,
 
   items: {
     cache: {
-      clear: function ()
-      {
+      clear: Task.async(function* (range) {
+        let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
 
-        var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"].
-                    getService(Ci.nsICacheStorageService);
         try {
           // Cache doesn't consult timespan, nor does it have the
           // facility for timespan-based eviction.  Wipe it.
+          let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+                        .getService(Ci.nsICacheStorageService);
           cache.clear();
-        } catch(er) {}
+        } catch (ex) {
+          seenException = ex;
+        }
 
-        var imageCache = Cc["@mozilla.org/image/tools;1"].
-                         getService(Ci.imgITools).getImgCacheForDocument(null);
         try {
+          let imageCache = Cc["@mozilla.org/image/tools;1"]
+                             .getService(Ci.imgITools)
+                             .getImgCacheForDocument(null);
           imageCache.clearCache(false); // true=chrome, false=content
-        } catch(er) {}
+        } catch (ex) {
+          seenException = ex;
+        }
 
         TelemetryStopwatch.finish("FX_SANITIZE_CACHE", refObj);
-      }
+        if (seenException) {
+          throw seenException;
+        }
+      })
     },
 
     cookies: {
-      clear: Task.async(function* ()
-      {
+      clear: Task.async(function* (range) {
+        let seenException;
         let yieldCounter = 0;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_COOKIES", refObj);
-        TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj);
 
-        var cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"]
-                                  .getService(Ci.nsICookieManager);
-        if (this.range) {
-          // Iterate through the cookies and delete any created after our cutoff.
-          var cookiesEnum = cookieMgr.enumerator;
-          while (cookiesEnum.hasMoreElements()) {
-            var cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
+        // Clear cookies.
+        TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj);
+        try {
+          let cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"]
+                                    .getService(Ci.nsICookieManager);
+          if (range) {
+            // Iterate through the cookies and delete any created after our cutoff.
+            let cookiesEnum = cookieMgr.enumerator;
+            while (cookiesEnum.hasMoreElements()) {
+              let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
 
-            if (cookie.creationTime > this.range[0]) {
-              // This cookie was created after our cutoff, clear it
-              cookieMgr.remove(cookie.host, cookie.name, cookie.path, false);
+              if (cookie.creationTime > range[0]) {
+                // This cookie was created after our cutoff, clear it
+                cookieMgr.remove(cookie.host, cookie.name, cookie.path, false);
 
-              if (++yieldCounter % YIELD_PERIOD == 0) {
-                yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
+                if (++yieldCounter % YIELD_PERIOD == 0) {
+                  yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
+                }
               }
             }
           }
+          else {
+            // Remove everything
+            cookieMgr.removeAll();
+            yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
+          }
+        } catch (ex) {
+          seenException = ex;
+        } finally {
+          TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj);
         }
-        else {
-          // Remove everything
-          cookieMgr.removeAll();
-          yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
-        }
-        TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj);
 
         // Clear deviceIds. Done asynchronously (returns before complete).
-        let mediaMgr = Components.classes["@mozilla.org/mediaManagerService;1"]
-                                 .getService(Ci.nsIMediaManagerService);
-        mediaMgr.sanitizeDeviceIds(this.range && this.range[0]);
+        try {
+          let mediaMgr = Components.classes["@mozilla.org/mediaManagerService;1"]
+                                   .getService(Ci.nsIMediaManagerService);
+          mediaMgr.sanitizeDeviceIds(range && range[0]);
+        } catch (ex) {
+          seenException = ex;
+        }
 
         // Clear plugin data.
         TelemetryStopwatch.start("FX_SANITIZE_PLUGINS", refObj);
-        yield this.promiseClearPluginCookies();
-        TelemetryStopwatch.finish("FX_SANITIZE_PLUGINS", refObj);
+        try {
+          yield this.promiseClearPluginCookies(range);
+        } catch (ex) {
+          seenException = ex;
+        } finally {
+          TelemetryStopwatch.finish("FX_SANITIZE_PLUGINS", refObj);
+        }
+
         TelemetryStopwatch.finish("FX_SANITIZE_COOKIES", refObj);
+        if (seenException) {
+          throw seenException;
+        }
       }),
 
-      promiseClearPluginCookies: Task.async(function*() {
+      promiseClearPluginCookies: Task.async(function* (range) {
         const phInterface = Ci.nsIPluginHost;
         const FLAG_CLEAR_ALL = phInterface.FLAG_CLEAR_ALL;
         let ph = Cc["@mozilla.org/plugin/host;1"].getService(phInterface);
 
         // Determine age range in seconds. (-1 means clear all.) We don't know
-        // that this.range[1] is actually now, so we compute age range based
-        // on the lower bound. If this.range results in a negative age, do
-        // nothing.
-        let age = this.range ? (Date.now() / 1000 - this.range[0] / 1000000) : -1;
-        if (!this.range || age >= 0) {
+        // that range[1] is actually now, so we compute age range based
+        // on the lower bound. If range results in a negative age, do nothing.
+        let age = range ? (Date.now() / 1000 - range[0] / 1000000) : -1;
+        if (!range || age >= 0) {
           let tags = ph.getPluginTags();
           for (let tag of tags) {
             try {
               let rv = yield new Promise(resolve =>
                 ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve)
               );
               // If the plugin doesn't support clearing by age, clear everything.
               if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
@@ -275,208 +311,248 @@ Sanitizer.prototype = {
               // Ignore errors from plug-ins
             }
           }
         }
       })
     },
 
     offlineApps: {
-      clear: function ()
-      {
+      clear: Task.async(function* (range) {
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS", refObj);
-        Components.utils.import("resource:///modules/offlineAppCache.jsm");
-        OfflineAppCacheHelper.clear();
-        TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS", refObj);
-      }
+        try {
+          Components.utils.import("resource:///modules/offlineAppCache.jsm");
+          // This doesn't wait for the cleanup to be complete.
+          OfflineAppCacheHelper.clear();
+        } finally {
+          TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS", refObj);
+        }
+      })
     },
 
     history: {
-      clear: Task.async(function* ()
-      {
+      clear: Task.async(function* (range) {
+        let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj);
         try {
-          if (this.range) {
+          if (range) {
             yield PlacesUtils.history.removeVisitsByFilter({
-              beginDate: new Date(this.range[0] / 1000),
-              endDate: new Date(this.range[1] / 1000)
+              beginDate: new Date(range[0] / 1000),
+              endDate: new Date(range[1] / 1000)
             });
           } else {
             // Remove everything.
             yield PlacesUtils.history.clear();
           }
-
-          try {
-            let clearStartingTime = this.range ? String(this.range[0]) : "";
-            Services.obs.notifyObservers(null, "browser:purge-session-history", clearStartingTime);
-          } catch (e) { }
-
-          try {
-            let predictor = Components.classes["@mozilla.org/network/predictor;1"]
-                                      .getService(Components.interfaces.nsINetworkPredictor);
-            predictor.reset();
-          } catch (e) {
-            console.error("Error while resetting the predictor", e);
-          }
+        } catch (ex) {
+          seenException = ex;
         } finally {
           TelemetryStopwatch.finish("FX_SANITIZE_HISTORY", refObj);
         }
+
+        try {
+          let clearStartingTime = range ? String(range[0]) : "";
+          Services.obs.notifyObservers(null, "browser:purge-session-history", clearStartingTime);
+        } catch (ex) {
+          seenException = ex;
+        }
+
+        try {
+          let predictor = Components.classes["@mozilla.org/network/predictor;1"]
+                                    .getService(Components.interfaces.nsINetworkPredictor);
+          predictor.reset();
+        } catch (ex) {
+          seenException = ex;
+        }
+
+        if (seenException) {
+          throw seenException;
+        }
       })
     },
 
     formdata: {
-      clear: function ()
-      {
+      clear: Task.async(function* (range) {
+        let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_FORMDATA", refObj);
-
-        // Clear undo history of all searchBars
-        var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
-                                      .getService(Components.interfaces.nsIWindowMediator);
-        var windows = windowManager.getEnumerator("navigator:browser");
-        while (windows.hasMoreElements()) {
-          let currentWindow = windows.getNext();
-          let currentDocument = currentWindow.document;
-          let searchBar = currentDocument.getElementById("searchbar");
-          if (searchBar)
-            searchBar.textbox.reset();
-          let tabBrowser = currentWindow.gBrowser;
-          if (!tabBrowser) {
-            // No tab browser? This means that it's too early during startup (typically,
-            // Session Restore hasn't completed yet). Since we don't have find
-            // bars at that stage and since Session Restore will not restore
-            // find bars further down during startup, we have nothing to clear.
-            continue;
+        try {
+          // Clear undo history of all searchBars
+          let windows = Services.wm.getEnumerator("navigator:browser");
+          while (windows.hasMoreElements()) {
+            let currentWindow = windows.getNext();
+            let currentDocument = currentWindow.document;
+            let searchBar = currentDocument.getElementById("searchbar");
+            if (searchBar)
+              searchBar.textbox.reset();
+            let tabBrowser = currentWindow.gBrowser;
+            if (!tabBrowser) {
+              // No tab browser? This means that it's too early during startup (typically,
+              // Session Restore hasn't completed yet). Since we don't have find
+              // bars at that stage and since Session Restore will not restore
+              // find bars further down during startup, we have nothing to clear.
+              continue;
+            }
+            for (let tab of tabBrowser.tabs) {
+              if (tabBrowser.isFindBarInitialized(tab))
+                tabBrowser.getFindBar(tab).clear();
+            }
+            // Clear any saved find value
+            tabBrowser._lastFindValue = "";
           }
-          for (let tab of tabBrowser.tabs) {
-            if (tabBrowser.isFindBarInitialized(tab))
-              tabBrowser.getFindBar(tab).clear();
-          }
-          // Clear any saved find value
-          tabBrowser._lastFindValue = "";
+        } catch (ex) {
+          seenException = ex;
         }
 
-        let change = { op: "remove" };
-        if (this.range) {
-          [ change.firstUsedStart, change.firstUsedEnd ] = this.range;
+        try {
+          let change = { op: "remove" };
+          if (range) {
+            [ change.firstUsedStart, change.firstUsedEnd ] = range;
+          }
+          yield new Promise(resolve => {
+            FormHistory.update(change, {
+              handleError(e) {
+                seenException = new Error("Error " + e.result + ": " + e.message);
+              },
+              handleCompletion() {
+                resolve();
+              }
+            });
+          });
+        } catch (ex) {
+          seenException = ex;
         }
-        FormHistory.update(change);
 
         TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA", refObj);
-      }
+        if (seenException) {
+          throw seenException;
+        }
+      })
     },
 
     downloads: {
-      clear: function ()
-      {
+      clear: Task.async(function* (range) {
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
-        Task.spawn(function*() {
+        try {
           let filterByTime = null;
-          if (this.range) {
+          if (range) {
             // Convert microseconds back to milliseconds for date comparisons.
-            let rangeBeginMs = this.range[0] / 1000;
-            let rangeEndMs = this.range[1] / 1000;
+            let rangeBeginMs = range[0] / 1000;
+            let rangeEndMs = range[1] / 1000;
             filterByTime = download => download.startTime >= rangeBeginMs &&
                                        download.startTime <= rangeEndMs;
           }
 
           // Clear all completed/cancelled downloads
           let list = yield Downloads.getList(Downloads.ALL);
           list.removeFinished(filterByTime);
-          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
-        }.bind(this)).then(null, error => {
+        } finally {
           TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
-          Components.utils.reportError(error);
-        });
-      }
+        }
+      })
     },
 
     sessions: {
-      clear: function ()
-      {
+      clear: Task.async(function* (range) {
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_SESSIONS", refObj);
 
-        // clear all auth tokens
-        var sdr = Components.classes["@mozilla.org/security/sdr;1"]
-                            .getService(Components.interfaces.nsISecretDecoderRing);
-        sdr.logoutAndTeardown();
+        try {
+          // clear all auth tokens
+          let sdr = Components.classes["@mozilla.org/security/sdr;1"]
+                              .getService(Components.interfaces.nsISecretDecoderRing);
+          sdr.logoutAndTeardown();
 
-        // clear FTP and plain HTTP auth sessions
-        var os = Components.classes["@mozilla.org/observer-service;1"]
-                           .getService(Components.interfaces.nsIObserverService);
-        os.notifyObservers(null, "net:clear-active-logins", null);
-
-        TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS", refObj);
-      }
+          // clear FTP and plain HTTP auth sessions
+          Services.obs.notifyObservers(null, "net:clear-active-logins", null);
+        } finally {
+          TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS", refObj);
+        }
+      })
     },
 
     siteSettings: {
-      clear: function ()
-      {
+      clear: Task.async(function* (range) {
+        let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS", refObj);
 
-        // Clear site-specific permissions like "Allow this site to open popups"
-        // we ignore the "end" range and hope it is now() - none of the
-        // interfaces used here support a true range anyway.
-        let startDateMS = this.range == null ? null : this.range[0] / 1000;
-        var pm = Components.classes["@mozilla.org/permissionmanager;1"]
-                           .getService(Components.interfaces.nsIPermissionManager);
-        if (startDateMS == null) {
-          pm.removeAll();
-        } else {
-          pm.removeAllSince(startDateMS);
+        let startDateMS = range ? range[0] / 1000 : null;
+
+        try {
+          // Clear site-specific permissions like "Allow this site to open popups"
+          // we ignore the "end" range and hope it is now() - none of the
+          // interfaces used here support a true range anyway.
+          if (startDateMS == null) {
+            Services.perms.removeAll();
+          } else {
+            Services.perms.removeAllSince(startDateMS);
+          }
+        } catch (ex) {
+          seenException = ex;
         }
 
-        // Clear site-specific settings like page-zoom level
-        var cps = Components.classes["@mozilla.org/content-pref/service;1"]
-                            .getService(Components.interfaces.nsIContentPrefService2);
-        if (startDateMS == null) {
-          cps.removeAllDomains(null);
-        } else {
-          cps.removeAllDomainsSince(startDateMS, null);
+        try {
+          // Clear site-specific settings like page-zoom level
+          let cps = Components.classes["@mozilla.org/content-pref/service;1"]
+                              .getService(Components.interfaces.nsIContentPrefService2);
+          if (startDateMS == null) {
+            cps.removeAllDomains(null);
+          } else {
+            cps.removeAllDomainsSince(startDateMS, null);
+          }
+        } catch (ex) {
+          seenException = ex;
         }
 
-        // Clear "Never remember passwords for this site", which is not handled by
-        // the permission manager
-        // (Note the login manager doesn't support date ranges yet, and bug
-        //  1058438 is calling for loginSaving stuff to end up in the
-        // permission manager)
-        var pwmgr = Components.classes["@mozilla.org/login-manager;1"]
-                              .getService(Components.interfaces.nsILoginManager);
-        var hosts = pwmgr.getAllDisabledHosts();
-        for (var host of hosts) {
-          pwmgr.setLoginSavingEnabled(host, true);
+        try {
+          // Clear "Never remember passwords for this site", which is not handled by
+          // the permission manager
+          // (Note the login manager doesn't support date ranges yet, and bug
+          //  1058438 is calling for loginSaving stuff to end up in the
+          // permission manager)
+          let hosts = Services.logins.getAllDisabledHosts();
+          for (let host of hosts) {
+            Services.logins.setLoginSavingEnabled(host, true);
+          }
+        } catch (ex) {
+          seenException = ex;
         }
 
-        // Clear site security settings - no support for ranges in this
-        // interface either, so we clearAll().
-        var sss = Cc["@mozilla.org/ssservice;1"]
-                    .getService(Ci.nsISiteSecurityService);
-        sss.clearAll();
+        try {
+          // Clear site security settings - no support for ranges in this
+          // interface either, so we clearAll().
+          let sss = Cc["@mozilla.org/ssservice;1"]
+                      .getService(Ci.nsISiteSecurityService);
+          sss.clearAll();
+        } catch (ex) {
+          seenException = ex;
+        }
 
         // Clear all push notification subscriptions
         try {
-          var push = Cc["@mozilla.org/push/Service;1"]
+          let push = Cc["@mozilla.org/push/Service;1"]
                        .getService(Ci.nsIPushService);
           push.clearForDomain("*", status => {
             if (!Components.isSuccessCode(status)) {
               dump("Error clearing Web Push data: " + status + "\n");
             }
           });
         } catch (e) {
           dump("Web Push may not be available.\n");
         }
 
         TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj);
-      }
+        if (seenException) {
+          throw seenException;
+        }
+      })
     },
 
     openWindows: {
       privateStateForNewWindow: "non-private",
       _canCloseWindow: function(aWindow) {
         if (aWindow.CanCloseWindow()) {
           // We already showed PermitUnload for the window, so let's
           // make sure we don't do it again when we actually close the
@@ -485,17 +561,17 @@ Sanitizer.prototype = {
           return true;
         }
       },
       _resetAllWindowClosures: function(aWindowList) {
         for (let win of aWindowList) {
           win.skipNextCanClose = false;
         }
       },
-      clear: Task.async(function*() {
+      clear: Task.async(function* () {
         // NB: this closes all *browser* windows, not other windows like the library, about window,
         // browser console, etc.
 
         // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload
         // dialogs
         let existingWindow = Services.appShell.hiddenDOMWindow;
         let startDate = existingWindow.performance.now();
 
@@ -665,26 +741,24 @@ Sanitizer.__defineGetter__("prefs", func
     : Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"]
                          .getService(Components.interfaces.nsIPrefService)
                          .getBranch(Sanitizer.PREF_DOMAIN);
 });
 
 // Shows sanitization UI
 Sanitizer.showUI = function(aParentWindow)
 {
-  var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
-                     .getService(Components.interfaces.nsIWindowWatcher);
   let win = AppConstants.platform == "macosx" ?
     null: // make this an app-modal window on Mac
     aParentWindow;
-  ww.openWindow(win,
-                "chrome://browser/content/sanitize.xul",
-                "Sanitize",
-                "chrome,titlebar,dialog,centerscreen,modal",
-                null);
+  Services.ww.openWindow(win,
+                         "chrome://browser/content/sanitize.xul",
+                         "Sanitize",
+                         "chrome,titlebar,dialog,centerscreen,modal",
+                         null);
 };
 
 /**
  * Deletes privacy sensitive data in a batch, optionally showing the
  * sanitize UI, according to user preferences
  */
 Sanitizer.sanitize = function(aParentWindow)
 {
--- a/browser/base/content/test/general/browser_sanitize-timespans.js
+++ b/browser/base/content/test/general/browser_sanitize-timespans.js
@@ -37,26 +37,22 @@ function promiseDownloadRemoved(list) {
     }
   };
 
   list.addView(view);
 
   return deferred.promise;
 }
 
-function test() {
-  waitForExplicitFinish();
-
-  Task.spawn(function() {
-    yield setupDownloads();
-    yield setupFormHistory();
-    yield setupHistory();
-    yield onHistoryReady();
-  }).then(null, ex => ok(false, ex)).then(finish);
-}
+add_task(function* test() {
+  yield setupDownloads();
+  yield setupFormHistory();
+  yield setupHistory();
+  yield onHistoryReady();
+});
 
 function countEntries(name, message, check) {
   let deferred = Promise.defer();
 
   var obj = {};
   if (name !== null)
     obj.fieldname = name;
 
@@ -72,17 +68,17 @@ function countEntries(name, message, che
                                deferred.resolve();
                              }
                            },
                          });
 
   return deferred.promise;
 }
 
-function onHistoryReady() {
+function* onHistoryReady() {
   var hoursSinceMidnight = new Date().getHours();
   var minutesSinceMidnight = hoursSinceMidnight * 60 + new Date().getMinutes();
 
   // Should test cookies here, but nsICookieManager/nsICookieService
   // doesn't let us fake creation times.  bug 463127
   
   let s = new Sanitizer();
   s.ignoreTimespan = false;
@@ -95,23 +91,24 @@ function onHistoryReady() {
   itemPrefs.setBoolPref("formdata", true);
   itemPrefs.setBoolPref("offlineApps", false);
   itemPrefs.setBoolPref("passwords", false);
   itemPrefs.setBoolPref("sessions", false);
   itemPrefs.setBoolPref("siteSettings", false);
 
   let publicList = yield Downloads.getList(Downloads.PUBLIC);
   let downloadPromise = promiseDownloadRemoved(publicList);
+  let formHistoryPromise = promiseFormHistoryRemoved();
 
   // Clear 10 minutes ago
   s.range = [now_uSec - 10*60*1000000, now_uSec];
-  s.sanitize();
+  yield s.sanitize();
   s.range = null;
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   ok(!(yield promiseIsURIVisited(makeURI("http://10minutes.com"))),
      "Pretend visit to 10minutes.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://1hour.com"))),
      "Pretend visit to 1hour.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
      "Pretend visit to 1hour10minutes.com should should still exist");
@@ -152,22 +149,23 @@ function onHistoryReady() {
   ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
 
   if (minutesSinceMidnight > 10)
     ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
 
   downloadPromise = promiseDownloadRemoved(publicList);
+  formHistoryPromise = promiseFormHistoryRemoved();
 
   // Clear 1 hour
   Sanitizer.prefs.setIntPref("timeSpan", 1);
-  s.sanitize();
+  yield s.sanitize();
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   ok(!(yield promiseIsURIVisited(makeURI("http://1hour.com"))),
      "Pretend visit to 1hour.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
      "Pretend visit to 1hour10minutes.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))),
      "Pretend visit to 2hour.com should should still exist");
@@ -201,23 +199,24 @@ function onHistoryReady() {
   ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
 
   if (hoursSinceMidnight > 1)
     ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
   
   downloadPromise = promiseDownloadRemoved(publicList);
+  formHistoryPromise = promiseFormHistoryRemoved();
 
   // Clear 1 hour 10 minutes
   s.range = [now_uSec - 70*60*1000000, now_uSec];
-  s.sanitize();
+  yield s.sanitize();
   s.range = null;
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   ok(!(yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
      "Pretend visit to 1hour10minutes.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))),
      "Pretend visit to 2hour.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
      "Pretend visit to 2hour10minutes.com should should still exist");
@@ -246,22 +245,23 @@ function onHistoryReady() {
   ok((yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should still be present");
   ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
   if (minutesSinceMidnight > 70)
     ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
 
   downloadPromise = promiseDownloadRemoved(publicList);
+  formHistoryPromise = promiseFormHistoryRemoved();
 
   // Clear 2 hours
   Sanitizer.prefs.setIntPref("timeSpan", 2);
-  s.sanitize();
+  yield s.sanitize();
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   ok(!(yield promiseIsURIVisited(makeURI("http://2hour.com"))),
      "Pretend visit to 2hour.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
      "Pretend visit to 2hour10minutes.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
      "Pretend visit to 4hour.com should should still exist");
@@ -284,25 +284,26 @@ function onHistoryReady() {
 
   ok(!(yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should now be deleted");
   ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
   ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
   if (hoursSinceMidnight > 2)
     ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
-  
+
   downloadPromise = promiseDownloadRemoved(publicList);
+  formHistoryPromise = promiseFormHistoryRemoved();
 
   // Clear 2 hours 10 minutes
   s.range = [now_uSec - 130*60*1000000, now_uSec];
-  s.sanitize();
+  yield s.sanitize();
   s.range = null;
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   ok(!(yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
      "Pretend visit to 2hour10minutes.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
      "Pretend visit to 4hour.com should should still exist");
   ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
      "Pretend visit to 4hour10minutes.com should should still exist");
@@ -323,22 +324,23 @@ function onHistoryReady() {
   ok(!(yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute old download should now be deleted");
   ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
   ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
   ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
   if (minutesSinceMidnight > 130)
     ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
 
   downloadPromise = promiseDownloadRemoved(publicList);
+  formHistoryPromise = promiseFormHistoryRemoved();
 
   // Clear 4 hours
   Sanitizer.prefs.setIntPref("timeSpan", 3);
-  s.sanitize();
+  yield s.sanitize();
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   ok(!(yield promiseIsURIVisited(makeURI("http://4hour.com"))),
      "Pretend visit to 4hour.com should now be deleted");
   ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
      "Pretend visit to 4hour10minutes.com should should still exist");
   if (hoursSinceMidnight > 4) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
@@ -355,23 +357,24 @@ function onHistoryReady() {
 
   ok(!(yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should now be deleted");
   ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
   ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
   if (hoursSinceMidnight > 4)
     ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
 
   downloadPromise = promiseDownloadRemoved(publicList);
+  formHistoryPromise = promiseFormHistoryRemoved();
 
   // Clear 4 hours 10 minutes
   s.range = [now_uSec - 250*60*1000000, now_uSec];
-  s.sanitize();
+  yield s.sanitize();
   s.range = null;
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   ok(!(yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
      "Pretend visit to 4hour10minutes.com should now be deleted");
   if (minutesSinceMidnight > 250) {
     ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
        "Pretend visit to today.com should still exist");
   }
@@ -390,22 +393,23 @@ function onHistoryReady() {
 
   // The 'Today' download might have been already deleted, in which case we
   // should not wait for a download removal notification.
   if (minutesSinceMidnight > 250) {
     downloadPromise = promiseDownloadRemoved(publicList);
   } else {
     downloadPromise = Promise.resolve();
   }
+  formHistoryPromise = promiseFormHistoryRemoved();
 
   // Clear Today
   Sanitizer.prefs.setIntPref("timeSpan", 4);
-  s.sanitize();
+  yield s.sanitize();
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   // Be careful.  If we add our objectss just before midnight, and sanitize
   // runs immediately after, they won't be expired.  This is expected, but
   // we should not test in that case.  We cannot just test for opposite
   // condition because we could cross midnight just one moment after we
   // cache our time, then we would have an even worse random failure.
   var today = isToday(new Date(now_mSec));
@@ -418,22 +422,23 @@ function onHistoryReady() {
   }
 
   ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
      "Pretend visit to before-today.com should still exist");
   yield countEntries("b4today", "b4today form entry should still exist", checkOne);
   ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
 
   downloadPromise = promiseDownloadRemoved(publicList);
+  formHistoryPromise = promiseFormHistoryRemoved();
 
   // Choose everything
   Sanitizer.prefs.setIntPref("timeSpan", 0);
-  s.sanitize();
+  yield s.sanitize();
 
-  yield promiseFormHistoryRemoved();
+  yield formHistoryPromise;
   yield downloadPromise;
 
   ok(!(yield promiseIsURIVisited(makeURI("http://before-today.com"))),
      "Pretend visit to before-today.com should now be deleted");
 
   yield countEntries("b4today", "b4today form entry should be deleted", checkZero);
 
   ok(!(yield downloadExists(publicList, "fakefile-old")), "Year old download should now be deleted");
@@ -467,17 +472,16 @@ function setupHistory() {
   today.setHours(0);
   today.setMinutes(0);
   today.setSeconds(1);
   addPlace(makeURI("http://today.com/"), "Today", today.getTime() * 1000);
 
   let lastYear = new Date();
   lastYear.setFullYear(lastYear.getFullYear() - 1);
   addPlace(makeURI("http://before-today.com/"), "Before Today", lastYear.getTime() * 1000);
-
   PlacesUtils.asyncHistory.updatePlaces(places, {
     handleError: () => ok(false, "Unexpected error in adding visit."),
     handleResult: () => { },
     handleCompletion: () => deferred.resolve()
   });
 
   return deferred.promise;
 }
--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -30,16 +30,18 @@ const UNEXPECTED_NOTIFICATIONS = [
 
 const FTP_URL = "ftp://localhost/clearHistoryOnShutdown/";
 
 // Send the profile-after-change notification to the form history component to ensure
 // that it has been initialized.
 var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
                          getService(Ci.nsIObserver);
 formHistoryStartup.observe(null, "profile-after-change", null);
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+                                  "resource://gre/modules/FormHistory.jsm");
 
 var timeInMicroseconds = Date.now() * 1000;
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function* test_execute() {
@@ -68,20 +70,25 @@ add_task(function* test_execute() {
   for (let aUrl of URIS) {
     yield PlacesTestUtils.addVisits({
       uri: uri(aUrl), visitDate: timeInMicroseconds++,
       transition: PlacesUtils.history.TRANSITION_TYPED
     });
   }
   do_print("Add cache.");
   yield storeCache(FTP_URL, "testData");
+  do_print("Add form history.");
+  yield addFormHistory();
+  Assert.equal((yield getFormHistoryCount()), 1, "Added form history");
 
   do_print("Simulate and wait shutdown.");
   yield shutdownPlaces();
 
+  Assert.equal((yield getFormHistoryCount()), 0, "Form history cleared");
+
   let stmt = DBConn(true).createStatement(
     "SELECT id FROM moz_places WHERE url = :page_url "
   );
 
   try {
     URIS.forEach(function(aUrl) {
       stmt.params.page_url = aUrl;
       do_check_false(stmt.executeStep());
@@ -91,16 +98,40 @@ add_task(function* test_execute() {
     stmt.finalize();
   }
 
   do_print("Check cache");
   // Check cache.
   yield checkCache(FTP_URL);
 });
 
+function addFormHistory() {
+  return new Promise(resolve => {
+    let now = Date.now() * 1000;
+    FormHistory.update({ op: "add",
+                         fieldname: "testfield",
+                         value: "test",
+                         timesUsed: 1,
+                         firstUsed: now,
+                         lastUsed: now
+                       },
+                       { handleCompletion(reason) { resolve(); } });
+  });
+}
+
+function getFormHistoryCount() {
+  return new Promise((resolve, reject) => {
+    let count = -1;
+    FormHistory.count({ fieldname: "testfield" },
+                      { handleResult(result) { count = result; },
+                        handleCompletion(reason) { resolve(count); }
+                      });
+  });
+}
+
 function storeCache(aURL, aContent) {
   let cache = Services.cache2;
   let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
 
   return new Promise(resolve => {
     let storeCacheListener = {
       onCacheEntryCheck: function (entry, appcache) {
         return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -289,279 +289,16 @@ CreateRoot(nsCOMPtr<mozIStorageConnectio
     ++itemPosition;
 
   return NS_OK;
 }
 
 
 } // namespace
 
-/**
- * An AsyncShutdown blocker in charge of shutting down places
- */
-class DatabaseShutdown final:
-    public nsIAsyncShutdownBlocker,
-    public nsIAsyncShutdownCompletionCallback,
-    public mozIStorageCompletionCallback
-{
-public:
-  NS_DECL_THREADSAFE_ISUPPORTS
-  NS_DECL_NSIASYNCSHUTDOWNBLOCKER
-  NS_DECL_NSIASYNCSHUTDOWNCOMPLETIONCALLBACK
-  NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
-
-  explicit DatabaseShutdown(Database* aDatabase);
-
-  already_AddRefed<nsIAsyncShutdownClient> GetClient();
-
-  /**
-   * `true` if we have not started shutdown, i.e.  if
-   * `BlockShutdown()` hasn't been called yet, false otherwise.
-   */
-  static bool IsStarted() {
-    return sIsStarted;
-  }
-
-private:
-  nsCOMPtr<nsIAsyncShutdownBarrier> mBarrier;
-  nsCOMPtr<nsIAsyncShutdownClient> mParentClient;
-
-  // The owning database.
-  // The cycle is broken in method Complete(), once the connection
-  // has been closed by mozStorage.
-  RefPtr<Database> mDatabase;
-
-  // The current state, used both internally and for
-  // forensics/debugging purposes.
-  enum State {
-    NOT_STARTED,
-
-    // Execution of `BlockShutdown` in progress
-    // a. `BlockShutdown` is starting.
-    RECEIVED_BLOCK_SHUTDOWN,
-    // b. `BlockShutdown` is complete, waiting for clients.
-    CALLED_WAIT_CLIENTS,
-
-    // Execution of `Done` in progress
-    // a. `Done` is starting.
-    RECEIVED_DONE,
-    // b. We have notified observers that Places will close connection.
-    NOTIFIED_OBSERVERS_PLACES_WILL_CLOSE_CONNECTION,
-    // c. Execution of `Done` is complete, waiting for mozStorage shutdown.
-    CALLED_STORAGESHUTDOWN,
-
-    // Execution of `Complete` in progress
-    // a. `Complete` is starting.
-    RECEIVED_STORAGESHUTDOWN_COMPLETE,
-    // b. We have notified observers that Places as closed connection.
-    NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED,
-  };
-  State mState;
-
-  // As tests may resurrect a dead `Database`, we use a counter to
-  // give the instances of `DatabaseShutdown` unique names.
-  uint16_t mCounter;
-  static uint16_t sCounter;
-
-  static Atomic<bool> sIsStarted;
-
-  ~DatabaseShutdown() {}
-};
-uint16_t DatabaseShutdown::sCounter = 0;
-Atomic<bool> DatabaseShutdown::sIsStarted(false);
-
-DatabaseShutdown::DatabaseShutdown(Database* aDatabase)
-  : mDatabase(aDatabase)
-  , mState(NOT_STARTED)
-  , mCounter(sCounter++)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown();
-  MOZ_ASSERT(asyncShutdownSvc);
-
-  if (asyncShutdownSvc) {
-    DebugOnly<nsresult> rv = asyncShutdownSvc->MakeBarrier(
-      NS_LITERAL_STRING("Places Database shutdown"),
-      getter_AddRefs(mBarrier)
-    );
-    MOZ_ASSERT(NS_SUCCEEDED(rv));
-  }
-}
-
-already_AddRefed<nsIAsyncShutdownClient>
-DatabaseShutdown::GetClient()
-{
-  nsCOMPtr<nsIAsyncShutdownClient> client;
-  if (mBarrier) {
-    DebugOnly<nsresult> rv = mBarrier->GetClient(getter_AddRefs(client));
-    MOZ_ASSERT(NS_SUCCEEDED(rv));
-  }
-  return client.forget();
-}
-
-// nsIAsyncShutdownBlocker::GetName
-NS_IMETHODIMP
-DatabaseShutdown::GetName(nsAString& aName)
-{
-  if (mCounter > 0) {
-    // During tests, we can end up with the Database singleton being resurrected.
-    // Make sure that each instance of DatabaseShutdown has a unique name.
-    nsPrintfCString name("Places DatabaseShutdown: Blocking profile-before-change (%x)", this);
-    aName = NS_ConvertUTF8toUTF16(name);
-  } else {
-    aName = NS_LITERAL_STRING("Places DatabaseShutdown: Blocking profile-before-change");
-  }
-  return NS_OK;
-}
-
-// nsIAsyncShutdownBlocker::GetState
-NS_IMETHODIMP DatabaseShutdown::GetState(nsIPropertyBag** aState)
-{
-  nsresult rv;
-  nsCOMPtr<nsIWritablePropertyBag2> bag =
-    do_CreateInstance("@mozilla.org/hash-property-bag;1", &rv);
-  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
-
-  // Put `mState` in field `progress`
-  RefPtr<nsVariant> progress = new nsVariant();
-
-  rv = progress->SetAsUint8(mState);
-  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
-
-  rv = bag->SetPropertyAsInterface(NS_LITERAL_STRING("progress"), progress);
-  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
-
-  // Put `mBarrier`'s state in field `barrier`, if possible
-  if (!mBarrier) {
-    return NS_OK;
-  }
-  nsCOMPtr<nsIPropertyBag> barrierState;
-  rv = mBarrier->GetState(getter_AddRefs(barrierState));
-  if (NS_FAILED(rv)) {
-    return NS_OK;
-  }
-
-  RefPtr<nsVariant> barrier = new nsVariant();
-
-  rv = barrier->SetAsInterface(NS_GET_IID(nsIPropertyBag), barrierState);
-  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
-
-  rv = bag->SetPropertyAsInterface(NS_LITERAL_STRING("Barrier"), barrier);
-  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
-
-  return NS_OK;
-}
-
-
-// nsIAsyncShutdownBlocker::BlockShutdown
-//
-// Step 1 in shutdown, called during profile-before-change.
-// As a `nsIAsyncShutdownBarrier`, we now need to wait until all clients
-// of `this` barrier have completed their own shutdown.
-//
-// See `Done()` for step 2.
-NS_IMETHODIMP
-DatabaseShutdown::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
-{
-  mParentClient = aParentClient;
-  mState = RECEIVED_BLOCK_SHUTDOWN;
-  sIsStarted = true;
-
-  if (NS_WARN_IF(!mBarrier)) {
-    return NS_ERROR_NOT_AVAILABLE;
-  }
-
-  // Wait until all clients have removed their blockers, then proceed
-  // with own shutdown.
-  DebugOnly<nsresult> rv = mBarrier->Wait(this);
-  MOZ_ASSERT(NS_SUCCEEDED(rv));
-
-  mState = CALLED_WAIT_CLIENTS;
-  return NS_OK;
-}
-
-// nsIAsyncShutdownCompletionCallback::Done
-//
-// Step 2 in shutdown, called once all clients have removed their blockers.
-// We may now check sanity, inform observers, and close the database handler.
-//
-// See `Complete()` for step 3.
-NS_IMETHODIMP
-DatabaseShutdown::Done()
-{
-  mState = RECEIVED_DONE;
-
-  // Fire internal shutdown notifications.
-  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
-  MOZ_ASSERT(os);
-  if (os) {
-    (void)os->NotifyObservers(nullptr, TOPIC_PLACES_WILL_CLOSE_CONNECTION, nullptr);
-  }
-  mState = NOTIFIED_OBSERVERS_PLACES_WILL_CLOSE_CONNECTION;
-
-  // At this stage, any use of this database is forbidden. Get rid of
-  // `gDatabase`. Note, however, that the database could be
-  // resurrected.  This can happen in particular during tests.
-  MOZ_ASSERT(Database::gDatabase == nullptr || Database::gDatabase == mDatabase);
-  Database::gDatabase = nullptr;
-
-  mDatabase->Shutdown();
-  mState = CALLED_STORAGESHUTDOWN;
-  return NS_OK;
-}
-
-
-// mozIStorageCompletionCallback::Complete
-//
-// Step 3 (and last step) of shutdown
-//
-// Called once the connection has been closed by mozStorage.
-// Inform observers of TOPIC_PLACES_CONNECTION_CLOSED.
-//
-NS_IMETHODIMP
-DatabaseShutdown::Complete(nsresult, nsISupports*)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  mState = RECEIVED_STORAGESHUTDOWN_COMPLETE;
-  mDatabase = nullptr;
-
-  nsresult rv;
-  if (mParentClient) {
-    // mParentClient may be nullptr in tests
-    rv = mParentClient->RemoveBlocker(this);
-    if (NS_WARN_IF(NS_FAILED(rv))) return rv;
-  }
-
-  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
-  MOZ_ASSERT(os);
-  if (os) {
-    rv = os->NotifyObservers(nullptr,
-                             TOPIC_PLACES_CONNECTION_CLOSED,
-                             nullptr);
-    MOZ_ASSERT(NS_SUCCEEDED(rv));
-  }
-  mState = NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED;
-
-  if (NS_WARN_IF(!mBarrier)) {
-    return NS_ERROR_NOT_AVAILABLE;
-  }
-
-  NS_ReleaseOnMainThread(mBarrier.forget());
-  NS_ReleaseOnMainThread(mParentClient.forget());
-
-  return NS_OK;
-}
-
-NS_IMPL_ISUPPORTS(
-  DatabaseShutdown
-, nsIAsyncShutdownBlocker
-, nsIAsyncShutdownCompletionCallback
-, mozIStorageCompletionCallback
-)
-
 ////////////////////////////////////////////////////////////////////////////////
 //// Database
 
 PLACES_FACTORY_SINGLETON_IMPLEMENTATION(Database, gDatabase)
 
 NS_IMPL_ISUPPORTS(Database
 , nsIObserver
 , nsISupportsWeakReference
@@ -569,47 +306,53 @@ NS_IMPL_ISUPPORTS(Database
 
 Database::Database()
   : mMainThreadStatements(mMainConn)
   , mMainThreadAsyncStatements(mMainConn)
   , mAsyncThreadStatements(mMainConn)
   , mDBPageSize(0)
   , mDatabaseStatus(nsINavHistoryService::DATABASE_STATUS_OK)
   , mClosed(false)
-  , mConnectionShutdown(new DatabaseShutdown(this))
+  , mClientsShutdown(new ClientsShutdownBlocker())
+  , mConnectionShutdown(new ConnectionShutdownBlocker(this))
 {
   MOZ_ASSERT(!XRE_IsContentProcess(),
              "Cannot instantiate Places in the content process");
   // Attempting to create two instances of the service?
   MOZ_ASSERT(!gDatabase);
   gDatabase = this;
-
-  // Prepare async shutdown
-  nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetShutdownPhase();
-  MOZ_ASSERT(shutdownPhase);
-
-  if (shutdownPhase) {
-    DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
-      static_cast<nsIAsyncShutdownBlocker*>(mConnectionShutdown.get()),
-      NS_LITERAL_STRING(__FILE__),
-      __LINE__,
-      NS_LITERAL_STRING(""));
-    MOZ_ASSERT(NS_SUCCEEDED(rv));
-  }
 }
 
 already_AddRefed<nsIAsyncShutdownClient>
-Database::GetShutdownPhase()
+Database::GetProfileChangeTeardownPhase()
 {
   nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown();
   MOZ_ASSERT(asyncShutdownSvc);
   if (NS_WARN_IF(!asyncShutdownSvc)) {
     return nullptr;
   }
 
+  // Consumers of Places should shutdown before us, at profile-change-teardown.
+  nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase;
+  DebugOnly<nsresult> rv = asyncShutdownSvc->
+    GetProfileChangeTeardown(getter_AddRefs(shutdownPhase));
+  MOZ_ASSERT(NS_SUCCEEDED(rv));
+  return shutdownPhase.forget();
+}
+
+already_AddRefed<nsIAsyncShutdownClient>
+Database::GetProfileBeforeChangePhase()
+{
+  nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown();
+  MOZ_ASSERT(asyncShutdownSvc);
+  if (NS_WARN_IF(!asyncShutdownSvc)) {
+    return nullptr;
+  }
+
+  // Consumers of Places should shutdown before us, at profile-change-teardown.
   nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase;
   DebugOnly<nsresult> rv = asyncShutdownSvc->
     GetProfileBeforeChange(getter_AddRefs(shutdownPhase));
   MOZ_ASSERT(NS_SUCCEEDED(rv));
   return shutdownPhase.forget();
 }
 
 Database::~Database()
@@ -644,29 +387,29 @@ Database::GetStatement(const nsACString&
   }
   if (NS_IsMainThread()) {
     return mMainThreadStatements.GetCachedStatement(aQuery);
   }
   return mAsyncThreadStatements.GetCachedStatement(aQuery);
 }
 
 already_AddRefed<nsIAsyncShutdownClient>
-Database::GetConnectionShutdown()
+Database::GetClientsShutdown()
 {
-  MOZ_ASSERT(mConnectionShutdown);
-
-  return mConnectionShutdown->GetClient();
+  MOZ_ASSERT(mClientsShutdown);
+  return mClientsShutdown->GetClient();
 }
 
 // static
 already_AddRefed<Database>
 Database::GetDatabase()
 {
-  if (DatabaseShutdown::IsStarted())
+  if (PlacesShutdownBlocker::IsStarted()) {
     return nullptr;
+  }
   return GetSingleton();
 }
 
 nsresult
 Database::Init()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -727,16 +470,46 @@ Database::Init()
   // Notify we have finished database initialization.
   // Enqueue the notification, so if we init another service that requires
   // nsNavHistoryService we don't recursive try to get it.
   RefPtr<PlacesEvent> completeEvent =
     new PlacesEvent(TOPIC_PLACES_INIT_COMPLETE);
   rv = NS_DispatchToMainThread(completeEvent);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // At this point we know the Database object points to a valid connection
+  // and we need to setup async shutdown.
+  {
+    // First of all Places clients should block profile-change-teardown.
+    nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileChangeTeardownPhase();
+    MOZ_ASSERT(shutdownPhase);
+    if (shutdownPhase) {
+      DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
+        static_cast<nsIAsyncShutdownBlocker*>(mClientsShutdown.get()),
+        NS_LITERAL_STRING(__FILE__),
+        __LINE__,
+        NS_LITERAL_STRING(""));
+      MOZ_ASSERT(NS_SUCCEEDED(rv));
+    }
+  }
+
+  {
+    // Then connection closing should block profile-before-change.
+    nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileBeforeChangePhase();
+    MOZ_ASSERT(shutdownPhase);
+    if (shutdownPhase) {
+      DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
+        static_cast<nsIAsyncShutdownBlocker*>(mConnectionShutdown.get()),
+        NS_LITERAL_STRING(__FILE__),
+        __LINE__,
+        NS_LITERAL_STRING(""));
+      MOZ_ASSERT(NS_SUCCEEDED(rv));
+    }
+  }
+
   // Finally observe profile shutdown notifications.
   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
   if (os) {
     (void)os->AddObserver(this, TOPIC_PROFILE_CHANGE_TEARDOWN, true);
   }
 
   return NS_OK;
 }
@@ -1854,29 +1627,28 @@ Database::MigrateV30Up() {
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 void
 Database::Shutdown()
 {
-
   // As the last step in the shutdown path, finalize the database handle.
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mClosed);
 
-  // Break cycle
-  nsCOMPtr<mozIStorageCompletionCallback> closeListener = mConnectionShutdown.forget();
+  // Break cycles with the shutdown blockers.
+  mClientsShutdown = nullptr;
+  nsCOMPtr<mozIStorageCompletionCallback> connectionShutdown = mConnectionShutdown.forget();
 
   if (!mMainConn) {
-    // The connection has never been initialized. Just mark it
-    // as closed.
+    // The connection has never been initialized. Just mark it as closed.
     mClosed = true;
-    (void)closeListener->Complete(NS_OK, nullptr);
+    (void)connectionShutdown->Complete(NS_OK, nullptr);
     return;
   }
 
 #ifdef DEBUG
   { // Sanity check for missing guids.
     bool haveNullGuids = false;
     nsCOMPtr<mozIStorageStatement> stmt;
 
@@ -1925,30 +1697,29 @@ Database::Shutdown()
     new FinalizeStatementCacheProxy<mozIStorageStatement>(
           mAsyncThreadStatements,
           NS_ISUPPORTS_CAST(nsIObserver*, this)
         );
   DispatchToAsyncThread(event);
 
   mClosed = true;
 
-  (void)mMainConn->AsyncClose(closeListener);
+  (void)mMainConn->AsyncClose(connectionShutdown);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIObserver
 
 NS_IMETHODIMP
 Database::Observe(nsISupports *aSubject,
                   const char *aTopic,
                   const char16_t *aData)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (strcmp(aTopic, TOPIC_PROFILE_CHANGE_TEARDOWN) == 0 ||
-      strcmp(aTopic, TOPIC_SIMULATE_PLACES_MUST_CLOSE_1) == 0) {
+  if (strcmp(aTopic, TOPIC_PROFILE_CHANGE_TEARDOWN) == 0) {
     // Tests simulating shutdown may cause multiple notifications.
     if (IsShutdownStarted()) {
       return NS_OK;
     }
 
     nsCOMPtr<nsIObserverService> os = services::GetObserverService();
     NS_ENSURE_STATE(os);
 
@@ -1967,30 +1738,50 @@ Database::Observe(nsISupports *aSubject,
           nsCOMPtr<nsIObserver> observer = do_QueryInterface(supports);
           (void)observer->Observe(observer, TOPIC_PLACES_INIT_COMPLETE, nullptr);
         }
       }
     }
 
     // Notify all Places users that we are about to shutdown.
     (void)os->NotifyObservers(nullptr, TOPIC_PLACES_SHUTDOWN, nullptr);
-  } else if (strcmp(aTopic, TOPIC_SIMULATE_PLACES_MUST_CLOSE_2) == 0) {
+  } else if (strcmp(aTopic, TOPIC_SIMULATE_PLACES_SHUTDOWN) == 0) {
+    // This notification is (and must be) only used by tests that are trying
+    // to simulate Places shutdown out of the normal shutdown path.
+
     // Tests simulating shutdown may cause re-entrance.
     if (IsShutdownStarted()) {
       return NS_OK;
     }
 
-    // Since we are going through shutdown of Database,
-    // we don't need to block actual shutdown anymore.
-    nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetShutdownPhase();
-    if (shutdownPhase) {
-      shutdownPhase->RemoveBlocker(mConnectionShutdown.get());
+    // We are simulating a shutdown, so invoke the shutdown blockers,
+    // wait for them, then proceed with connection shutdown.
+    // Since we are already going through shutdown, but it's not the real one,
+    // we won't need to block the real one anymore, so we can unblock it.
+    {
+      nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileChangeTeardownPhase();
+      if (shutdownPhase) {
+        shutdownPhase->RemoveBlocker(mClientsShutdown.get());
+      }
+      (void)mClientsShutdown->BlockShutdown(nullptr);
     }
 
-    return mConnectionShutdown->BlockShutdown(nullptr);
+    // Spin the events loop until the clients are done.
+    // Note, this is just for tests, specifically test_clearHistory_shutdown.js
+    while (mClientsShutdown->State() != PlacesShutdownBlocker::States::RECEIVED_DONE) {
+      (void)NS_ProcessNextEvent();
+    }
+
+    {
+      nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileBeforeChangePhase();
+      if (shutdownPhase) {
+        shutdownPhase->RemoveBlocker(mConnectionShutdown.get());
+      }
+      (void)mConnectionShutdown->BlockShutdown(nullptr);
+    }
   }
   return NS_OK;
 }
 
 
 
 } // namespace places
 } // namespace mozilla
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -9,16 +9,17 @@
 #include "nsWeakReference.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIObserver.h"
 #include "nsIAsyncShutdown.h"
 #include "mozilla/storage.h"
 #include "mozilla/storage/StatementCache.h"
 #include "mozilla/Attributes.h"
 #include "nsIEventTarget.h"
+#include "Shutdown.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
 #define DATABASE_SCHEMA_VERSION 30
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
 // Fired when initialization fails due to a locked database.
@@ -36,21 +37,17 @@
 // cleanup tasks should run at this stage, nothing should be added to the
 // database, nor APIs should be called.
 #define TOPIC_PLACES_WILL_CLOSE_CONNECTION "places-will-close-connection"
 // Fired when the connection has gone, nothing will work from now on.
 #define TOPIC_PLACES_CONNECTION_CLOSED "places-connection-closed"
 
 // Simulate profile-before-change. This topic may only be used by
 // calling `observe` directly on the database. Used for testing only.
-#define TOPIC_SIMULATE_PLACES_MUST_CLOSE_1 "test-simulate-places-shutdown-phase-1"
-
-// Simulate profile-before-change. This topic may only be used by
-// calling `observe` directly on the database. Used for testing only.
-#define TOPIC_SIMULATE_PLACES_MUST_CLOSE_2 "test-simulate-places-shutdown-phase-2"
+#define TOPIC_SIMULATE_PLACES_SHUTDOWN "test-simulate-places-shutdown"
 
 class nsIRunnable;
 
 namespace mozilla {
 namespace places {
 
 enum JournalMode {
   // Default SQLite journal mode.
@@ -59,17 +56,18 @@ enum JournalMode {
   // We fallback to this mode when WAL is unavailable.
 , JOURNAL_TRUNCATE
   // Unsafe in case of crashes on database swap or low memory.
 , JOURNAL_MEMORY
   // Can reduce number of fsyncs.  We try to use this mode by default.
 , JOURNAL_WAL
 };
 
-class DatabaseShutdown;
+class ClientsShutdownBlocker;
+class ConnectionShutdownBlocker;
 
 class Database final : public nsIObserver
                      , public nsSupportsWeakReference
 {
   typedef mozilla::storage::StatementCache<mozIStorageStatement> StatementCache;
   typedef mozilla::storage::StatementCache<mozIStorageAsyncStatement> AsyncStatementCache;
 
 public:
@@ -82,17 +80,17 @@ public:
    * Initializes the database connection and the schema.
    * In case of corruption the database is copied to a backup file and replaced.
    */
   nsresult Init();
 
   /**
    * The AsyncShutdown client used by clients of this API to be informed of shutdown.
    */
-  already_AddRefed<nsIAsyncShutdownClient> GetConnectionShutdown();
+  already_AddRefed<nsIAsyncShutdownClient> GetClientsShutdown();
 
   /**
    * Getter to use when instantiating the class.
    *
    * @return Singleton instance of this class.
    */
   static already_AddRefed<Database> GetDatabase();
 
@@ -264,17 +262,17 @@ protected:
   nsresult MigrateV25Up();
   nsresult MigrateV26Up();
   nsresult MigrateV27Up();
   nsresult MigrateV28Up();
   nsresult MigrateV30Up();
 
   nsresult UpdateBookmarkRootTitles();
 
-  friend class DatabaseShutdown;
+  friend class ConnectionShutdownBlocker;
 
 private:
   ~Database();
 
   /**
    * Singleton getter, invoked by class instantiation.
    */
   static already_AddRefed<Database> GetSingleton();
@@ -287,27 +285,29 @@ private:
   mutable AsyncStatementCache mMainThreadAsyncStatements;
   mutable StatementCache mAsyncThreadStatements;
 
   int32_t mDBPageSize;
   uint16_t mDatabaseStatus;
   bool mClosed;
 
   /**
-   * Determine at which shutdown phase we need to start shutting down
-   * the Database.
+   * Phases for shutting down the Database.
+   * See Shutdown.h for further details about the shutdown procedure.
    */
-  already_AddRefed<nsIAsyncShutdownClient> GetShutdownPhase();
+  already_AddRefed<nsIAsyncShutdownClient> GetProfileChangeTeardownPhase();
+  already_AddRefed<nsIAsyncShutdownClient> GetProfileBeforeChangePhase();
 
   /**
-   * A companion object in charge of shutting down the mozStorage
-   * connection once all clients have disconnected.
+   * Blockers in charge of waiting for the Places clients and then shutting
+   * down the mozStorage connection.
+   * See Shutdown.h for further details about the shutdown procedure.
    *
-   * Cycles between `this` and `mConnectionShutdown` are broken
-   * in `Shutdown()`.
+   * Cycles with these are broken in `Shutdown()`.
    */
-  RefPtr<DatabaseShutdown> mConnectionShutdown;
+  RefPtr<ClientsShutdownBlocker> mClientsShutdown;
+  RefPtr<ConnectionShutdownBlocker> mConnectionShutdown;
 };
 
 } // namespace places
 } // namespace mozilla
 
 #endif // mozilla_places_Database_h_
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/Shutdown.cpp
@@ -0,0 +1,229 @@
+/* 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/. */
+
+#include "Shutdown.h"
+#include "mozilla/unused.h"
+
+namespace mozilla {
+namespace places {
+
+uint16_t PlacesShutdownBlocker::sCounter = 0;
+Atomic<bool> PlacesShutdownBlocker::sIsStarted(false);
+
+PlacesShutdownBlocker::PlacesShutdownBlocker(const nsString& aName)
+  : mName(aName)
+  , mState(NOT_STARTED)
+  , mCounter(sCounter++)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  // During tests, we can end up with the Database singleton being resurrected.
+  // Make sure that each instance of DatabaseShutdown has a unique name.
+  if (mCounter > 1) {
+    mName.AppendInt(mCounter);
+  }
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+PlacesShutdownBlocker::GetName(nsAString& aName)
+{
+  aName = mName;
+  return NS_OK;
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+PlacesShutdownBlocker::GetState(nsIPropertyBag** aState)
+{
+  nsresult rv;
+  nsCOMPtr<nsIWritablePropertyBag2> bag =
+    do_CreateInstance("@mozilla.org/hash-property-bag;1", &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+
+  // Put `mState` in field `progress`
+  RefPtr<nsVariant> progress = new nsVariant();
+  rv = progress->SetAsUint8(mState);
+  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+  rv = bag->SetPropertyAsInterface(NS_LITERAL_STRING("progress"), progress);
+  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+
+  // Put `mBarrier`'s state in field `barrier`, if possible
+  if (!mBarrier) {
+    return NS_OK;
+  }
+  nsCOMPtr<nsIPropertyBag> barrierState;
+  rv = mBarrier->GetState(getter_AddRefs(barrierState));
+  if (NS_FAILED(rv)) {
+    return NS_OK;
+  }
+
+  RefPtr<nsVariant> barrier = new nsVariant();
+  rv = barrier->SetAsInterface(NS_GET_IID(nsIPropertyBag), barrierState);
+  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+  rv = bag->SetPropertyAsInterface(NS_LITERAL_STRING("Barrier"), barrier);
+  if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+
+  return NS_OK;
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+PlacesShutdownBlocker::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
+{
+  MOZ_ASSERT(false, "should always be overridden");
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMPL_ISUPPORTS(
+  PlacesShutdownBlocker,
+  nsIAsyncShutdownBlocker
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+ClientsShutdownBlocker::ClientsShutdownBlocker()
+  : PlacesShutdownBlocker(NS_LITERAL_STRING("Places Clients shutdown"))
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  // Create a barrier that will be exposed to clients through GetClient(), so
+  // they can block Places shutdown.
+  nsCOMPtr<nsIAsyncShutdownService> asyncShutdown = services::GetAsyncShutdown();
+  MOZ_ASSERT(asyncShutdown);
+  if (asyncShutdown) {
+    nsCOMPtr<nsIAsyncShutdownBarrier> barrier;
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(asyncShutdown->MakeBarrier(mName, getter_AddRefs(barrier))));
+    mBarrier = new nsMainThreadPtrHolder<nsIAsyncShutdownBarrier>(barrier);
+  }
+}
+
+already_AddRefed<nsIAsyncShutdownClient>
+ClientsShutdownBlocker::GetClient()
+{
+  nsCOMPtr<nsIAsyncShutdownClient> client;
+  if (mBarrier) {
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(mBarrier->GetClient(getter_AddRefs(client))));
+  }
+  return client.forget();
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+ClientsShutdownBlocker::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mParentClient = new nsMainThreadPtrHolder<nsIAsyncShutdownClient>(aParentClient);
+  mState = RECEIVED_BLOCK_SHUTDOWN;
+
+  if (NS_WARN_IF(!mBarrier)) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  // Wait until all the clients have removed their blockers.
+  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(mBarrier->Wait(this)));
+
+  mState = CALLED_WAIT_CLIENTS;
+  return NS_OK;
+}
+
+// nsIAsyncShutdownCompletionCallback
+NS_IMETHODIMP
+ClientsShutdownBlocker::Done()
+{
+  // At this point all the clients are done, we can stop blocking the shutdown
+  // phase.
+  mState = RECEIVED_DONE;
+
+  // mParentClient is nullptr in tests.
+  if (mParentClient) {
+    nsresult rv = mParentClient->RemoveBlocker(this);
+    if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+    mParentClient = nullptr;
+  }
+  mBarrier = nullptr;
+  return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(
+  ClientsShutdownBlocker,
+  PlacesShutdownBlocker,
+  nsIAsyncShutdownCompletionCallback
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+ConnectionShutdownBlocker::ConnectionShutdownBlocker(Database* aDatabase)
+  : PlacesShutdownBlocker(NS_LITERAL_STRING("Places Clients shutdown"))
+  , mDatabase(aDatabase)
+{
+  // Do nothing.
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+ConnectionShutdownBlocker::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mParentClient = new nsMainThreadPtrHolder<nsIAsyncShutdownClient>(aParentClient);
+  mState = RECEIVED_BLOCK_SHUTDOWN;
+  // Annotate that Database shutdown started.
+  sIsStarted = true;
+
+  // Fire internal database closing notification.
+  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+  MOZ_ASSERT(os);
+  if (os) {
+    Unused << os->NotifyObservers(nullptr, TOPIC_PLACES_WILL_CLOSE_CONNECTION, nullptr);
+  }
+  mState = NOTIFIED_OBSERVERS_PLACES_WILL_CLOSE_CONNECTION;
+
+  // At this stage, any use of this database is forbidden. Get rid of
+  // `gDatabase`. Note, however, that the database could be
+  // resurrected.  This can happen in particular during tests.
+  MOZ_ASSERT(Database::gDatabase == nullptr || Database::gDatabase == mDatabase);
+  Database::gDatabase = nullptr;
+
+  // Database::Shutdown will invoke Complete once the connection is closed.
+  mDatabase->Shutdown();
+  mState = CALLED_STORAGESHUTDOWN;
+  return NS_OK;
+}
+
+// mozIStorageCompletionCallback
+NS_IMETHODIMP
+ConnectionShutdownBlocker::Complete(nsresult, nsISupports*)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mState = RECEIVED_STORAGESHUTDOWN_COMPLETE;
+
+  // The connection is closed, the Database has no more use, so we can break
+  // possible cycles.
+  mDatabase = nullptr;
+
+  // Notify the connection has gone.
+  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+  MOZ_ASSERT(os);
+  if (os) {
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(os->NotifyObservers(nullptr,
+                                                     TOPIC_PLACES_CONNECTION_CLOSED,
+                                                     nullptr)));
+  }
+  mState = NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED;
+
+  // mParentClient is nullptr in tests
+  if (mParentClient) {
+    nsresult rv = mParentClient->RemoveBlocker(this);
+    if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+    mParentClient = nullptr;
+  }
+  return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(
+  ConnectionShutdownBlocker,
+  PlacesShutdownBlocker,
+  mozIStorageCompletionCallback
+)
+
+} // namespace places
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/Shutdown.h
@@ -0,0 +1,171 @@
+/* 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/. */
+
+#ifndef mozilla_places_Shutdown_h_
+#define mozilla_places_Shutdown_h_
+
+#include "nsIAsyncShutdown.h"
+#include "Database.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla {
+namespace places {
+
+class Database;
+
+/**
+ * This is most of the code responsible for Places shutdown.
+ *
+ * PHASE 1 (Legacy clients shutdown)
+ * The shutdown procedure begins when the Database singleton receives
+ * profile-change-teardown (note that tests will instead notify nsNavHistory,
+ * that forwards the notification to the Database instance).
+ * Database::Observe first of all checks if initialization was completed
+ * properly, to avoid race conditions, then it notifies "places-shutdown" to
+ * legacy clients. Legacy clients are supposed to start and complete any
+ * shutdown critical work in the same tick, since we won't wait for them.
+
+ * PHASE 2 (Modern clients shutdown)
+ * Modern clients should instead register as a blocker by passing a promise to
+ * nsPIPlacesDatabase::shutdownClient (for example see sanitize.js), so they
+ * block Places shutdown until the promise is resolved.
+ * When profile-change-teardown is observed by async shutdown, it calls
+ * ClientsShutdownBlocker::BlockShutdown. This class is registered as a teardown
+ * phase blocker in Database::Init (see Database::mClientsShutdown).
+ * ClientsShutdownBlocker::BlockShudown waits for all the clients registered
+ * through nsPIPlacesDatabase::shutdownClient. When all the clients are done,
+ * its `Done` method is invoked, and it stops blocking the shutdown phase, so
+ * that it can continue.
+ *
+ * PHASE 3 (Connection shutdown)
+ * ConnectionBlocker is registered as a profile-before-change blocker in
+ * Database::Init (see Database::mConnectionShutdown).
+ * When profile-before-change is observer by async shutdown, it calls
+ * ConnectionShutdownBlocker::BlockShutdown.
+ * This is the last chance for any Places internal work, like privacy cleanups,
+ * before the connection is closed. This a places-will-close-connection
+ * notification is sent to legacy clients that must complete any operation in
+ * the same tick, since we won't wait for them.
+ * Then the control is passed to Database::Shutdown, that executes some sanity
+ * checks, clears cached statements and proceeds with asyncClose.
+ * Once the connection is definitely closed, Database will call back
+ * ConnectionBlocker::Complete. At this point a final
+ * places-connection-closed notification is sent, for testing purposes.
+ */
+
+/**
+ * A base AsyncShutdown blocker in charge of shutting down Places.
+ */
+class PlacesShutdownBlocker : public nsIAsyncShutdownBlocker
+{
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+  explicit PlacesShutdownBlocker(const nsString& aName);
+
+  /**
+   * `true` if we have not started shutdown, i.e.  if
+   * `BlockShutdown()` hasn't been called yet, false otherwise.
+   */
+  static bool IsStarted() {
+    return sIsStarted;
+  }
+
+  // The current state, used internally and for forensics/debugging purposes.
+  // Not all the states make sense for all the derived classes.
+  enum States {
+    NOT_STARTED,
+    // Execution of `BlockShutdown` in progress.
+    RECEIVED_BLOCK_SHUTDOWN,
+
+    // Values specific to ClientsShutdownBlocker
+    // a. Set while we are waiting for clients to do their job and unblock us.
+    CALLED_WAIT_CLIENTS,
+    // b. Set when all the clients are done.
+    RECEIVED_DONE,
+
+    // Values specific to ConnectionShutdownBlocker
+    // a. Set after we notified observers that Places is closing the connection.
+    NOTIFIED_OBSERVERS_PLACES_WILL_CLOSE_CONNECTION,
+    // b. Set after we pass control to Database::Shutdown, and wait for it to
+    // close the connection and call our `Complete` method when done.
+    CALLED_STORAGESHUTDOWN,
+    // c. Set when Database has closed the connection and passed control to
+    // us through `Complete`.
+    RECEIVED_STORAGESHUTDOWN_COMPLETE,
+    // d. We have notified observers that Places has closed the connection.
+    NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED,
+  };
+  States State() {
+    return mState;
+  }
+
+protected:
+  // The blocker name, also used as barrier name.
+  nsString mName;
+  // The current state, see States.
+  States mState;
+  // The barrier optionally used to wait for clients.
+  nsMainThreadPtrHandle<nsIAsyncShutdownBarrier> mBarrier;
+  // The parent object who registered this as a blocker.
+  nsMainThreadPtrHandle<nsIAsyncShutdownClient> mParentClient;
+
+  // As tests may resurrect a dead `Database`, we use a counter to
+  // give the instances of `PlacesShutdownBlocker` unique names.
+  uint16_t mCounter;
+  static uint16_t sCounter;
+
+  static Atomic<bool> sIsStarted;
+
+  virtual ~PlacesShutdownBlocker() {}
+};
+
+/**
+ * Blocker also used to wait for clients, through an owned barrier.
+ */
+class ClientsShutdownBlocker final : public PlacesShutdownBlocker
+                                   , public nsIAsyncShutdownCompletionCallback
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_NSIASYNCSHUTDOWNCOMPLETIONCALLBACK
+
+  explicit ClientsShutdownBlocker();
+
+  NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient* aParentClient) override;
+
+  already_AddRefed<nsIAsyncShutdownClient> GetClient();
+
+private:
+  ~ClientsShutdownBlocker() {}
+};
+
+/**
+ * Blocker used to wait when closing the database connection.
+ */
+class ConnectionShutdownBlocker final : public PlacesShutdownBlocker
+                                      , public mozIStorageCompletionCallback
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
+
+  NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient* aParentClient) override;
+
+  explicit ConnectionShutdownBlocker(mozilla::places::Database* aDatabase);
+
+private:
+  ~ConnectionShutdownBlocker() {}
+
+  // The owning database.
+  // The cycle is broken in method Complete(), once the connection
+  // has been closed by mozStorage.
+  RefPtr<mozilla::places::Database> mDatabase;
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_places_Shutdown_h_
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -42,16 +42,17 @@ if CONFIG['MOZ_PLACES']:
         'nsAnnotationService.cpp',
         'nsFaviconService.cpp',
         'nsNavBookmarks.cpp',
         'nsNavHistory.cpp',
         'nsNavHistoryQuery.cpp',
         'nsNavHistoryResult.cpp',
         'nsPlacesModule.cpp',
         'PlaceInfo.cpp',
+        'Shutdown.cpp',
         'SQLFunctions.cpp',
         'VisitInfo.cpp',
     ]
 
     LOCAL_INCLUDES += [
         '../build',
     ]
 
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -2965,17 +2965,17 @@ nsNavHistory::GetDBConnection(mozIStorag
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNavHistory::GetShutdownClient(nsIAsyncShutdownClient **_shutdownClient)
 {
   NS_ENSURE_ARG_POINTER(_shutdownClient);
-  RefPtr<nsIAsyncShutdownClient> client = mDB->GetConnectionShutdown();
+  RefPtr<nsIAsyncShutdownClient> client = mDB->GetClientsShutdown();
   MOZ_ASSERT(client);
   client.forget(_shutdownClient);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNavHistory::AsyncExecuteLegacyQueries(nsINavHistoryQuery** aQueries,
@@ -3077,18 +3077,17 @@ nsNavHistory::NotifyOnPageExpired(nsIURI
 
 NS_IMETHODIMP
 nsNavHistory::Observe(nsISupports *aSubject, const char *aTopic,
                     const char16_t *aData)
 {
   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
   if (strcmp(aTopic, TOPIC_PROFILE_TEARDOWN) == 0 ||
       strcmp(aTopic, TOPIC_PROFILE_CHANGE) == 0 ||
-      strcmp(aTopic, TOPIC_SIMULATE_PLACES_MUST_CLOSE_1) == 0 ||
-      strcmp(aTopic, TOPIC_SIMULATE_PLACES_MUST_CLOSE_2) == 0) {
+      strcmp(aTopic, TOPIC_SIMULATE_PLACES_SHUTDOWN) == 0) {
     // These notifications are used by tests to simulate a Places shutdown.
     // They should just be forwarded to the Database handle.
     mDB->Observe(aSubject, aTopic, aData);
   }
 
   else if (strcmp(aTopic, TOPIC_PLACES_CONNECTION_CLOSED) == 0) {
       // Don't even try to notify observers from this point on, the category
       // cache would init services that could try to use our APIs.
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -380,20 +380,20 @@ function promiseTopicObserved(aTopic)
  * Simulates a Places shutdown.
  */
 var shutdownPlaces = function() {
   do_print("shutdownPlaces: starting");
   let promise = new Promise(resolve => {
     Services.obs.addObserver(resolve, "places-connection-closed", false);
   });
   let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver);
-  hs.observe(null, "test-simulate-places-shutdown-phase-1", null);
-  do_print("shutdownPlaces: sent test-simulate-places-shutdown-phase-1");
-  hs.observe(null, "test-simulate-places-shutdown-phase-2", null);
-  do_print("shutdownPlaces: sent test-simulate-places-shutdown-phase-2");
+  hs.observe(null, "profile-change-teardown", null);
+  do_print("shutdownPlaces: sent profile-change-teardown");
+  hs.observe(null, "test-simulate-places-shutdown", null);
+  do_print("shutdownPlaces: sent test-simulate-places-shutdown");
   return promise.then(() => {
     do_print("shutdownPlaces: complete");
   });
 };
 
 const FILENAME_BOOKMARKS_HTML = "bookmarks.html";
 const FILENAME_BOOKMARKS_JSON = "bookmarks-" +
   (PlacesBackups.toISODateString(new Date())) + ".json";