Bug 1474557 - Clear the dbPromise cached by ext-storage when IndexedDB raises an exception while opening the db. r=mixedpuppy
authorLuca Greco <lgreco@mozilla.com>
Mon, 02 Jul 2018 21:37:58 +0200
changeset 426587 d2b1558a677c
parent 426586 18c5ad6ddb9e
child 426588 91cd5bee61ef
push id34276
push userncsoregi@mozilla.com
push date2018-07-14 09:41 +0000
treeherdermozilla-central@04dd259d71db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1474557
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1474557 - Clear the dbPromise cached by ext-storage when IndexedDB raises an exception while opening the db. r=mixedpuppy 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