bounce draft cache-crash
authorAndrew Sutherland <asutherland@asutherland.org>
Mon, 26 Jun 2017 02:12:39 -0400
changeset 403798 b854a3171eff3071ccefcb1924f6599bc8194f48
parent 400026 34ac1a5d6576d6775491c8a882710a1520551da6
push id4
push userbugmail@asutherland.org
push dateMon, 26 Jun 2017 06:55:04 +0000
milestone55.0a1
bounce
dom/cache/test/mochitest/mochitest.ini
dom/cache/test/mochitest/test_cache_qm_client_shutdown.html
dom/cache/test/mochitest/worker_cache_qm_client_shutdown.js
dom/quota/ActorsParent.cpp
dom/quota/Client.h
dom/quota/PQuota.ipdl
dom/quota/QuotaManager.h
dom/quota/QuotaManagerService.cpp
dom/quota/SerializationHelpers.h
dom/quota/nsIQuotaManagerService.idl
--- a/dom/cache/test/mochitest/mochitest.ini
+++ b/dom/cache/test/mochitest/mochitest.ini
@@ -20,30 +20,32 @@ support-files =
   test_cache_requestCache.js
   test_cache_delete.js
   test_cache_put_reorder.js
   test_cache_redirect.js
   test_cache_https.js
   large_url_list.js
   empty.html
   idle_worker.js
+  worker_cache_qm_client_shutdown.js
 
 [test_cache.html]
 [test_cache_add.html]
 [test_cache_match_request.html]
 [test_cache_matchAll_request.html]
 [test_cache_overwrite.html]
 [test_cache_match_vary.html]
 [test_caches.html]
 [test_cache_keys.html]
 [test_cache_put.html]
 [test_cache_requestCache.html]
 [test_cache_delete.html]
 [test_cache_put_reorder.html]
 [test_cache_https.html]
+[test_cache_qm_client_shutdown.html]
 [test_cache_redirect.html]
 [test_cache_restart.html]
 [test_cache_shrink.html]
 [test_cache_orphaned_cache.html]
 [test_cache_orphaned_body.html]
 scheme=https
 [test_cache_untrusted.html]
 [test_chrome_constructor.html]
new file mode 100644
--- /dev/null
+++ b/dom/cache/test/mochitest/test_cache_qm_client_shutdown.html
@@ -0,0 +1,109 @@
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test Cache QuotaManager Client Shutdown</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+/**
+ * This is a test of safe shutdown of our QuotaClient with live child streams.
+ **/
+function setupTestIframe() {
+  return new Promise(function(resolve) {
+    var iframe = document.createElement("iframe");
+    iframe.src = "empty.html";
+    iframe.onload = function() {
+      window.cachesApi = iframe.contentWindow.caches;
+      resolve();
+    };
+    document.body.appendChild(iframe);
+  });
+}
+
+// Returns the Worker and a waitForMessage(type) helper that, when called,
+// returns a promise that will be resolved when the message of the given type
+// arrives.
+function setupWorker() {
+  let pending = null;
+  let waitForMessage = function(expectType) {
+    return new Promise((resolve) => {
+      pending = { type: expectType, resolve };
+    });
+  }
+
+  let worker = new Worker("worker_cache_qm_client_shutdown.js");
+  worker.addEventListener(
+    "message",
+    (evt) => {
+      switch (evt.data.type) {
+        case "info":
+          info("worker says: " + evt.data.message);
+          break;
+        case "error":
+          ok(false, "worker failed: " + evt.data.message);
+          break;
+        default:
+          if (pending && pending.type === evt.data.type) {
+            pending.resolve(evt.data);
+            pending = null;
+          } else {
+            ok(false, "unexpected response type: " + evt.data.type);
+          }
+          break;
+      }
+    });
+
+  return { worker, waitForMessage };
+}
+
+function resetQuotaClient() {
+  return new Promise(function(resolve, reject) {
+    var qms = SpecialPowers.Services.qms;
+    var request = qms.resetAndRestartClient("cache");
+    var cb = SpecialPowers.wrapCallback(resolve);
+    request.callback = cb;
+  });
+}
+
+add_task(async function() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [["dom.caches.enabled", true],
+            ["dom.caches.testing.enabled", true],
+            ["dom.quotaManager.testing", true]],
+  });
+
+
+  let { worker, waitForMessage } = setupWorker();
+  await waitForMessage("ready");
+
+  info("worker ready, approving deletions");
+  worker.postMessage({ type: "delete" });
+  await waitForMessage("deleted");
+
+  worker.terminate();
+
+  // Reset the client.  This should have cleaned stuff up.
+  info("resetting cache quota client");
+  await resetQuotaClient();
+  info("...reset complete");
+
+  ok(true, "yay, we did not crash");
+
+  // Let"s make sure that didn"t break the world too much...
+/*
+  const reopenedCache = await caches.open(name);
+  const responseAgain = await reopenedCache.match(url1);
+  ok(responseAgain, "yay, things still work");
+
+  // Let"s get rid of that cache.
+  await caches.delete(name);
+*/
+});
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/cache/test/mochitest/worker_cache_qm_client_shutdown.js
@@ -0,0 +1,93 @@
+/**
+ * We're used by test_cache_qm_client_shutdown to:
+ * - be a global that can be cleaned up on command via termination
+ * - duplicate a crash related to ServiceWorkers but where we can kill it.
+ *
+ * We
+ */
+
+
+let pendingMessages = [];
+let pendingResolves = [];
+function waitForMessage() {
+  return new Promise(function(resolve) {
+    if (pendingMessages.length) {
+      resolve(pendingMessages.shift());
+      return;
+    }
+    pendingResolves.push(resolve);
+  });
+}
+addEventListener("message", function(evt) {
+  if (pendingResolves.length) {
+    pendingResolves.shift()(evt.data);
+    return;
+  }
+  pendingMessages.push(evt.data);
+});
+function info(message) {
+  postMessage({ type: "info", message });
+}
+
+async function runTest() {
+  const cacheNamePrefix = "resety";
+  const cacheCount = 9;
+  const urlPrefix = "./test_cache_add.js";
+  const url1 = "./test_cache_add.js?1";
+  const url2 = "./test_cache_add.js?2";
+
+  info("opening caches");
+  let cacheNames = []
+  for (let i = 0; i < cacheCount; i++) {
+    cacheNames.push(cacheNamePrefix + i);
+  }
+  let allCaches = await Promise.all(cacheNames.map(name => caches.open(name)));
+
+  // Put stuff in the cache.
+  info("populating caches");
+  await Promise.all(allCaches.map(cache => cache.addAll([url1, url2])));
+
+  // Get some responses
+  info("getting single responses");
+  let singleResponses =
+    await Promise.all(allCaches.map(cache => cache.match(url1)));
+
+  info("getting multiple responses");
+  let multiResponses =
+    await Promise.all(allCaches.map((cache) => {
+      return cache.matchAll(urlPrefix, { ignoreSearch: true });
+    }));
+
+  if (multiResponses[0].length !== 2) {
+    throw new Error("responses did not come back like we expected");
+  }
+
+  // uh, have at least one of them be used?
+  //const textPromise = multiResponses[0][1].text();
+
+  postMessage({ type: "ready" });
+  await waitForMessage();
+
+  // Delete the caches, rendering them orphans.  However they can't be deleted
+  // until the Cache and StreamList instances are removed.
+  info("deleting caches");
+  await Promise.all(cacheNames.map(name => caches.delete(name)));
+
+  // Drop 1/3 of our Cache references and 1/3 of our responses so we can create
+  // different shutdown scenarios.
+  const pivotMultiple = Math.floor(cacheCount / 3);
+  for (let i = 0; i < pivotMultiple; i++) {
+    caches[i] = null;
+    singleResponses[pivotMultiple + i] = null;
+    multiResponses[pivotMultiple + i] = null;
+  }
+
+  postMessage({ type: "deleted" });
+  // this message will never come.  it's just for us to keep our locals alive
+  // until terminated.
+  await waitForMessage();
+}
+
+runTest().catch((err) => {
+  postMessage({ type: "error", message: err + "" });
+});
--- a/dom/quota/ActorsParent.cpp
+++ b/dom/quota/ActorsParent.cpp
@@ -1238,29 +1238,36 @@ private:
   void
   GetResponse(RequestResponse& aResponse) override;
 };
 
 class ResetOrClearOp final
   : public QuotaRequestBase
 {
   const bool mClear;
+  // QuotaClient type to reset; TYPE_MAX means don't reset a client.
+  const Client::Type mResetClientType;
 
 public:
-  explicit ResetOrClearOp(bool aClear)
+  explicit ResetOrClearOp(bool aClear,
+                          Client::Type aResetClientType=Client::TYPE_MAX)
     : QuotaRequestBase(/* aExclusive */ true)
     , mClear(aClear)
+    , mResetClientType(aResetClientType)
   {
     AssertIsOnOwningThread();
   }
 
 private:
   ~ResetOrClearOp()
   { }
 
+  virtual void
+  Open() override;
+
   void
   DeleteFiles(QuotaManager* aQuotaManager);
 
   virtual nsresult
   DoDirectoryWork(QuotaManager* aQuotaManager) override;
 
   virtual void
   GetResponse(RequestResponse& aResponse) override;
@@ -5212,16 +5219,49 @@ QuotaManager::ResetOrClearCompleted()
 
   mInitializedOrigins.Clear();
   mTemporaryStorageInitialized = false;
   mStorageInitialized = false;
 
   ReleaseIOThreadObjects();
 }
 
+
+void
+QuotaManager::ResetQuotaClient(Client::Type aClientType)
+{
+  MOZ_ASSERT(aClientType >= Client::IDB);
+  MOZ_ASSERT(aClientType < Client::TYPE_MAX);
+  // It's not enough to have added it to QuotaManager::Init!  (Note that we need
+  // to assert on the specific indices, not just TYPE_MAX.)
+  static_assert(Client::IDB == 0 && Client::ASMJS == 1 && Client::DOMCACHE == 2,
+                "Add your new Client here and below too.");
+
+  // Invoke shutdown and destroy the client before creating a new one.  The
+  // intent is to duplicate what QuotaManager::Shutdown() does.  Note that the
+  // ReleaseIOThreadObjects call already happened on the IO thread.
+  printf("resetting client type: %d\n", (int)aClientType);
+  mClients[aClientType]->ShutdownWorkThreads();
+  mClients[aClientType] = nullptr;
+  switch (aClientType) {
+    case Client::IDB:
+      mClients[aClientType] = indexedDB::CreateQuotaClient();
+      break;
+    case Client::ASMJS:
+      mClients[aClientType] = asmjscache::CreateClient();
+      break;
+    case Client::DOMCACHE:
+      mClients[aClientType] = cache::CreateQuotaClient();
+      break;
+    default:
+      NS_NOTREACHED("no such client type");
+      break;
+  }
+}
+
 Client*
 QuotaManager::GetClient(Client::Type aClientType)
 {
   MOZ_ASSERT(aClientType >= Client::IDB);
   MOZ_ASSERT(aClientType < Client::TYPE_MAX);
 
   return mClients.ElementAt(aClientType);
 }
@@ -6435,16 +6475,22 @@ Quota::AllocPQuotaRequestParent(const Re
     case RequestParams::TClearAllParams:
       actor = new ResetOrClearOp(/* aClear */ true);
       break;
 
     case RequestParams::TResetAllParams:
       actor = new ResetOrClearOp(/* aClear */ false);
       break;
 
+    case RequestParams::TResetClientParams:
+      actor = new ResetOrClearOp(
+                    /* aClear */ false,
+                    aParams.get_ResetClientParams().resetClientType());
+      break;
+
     case RequestParams::TPersistedParams:
       actor = new PersistedOp(aParams);
       break;
 
     case RequestParams::TPersistParams:
       actor = new PersistOp(aParams);
       break;
 
@@ -7223,16 +7269,26 @@ ResetOrClearOp::DeleteFiles(QuotaManager
   if (rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
       rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) {
     // This should never fail if we've closed the storage connection
     // correctly...
     MOZ_ASSERT(false, "Failed to remove storage file!");
   }
 }
 
+void
+ResetOrClearOp::Open()
+{
+  if (mResetClientType != Client::TYPE_MAX) {
+    QuotaManager::Get()->ResetQuotaClient(mResetClientType);
+  }
+
+  NormalOriginOperationBase::Open();
+}
+
 nsresult
 ResetOrClearOp::DoDirectoryWork(QuotaManager* aQuotaManager)
 {
   AssertIsOnIOThread();
 
   PROFILER_LABEL("Quota", "ResetOrClearOp::DoDirectoryWork",
                  js::ProfileEntry::Category::OTHER);
 
@@ -7246,16 +7302,17 @@ ResetOrClearOp::DoDirectoryWork(QuotaMan
 
   return NS_OK;
 }
 
 void
 ResetOrClearOp::GetResponse(RequestResponse& aResponse)
 {
   AssertIsOnOwningThread();
+
   if (mClear) {
     aResponse = ClearAllResponse();
   } else {
     aResponse = ResetAllResponse();
   }
 }
 
 void
--- a/dom/quota/Client.h
+++ b/dom/quota/Client.h
@@ -144,11 +144,15 @@ public:
   WillShutdown()
   { }
 
 protected:
   virtual ~Client()
   { }
 };
 
+// Create a more descriptive type alias that IPDL can use.  The parser chokes on
+// "Client::Type" in the struct or a typedef in the ipdl file.
+typedef Client::Type ClientType;
+
 END_QUOTA_NAMESPACE
 
 #endif // mozilla_dom_quota_client_h__
--- a/dom/quota/PQuota.ipdl
+++ b/dom/quota/PQuota.ipdl
@@ -7,16 +7,18 @@ include protocol PQuotaRequest;
 include protocol PQuotaUsageRequest;
 
 include PBackgroundSharedTypes;
 
 include "mozilla/dom/quota/SerializationHelpers.h";
 
 using mozilla::dom::quota::PersistenceType
   from "mozilla/dom/quota/PersistenceType.h";
+using mozilla::dom::quota::ClientType
+  from "mozilla/dom/quota/Client.h";
 
 namespace mozilla {
 namespace dom {
 namespace quota {
 
 struct InitParams
 {
 };
@@ -60,16 +62,21 @@ struct ClearDataParams
 struct ClearAllParams
 {
 };
 
 struct ResetAllParams
 {
 };
 
+struct ResetClientParams
+{
+  ClientType resetClientType;
+};
+
 struct PersistedParams
 {
   PrincipalInfo principalInfo;
 };
 
 struct PersistParams
 {
   PrincipalInfo principalInfo;
@@ -78,16 +85,17 @@ struct PersistParams
 union RequestParams
 {
   InitParams;
   InitOriginParams;
   ClearOriginParams;
   ClearDataParams;
   ClearAllParams;
   ResetAllParams;
+  ResetClientParams;
   PersistedParams;
   PersistParams;
 };
 
 protocol PQuota
 {
   manager PBackground;
 
--- a/dom/quota/QuotaManager.h
+++ b/dom/quota/QuotaManager.h
@@ -299,16 +299,19 @@ public:
   void
   OriginClearCompleted(PersistenceType aPersistenceType,
                        const nsACString& aOrigin);
 
   void
   ResetOrClearCompleted();
 
   void
+  ResetQuotaClient(Client::Type aClientType);
+
+  void
   StartIdleMaintenance()
   {
     AssertIsOnOwningThread();
 
     for (auto& client : mClients) {
       client->StartIdleMaintenance();
     }
   }
--- a/dom/quota/QuotaManagerService.cpp
+++ b/dom/quota/QuotaManagerService.cpp
@@ -740,16 +740,49 @@ QuotaManagerService::Reset(nsIQuotaReque
     return rv;
   }
 
   request.forget(_retval);
   return NS_OK;
 }
 
 NS_IMETHODIMP
+QuotaManagerService::ResetAndRestartClient(const nsAString& aClientType,
+                                           nsIQuotaRequest** _retval)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (NS_WARN_IF(!gTestingMode)) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  Client::Type clientType = Client::TYPE_MAX;
+  nsresult rv = Client::TypeFromText(aClientType, clientType);
+  // Throw if that's not a client type.
+  if (NS_FAILED(rv)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  RefPtr<Request> request = new Request();
+
+  ResetClientParams params(clientType);
+
+  nsAutoPtr<PendingRequestInfo> info(new RequestInfo(request, params));
+
+  rv = InitiateRequest(info);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  request.forget(_retval);
+  return NS_OK;
+}
+
+
+NS_IMETHODIMP
 QuotaManagerService::Persisted(nsIPrincipal* aPrincipal,
                                nsIQuotaRequest** _retval)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aPrincipal);
   MOZ_ASSERT(_retval);
 
   RefPtr<Request> request = new Request(aPrincipal);
--- a/dom/quota/SerializationHelpers.h
+++ b/dom/quota/SerializationHelpers.h
@@ -4,23 +4,32 @@
  * 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_dom_quota_SerializationHelpers_h
 #define mozilla_dom_quota_SerializationHelpers_h
 
 #include "ipc/IPCMessageUtils.h"
 
+#include "mozilla/dom/quota/Client.h"
 #include "mozilla/dom/quota/PersistenceType.h"
 
 namespace IPC {
 
 template <>
 struct ParamTraits<mozilla::dom::quota::PersistenceType> :
   public ContiguousEnumSerializer<
                                mozilla::dom::quota::PersistenceType,
                                mozilla::dom::quota::PERSISTENCE_TYPE_PERSISTENT,
                                mozilla::dom::quota::PERSISTENCE_TYPE_INVALID>
 { };
 
+template <>
+struct ParamTraits<mozilla::dom::quota::Client::Type> :
+  public ContiguousEnumSerializer<
+                               mozilla::dom::quota::Client::Type,
+                               mozilla::dom::quota::Client::IDB,
+                               mozilla::dom::quota::Client::TYPE_MAX>
+{ };
+
 } // namespace IPC
 
 #endif // mozilla_dom_quota_SerializationHelpers_h
--- a/dom/quota/nsIQuotaManagerService.idl
+++ b/dom/quota/nsIQuotaManagerService.idl
@@ -112,16 +112,33 @@ interface nsIQuotaManagerService : nsISu
    *
    * If the dom.quotaManager.testing preference is not true the call will be
    * a no-op.
    */
   [must_use] nsIQuotaRequest
   reset();
 
   /**
+   * Perform a reset where we also invoke ShutdownWorkThreads() on the given
+   * QuotaClient and re-create its client.  This is appropriate to use when
+   * you are either trying to test the shutdown behavior of a QuotaManager
+   * client or have a client that populates state from calls to InitOrigin() and
+   * you want it to start from a clean slate.  Normal reset() calls do not
+   * shutdown or otherwise reinitialize the QuotaClient instances.
+   *
+   * You must provided a valid client type string as understood by
+   * Client::TypeFromText or an error will be thrown.
+   *
+   * If the dom.quotaManager.testing preference is not true the call will be
+   * a no-op.
+   */
+  [must_use] nsIQuotaRequest
+  resetAndRestartClient(in AString aClientType);
+
+  /**
    * Check if given origin is persisted.
    *
    * @param aPrincipal
    *        A principal for the origin which we want to check.
    */
   [must_use] nsIQuotaRequest
   persisted(in nsIPrincipal aPrincipal);