Bug 1474557 - Clear the dbPromise cached by ext-storage when IndexedDB raises an exception while opening the db. r=mixedpuppy a=lizzard
authorLuca Greco <lgreco@mozilla.com>
Mon, 02 Jul 2018 21:37:58 +0200
changeset 478023 52e3d099d0632f231688d9b3142cb50a72d1aa2a
parent 478022 7fa538f4814b2e18309f4a1c0c092fbef2d2fcf2
child 478024 2cb21e6a62f8befbb2550d7770aa23d1314432cb
push id9500
push userarchaeopteryx@coole-files.de
push dateThu, 19 Jul 2018 07:13:35 +0000
treeherdermozilla-beta@9573e5a35f1c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, lizzard
bugs1474557
milestone62.0
Bug 1474557 - Clear the dbPromise cached by ext-storage when IndexedDB raises an exception while opening the db. r=mixedpuppy a=lizzard MozReview-Commit-ID: 9qsDYI0wgmI
toolkit/components/extensions/child/ext-storage.js
toolkit/components/extensions/test/xpcshell/head_storage.js
toolkit/components/extensions/test/xpcshell/test_ext_storage_tab.js
toolkit/components/extensions/test/xpcshell/xpcshell-remote.ini
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/child/ext-storage.js
+++ b/toolkit/components/extensions/child/ext-storage.js
@@ -9,17 +9,16 @@ ChromeUtils.defineModuleGetter(this, "Te
 
 // Telemetry histogram keys for the JSONFile backend.
 const storageGetHistogram = "WEBEXT_STORAGE_LOCAL_GET_MS";
 const storageSetHistogram = "WEBEXT_STORAGE_LOCAL_SET_MS";
 // Telemetry  histogram keys for the IndexedDB backend.
 const storageGetIDBHistogram = "WEBEXT_STORAGE_LOCAL_IDB_GET_MS";
 const storageSetIDBHistogram = "WEBEXT_STORAGE_LOCAL_IDB_SET_MS";
 
-
 // Wrap a storage operation in a TelemetryStopWatch.
 async function measureOp(histogram, fn) {
   const stopwatchKey = {};
   TelemetryStopwatch.start(histogram, stopwatchKey);
   try {
     let result = await fn();
     TelemetryStopwatch.finish(histogram, stopwatchKey);
     return result;
@@ -52,59 +51,73 @@ this.storage = class extends ExtensionAP
       clear() {
         return context.childManager.callParentAsyncFunction(
           "storage.local.JSONFileBackend.clear", []);
       },
     };
   }
 
   getLocalIDBBackend(context, {hasParentListeners, serialize, storagePrincipal}) {
-    const dbPromise = ExtensionStorageIDB.open(storagePrincipal);
+    let dbPromise;
+    async function getDB() {
+      if (dbPromise) {
+        return dbPromise;
+      }
+
+      dbPromise = ExtensionStorageIDB.open(storagePrincipal).catch(err => {
+        // Reset the cached promise if it has been rejected, so that the next
+        // API call is going to retry to open the DB.
+        dbPromise = null;
+        throw err;
+      });
+
+      return dbPromise;
+    }
 
     return {
       get(keys) {
         return measureOp(storageGetIDBHistogram, async () => {
-          const db = await dbPromise;
+          const db = await getDB();
           return db.get(keys);
         });
       },
       set(items) {
         return measureOp(storageSetIDBHistogram, async () => {
-          const db = await dbPromise;
+          const db = await getDB();
           const changes = await db.set(items, {
             serialize: ExtensionStorage.serialize,
           });
 
           if (!changes) {
             return;
           }
 
           const hasListeners = await hasParentListeners();
           if (hasListeners) {
             await context.childManager.callParentAsyncFunction(
               "storage.local.IDBBackend.fireOnChanged", [changes]);
           }
         });
       },
       async remove(keys) {
-        const db = await dbPromise;
+        const db = await getDB();
         const changes = await db.remove(keys);
 
         if (!changes) {
           return;
         }
 
         const hasListeners = await hasParentListeners();
         if (hasListeners) {
           await context.childManager.callParentAsyncFunction(
             "storage.local.IDBBackend.fireOnChanged", [changes]);
         }
       },
       async clear() {
-        const db = await dbPromise;
+        const db = await getDB();
         const changes = await db.clear(context.extension);
 
         if (!changes) {
           return;
         }
 
         const hasListeners = await hasParentListeners();
         if (hasListeners) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/head_storage.js
@@ -0,0 +1,21 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported setLowDiskMode */
+
+// Small test helper to trigger the QuotaExceededError (it is basically the same
+// test helper defined in "dom/indexedDB/test/unit/test_lowDiskSpace.js", but it doesn't
+// use SpecialPowers).
+let lowDiskMode = false;
+function setLowDiskMode(val) {
+  let data = val ? "full" : "free";
+
+  if (val == lowDiskMode) {
+    info("Low disk mode is: " + data);
+  } else {
+    info("Changing low disk mode to: " + data);
+    Services.obs.notifyObservers(null, "disk-space-watcher", data);
+    lowDiskMode = val;
+  }
+}
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_tab.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_tab.js
@@ -175,8 +175,91 @@ add_task(async function test_storage_loc
   return runWithPrefs([[ExtensionStorageIDB.BACKEND_ENABLED_PREF, false]],
                       test_storage_local_call_from_destroying_context);
 });
 
 add_task(async function test_storage_local_idb_backend_destroyed_context_promise() {
   return runWithPrefs([[ExtensionStorageIDB.BACKEND_ENABLED_PREF, true]],
                       test_storage_local_call_from_destroying_context);
 });
+
+add_task(async function test_storage_local_should_not_cache_idb_open_rejections() {
+  async function test_storage_local_on_idb_disk_full_rejection() {
+    let extension = ExtensionTestUtils.loadExtension({
+      async background() {
+        browser.test.sendMessage("ext-page-url", browser.runtime.getURL("tab.html"));
+      },
+      files: {
+        "tab.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <script src="tab.js"></script>
+          </head>
+        </html>`,
+
+        "tab.js"() {
+          browser.test.onMessage.addListener(async ({msg, expectErrorOnSet}) => {
+            if (msg !== "call-storage-local") {
+              return;
+            }
+
+            const expectedValue = "newvalue";
+
+            try {
+              await browser.storage.local.set({"newkey": expectedValue});
+            } catch (err) {
+              if (expectErrorOnSet) {
+                browser.test.sendMessage("storage-local-set-rejected");
+                return;
+              }
+
+              browser.test.fail(`Got an unexpected exception on storage.local.get: ${err}`);
+              throw err;
+            }
+
+            try {
+              const res = await browser.storage.local.get("newvalue");
+              browser.test.assertEq(expectedValue, res.newkey);
+              browser.test.sendMessage("storage-local-get-resolved");
+            } catch (err) {
+              browser.test.fail(`Got an unexpected exception on storage.local.get: ${err}`);
+              throw err;
+            }
+          });
+
+          browser.test.sendMessage("extension-tab-ready");
+        },
+      },
+      manifest: {
+        permissions: ["storage"],
+      },
+    });
+
+    await extension.startup();
+    const url = await extension.awaitMessage("ext-page-url");
+
+    let contentPage = await ExtensionTestUtils.loadContentPage(url, {extension});
+    await extension.awaitMessage("extension-tab-ready");
+
+    // Turn the low disk mode on (so that opening an IndexedDB connection raises a
+    // QuotaExceededError).
+    setLowDiskMode(true);
+
+    extension.sendMessage({msg: "call-storage-local", expectErrorOnSet: true});
+    info(`Wait the storage.local.set API call to reject while the disk is full`);
+    await extension.awaitMessage("storage-local-set-rejected");
+    info("Got the a rejection on storage.local.set while the disk is full as expected");
+
+    setLowDiskMode(false);
+    extension.sendMessage({msg: "call-storage-local", expectErrorOnSet: false});
+    info(`Wait the storage.local API calls to resolve successfully once the disk is free again`);
+    await extension.awaitMessage("storage-local-get-resolved");
+    info("storage.local.set and storage.local.get resolve successfully once the disk is free again");
+
+    contentPage.close();
+    await extension.unload();
+  }
+
+  return runWithPrefs([[ExtensionStorageIDB.BACKEND_ENABLED_PREF, true]],
+                      test_storage_local_on_idb_disk_full_rejection);
+});
+
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-remote.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-remote.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-head = head.js head_remote.js head_e10s.js head_telemetry.js
+head = head.js head_remote.js head_e10s.js head_telemetry.js head_storage.js
 tail =
 firefox-appdir = browser
 skip-if = appname == "thunderbird" || os == "android"
 dupe-manifest =
 support-files =
   data/**
   xpcshell-content.ini
 tags = webextensions remote-webextensions
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-head = head.js head_telemetry.js
+head = head.js head_telemetry.js head_storage.js
 firefox-appdir = browser
 skip-if = appname == "thunderbird"
 dupe-manifest =
 support-files =
   data/**
   head_sync.js
   xpcshell-content.ini
 tags = webextensions in-process-webextensions