Bug 1110487 P2 Implement the nsIOfflineStorage interface in Cache. r=janv,ehsan
authorBen Kelly <ben@wanderview.com>
Mon, 16 Mar 2015 07:10:36 -0700
changeset 264061 a13277e46eb96344cc4fb36d036d6b59ba287db4
parent 264060 794e1729fb0deed5c535ae3177f47e14db6384a5
child 264062 1a468067441b53e56717df11f868125d6e144638
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanv, ehsan
bugs1110487
milestone39.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 1110487 P2 Implement the nsIOfflineStorage interface in Cache. r=janv,ehsan
dom/cache/Context.cpp
dom/cache/Context.h
dom/cache/Manager.cpp
dom/cache/Manager.h
dom/cache/OfflineStorage.cpp
dom/cache/OfflineStorage.h
dom/cache/QuotaClient.cpp
dom/cache/QuotaClient.h
dom/cache/StreamList.cpp
dom/cache/StreamList.h
dom/cache/Types.h
dom/cache/moz.build
dom/cache/test/mochitest/driver.js
--- a/dom/cache/Context.cpp
+++ b/dom/cache/Context.cpp
@@ -5,58 +5,57 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/cache/Context.h"
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/dom/cache/Action.h"
 #include "mozilla/dom/cache/Manager.h"
 #include "mozilla/dom/cache/ManagerId.h"
+#include "mozilla/dom/cache/OfflineStorage.h"
 #include "mozilla/dom/quota/OriginOrPatternString.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "nsIFile.h"
 #include "nsIPrincipal.h"
 #include "nsIRunnable.h"
 #include "nsThreadUtils.h"
 
 namespace {
 
 using mozilla::dom::Nullable;
 using mozilla::dom::cache::QuotaInfo;
+using mozilla::dom::quota::Client;
 using mozilla::dom::quota::OriginOrPatternString;
 using mozilla::dom::quota::QuotaManager;
 using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
 using mozilla::dom::quota::PersistenceType;
 
-// Executed when the context is destroyed to release our lock on the
-// QuotaManager.
+// Release our lock on the QuotaManager directory asynchronously.
 class QuotaReleaseRunnable final : public nsRunnable
 {
 public:
-  QuotaReleaseRunnable(const QuotaInfo& aQuotaInfo, const nsACString& aQuotaId)
+  explicit QuotaReleaseRunnable(const QuotaInfo& aQuotaInfo)
     : mQuotaInfo(aQuotaInfo)
-    , mQuotaId(aQuotaId)
   { }
 
   NS_IMETHOD Run() override
   {
     MOZ_ASSERT(NS_IsMainThread());
     QuotaManager* qm = QuotaManager::Get();
     MOZ_ASSERT(qm);
     qm->AllowNextSynchronizedOp(OriginOrPatternString::FromOrigin(mQuotaInfo.mOrigin),
                                 Nullable<PersistenceType>(PERSISTENCE_TYPE_DEFAULT),
-                                mQuotaId);
+                                mQuotaInfo.mStorageId);
     return NS_OK;
   }
 
 private:
   ~QuotaReleaseRunnable() { }
 
   const QuotaInfo mQuotaInfo;
-  const nsCString mQuotaId;
 };
 
 } // anonymous namespace
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
@@ -65,30 +64,30 @@ using mozilla::dom::quota::OriginOrPatte
 using mozilla::dom::quota::QuotaManager;
 using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
 using mozilla::dom::quota::PersistenceType;
 
 // Executed to perform the complicated dance of steps necessary to initialize
 // the QuotaManager.  This must be performed for each origin before any disk
 // IO occurrs.
 class Context::QuotaInitRunnable final : public nsIRunnable
-                                           , public Action::Resolver
+                                       , public Action::Resolver
 {
 public:
   QuotaInitRunnable(Context* aContext,
                     Manager* aManager,
-                    const nsACString& aQuotaId,
                     Action* aQuotaIOThreadAction)
     : mContext(aContext)
+    , mThreadsafeHandle(aContext->CreateThreadsafeHandle())
     , mManager(aManager)
-    , mQuotaId(aQuotaId)
     , mQuotaIOThreadAction(aQuotaIOThreadAction)
     , mInitiatingThread(NS_GetCurrentThread())
+    , mResult(NS_OK)
     , mState(STATE_INIT)
-    , mResult(NS_OK)
+    , mNeedsQuotaRelease(false)
   {
     MOZ_ASSERT(mContext);
     MOZ_ASSERT(mManager);
     MOZ_ASSERT(mInitiatingThread);
   }
 
   nsresult Dispatch()
   {
@@ -147,23 +146,25 @@ private:
     NS_ASSERT_OWNINGTHREAD(Action::Resolver);
     MOZ_ASSERT(mContext);
     mContext = nullptr;
     mManager = nullptr;
     mQuotaIOThreadAction = nullptr;
   }
 
   nsRefPtr<Context> mContext;
+  nsRefPtr<ThreadsafeHandle> mThreadsafeHandle;
   nsRefPtr<Manager> mManager;
-  const nsCString mQuotaId;
   nsRefPtr<Action> mQuotaIOThreadAction;
   nsCOMPtr<nsIThread> mInitiatingThread;
-  State mState;
   nsresult mResult;
   QuotaInfo mQuotaInfo;
+  nsMainThreadPtrHandle<OfflineStorage> mOfflineStorage;
+  State mState;
+  bool mNeedsQuotaRelease;
 
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSIRUNNABLE
 };
 
 NS_IMPL_ISUPPORTS_INHERITED(mozilla::dom::cache::Context::QuotaInitRunnable,
                             Action::Resolver, nsIRunnable);
@@ -225,35 +226,49 @@ Context::QuotaInitRunnable::Run()
                                              &mQuotaInfo.mGroup,
                                              &mQuotaInfo.mOrigin,
                                              &mQuotaInfo.mIsApp);
       if (NS_WARN_IF(NS_FAILED(rv))) {
         Resolve(rv);
         return NS_OK;
       }
 
+      QuotaManager::GetStorageId(PERSISTENCE_TYPE_DEFAULT,
+                                 mQuotaInfo.mOrigin,
+                                 Client::DOMCACHE,
+                                 NS_LITERAL_STRING("cache"),
+                                 mQuotaInfo.mStorageId);
+
       // QuotaManager::WaitForOpenAllowed() will hold a reference to us as
       // a callback.  We will then get executed again on the main thread when
       // it is safe to open the quota directory.
       mState = STATE_WAIT_FOR_OPEN_ALLOWED;
       rv = qm->WaitForOpenAllowed(OriginOrPatternString::FromOrigin(mQuotaInfo.mOrigin),
                                   Nullable<PersistenceType>(PERSISTENCE_TYPE_DEFAULT),
-                                  mQuotaId, this);
+                                  mQuotaInfo.mStorageId, this);
       if (NS_FAILED(rv)) {
         Resolve(rv);
         return NS_OK;
       }
       break;
     }
     // ------------------------------
     case STATE_WAIT_FOR_OPEN_ALLOWED:
     {
       MOZ_ASSERT(NS_IsMainThread());
+
+      mNeedsQuotaRelease = true;
+
       QuotaManager* qm = QuotaManager::Get();
       MOZ_ASSERT(qm);
+
+      nsRefPtr<OfflineStorage> offlineStorage =
+        OfflineStorage::Register(mThreadsafeHandle, mQuotaInfo);
+      mOfflineStorage = new nsMainThreadPtrHolder<OfflineStorage>(offlineStorage);
+
       mState = STATE_ENSURE_ORIGIN_INITIALIZED;
       nsresult rv = qm->IOThread()->Dispatch(this, nsIThread::DISPATCH_NORMAL);
       if (NS_WARN_IF(NS_FAILED(rv))) {
         Resolve(rv);
         return NS_OK;
       }
       break;
     }
@@ -293,18 +308,25 @@ Context::QuotaInitRunnable::Run()
     }
     // -------------------
     case STATE_COMPLETING:
     {
       NS_ASSERT_OWNINGTHREAD(Action::Resolver);
       if (mQuotaIOThreadAction) {
         mQuotaIOThreadAction->CompleteOnInitiatingThread(mResult);
       }
-      mContext->OnQuotaInit(mResult, mQuotaInfo);
+      mContext->OnQuotaInit(mResult, mQuotaInfo, mOfflineStorage);
       mState = STATE_COMPLETE;
+
+      if (mNeedsQuotaRelease) {
+        // Unlock the quota dir if we locked it previously
+        nsCOMPtr<nsIRunnable> runnable = new QuotaReleaseRunnable(mQuotaInfo);
+        MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable)));
+      }
+
       // Explicitly cleanup here as the destructor could fire on any of
       // the threads we have bounced through.
       Clear();
       break;
     }
     // -----
     default:
     {
@@ -316,16 +338,17 @@ Context::QuotaInitRunnable::Run()
   return NS_OK;
 }
 
 // Runnable wrapper around Action objects dispatched on the Context.  This
 // runnable executes the Action on the appropriate threads while the Context
 // is initialized.
 class Context::ActionRunnable final : public nsIRunnable
                                     , public Action::Resolver
+                                    , public Context::Activity
 {
 public:
   ActionRunnable(Context* aContext, nsIEventTarget* aTarget, Action* aAction,
                  const QuotaInfo& aQuotaInfo)
     : mContext(aContext)
     , mTarget(aTarget)
     , mAction(aAction)
     , mQuotaInfo(aQuotaInfo)
@@ -349,22 +372,25 @@ public:
     nsresult rv = mTarget->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       mState = STATE_COMPLETE;
       Clear();
     }
     return rv;
   }
 
-  bool MatchesCacheId(CacheId aCacheId) {
+  virtual bool
+  MatchesCacheId(CacheId aCacheId) const override
+  {
     NS_ASSERT_OWNINGTHREAD(Action::Resolver);
     return mAction->MatchesCacheId(aCacheId);
   }
 
-  void Cancel()
+  virtual void
+  Cancel() override
   {
     NS_ASSERT_OWNINGTHREAD(Action::Resolver);
     mAction->CancelOnInitiatingThread();
   }
 
   virtual void Resolve(nsresult aRv) override
   {
     MOZ_ASSERT(mTarget == NS_GetCurrentThread());
@@ -387,17 +413,17 @@ private:
     MOZ_ASSERT(!mAction);
   }
 
   void Clear()
   {
     NS_ASSERT_OWNINGTHREAD(Action::Resolver);
     MOZ_ASSERT(mContext);
     MOZ_ASSERT(mAction);
-    mContext->OnActionRunnableComplete(this);
+    mContext->RemoveActivity(this);
     mContext = nullptr;
     mAction = nullptr;
   }
 
   enum State
   {
     STATE_INIT,
     STATE_RUN_ON_TARGET,
@@ -477,25 +503,117 @@ Context::ActionRunnable::Run()
     {
       MOZ_CRASH("unexpected state in ActionRunnable");
       break;
     }
   }
   return NS_OK;
 }
 
+void
+Context::ThreadsafeHandle::AllowToClose()
+{
+  if (mOwningThread == NS_GetCurrentThread()) {
+    AllowToCloseOnOwningThread();
+    return;
+  }
+
+  // Dispatch is guaranteed to succeed here because we block shutdown until
+  // all Contexts have been destroyed.
+  nsCOMPtr<nsIRunnable> runnable =
+    NS_NewRunnableMethod(this, &ThreadsafeHandle::AllowToCloseOnOwningThread);
+  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+    mOwningThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)));
+}
+
+void
+Context::ThreadsafeHandle::InvalidateAndAllowToClose()
+{
+  if (mOwningThread == NS_GetCurrentThread()) {
+    InvalidateAndAllowToCloseOnOwningThread();
+    return;
+  }
+
+  // Dispatch is guaranteed to succeed here because we block shutdown until
+  // all Contexts have been destroyed.
+  nsCOMPtr<nsIRunnable> runnable =
+    NS_NewRunnableMethod(this, &ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread);
+  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+    mOwningThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)));
+}
+
+Context::ThreadsafeHandle::ThreadsafeHandle(Context* aContext)
+  : mStrongRef(aContext)
+  , mWeakRef(aContext)
+  , mOwningThread(NS_GetCurrentThread())
+{
+}
+
+Context::ThreadsafeHandle::~ThreadsafeHandle()
+{
+  // Normally we only touch mStrongRef on the owning thread.  This is safe,
+  // however, because when we do use mStrongRef on the owning thread we are
+  // always holding a strong ref to the ThreadsafeHandle via the owning
+  // runnable.  So we cannot run the ThreadsafeHandle destructor simultaneously.
+  if (!mStrongRef || mOwningThread == NS_GetCurrentThread()) {
+    return;
+  }
+
+  // Dispatch is guaranteed to succeed here because we block shutdown until
+  // all Contexts have been destroyed.
+  nsCOMPtr<nsIRunnable> runnable =
+    NS_NewNonOwningRunnableMethod(mStrongRef.forget().take(), &Context::Release);
+  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+    mOwningThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)));
+}
+
+void
+Context::ThreadsafeHandle::AllowToCloseOnOwningThread()
+{
+  MOZ_ASSERT(mOwningThread == NS_GetCurrentThread());
+  // A Context "closes" when its ref count drops to zero.  Dropping this
+  // strong ref is necessary, but not sufficient for the close to occur.
+  // Any outstanding IO will continue and keep the Context alive.  Once
+  // the Context is idle, it will be destroyed.
+  mStrongRef = nullptr;
+}
+
+void
+Context::ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread()
+{
+  MOZ_ASSERT(mOwningThread == NS_GetCurrentThread());
+  // Cancel the Context through the weak reference.  This means we can
+  // allow the Context to close by dropping the strong ref, but then
+  // still cancel ongoing IO if necessary.
+  if (mWeakRef) {
+    mWeakRef->Invalidate();
+  }
+  // We should synchronously have AllowToCloseOnOwningThread called when
+  // the Context is canceled.
+  MOZ_ASSERT(!mStrongRef);
+}
+
+void
+Context::ThreadsafeHandle::ContextDestroyed(Context* aContext)
+{
+  MOZ_ASSERT(mOwningThread == NS_GetCurrentThread());
+  MOZ_ASSERT(!mStrongRef);
+  MOZ_ASSERT(mWeakRef);
+  MOZ_ASSERT(mWeakRef == aContext);
+  mWeakRef = nullptr;
+}
+
 // static
 already_AddRefed<Context>
 Context::Create(Manager* aManager, Action* aQuotaIOThreadAction)
 {
   nsRefPtr<Context> context = new Context(aManager);
 
   nsRefPtr<QuotaInitRunnable> runnable =
-    new QuotaInitRunnable(context, aManager, NS_LITERAL_CSTRING("Cache"),
-                          aQuotaIOThreadAction);
+    new QuotaInitRunnable(context, aManager, aQuotaIOThreadAction);
   nsresult rv = runnable->Dispatch();
   if (NS_FAILED(rv)) {
     // Shutdown must be delayed until all Contexts are destroyed.  Shutdown
     // must also prevent any new Contexts from being constructed.  Crash
     // for this invariant violation.
     MOZ_CRASH("Failed to dispatch QuotaInitRunnable.");
   }
 
@@ -530,52 +648,71 @@ Context::Dispatch(nsIEventTarget* aTarge
 }
 
 void
 Context::CancelAll()
 {
   NS_ASSERT_OWNINGTHREAD(Context);
   mState = STATE_CONTEXT_CANCELED;
   mPendingActions.Clear();
-  for (uint32_t i = 0; i < mActionRunnables.Length(); ++i) {
-    nsRefPtr<ActionRunnable> runnable = mActionRunnables[i];
-    runnable->Cancel();
+  {
+    ActivityList::ForwardIterator iter(mActivityList);
+    while (iter.HasMore()) {
+      iter.GetNext()->Cancel();
+    }
+  }
+  AllowToClose();
+}
+
+void
+Context::Invalidate()
+{
+  NS_ASSERT_OWNINGTHREAD(Context);
+  mManager->Invalidate();
+  CancelAll();
+}
+
+void
+Context::AllowToClose()
+{
+  NS_ASSERT_OWNINGTHREAD(Context);
+  if (mThreadsafeHandle) {
+    mThreadsafeHandle->AllowToClose();
   }
 }
 
 void
 Context::CancelForCacheId(CacheId aCacheId)
 {
   NS_ASSERT_OWNINGTHREAD(Context);
-  for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
+
+  // Remove matching pending actions
+  for (int32_t i = mPendingActions.Length() - 1; i >= 0; --i) {
     if (mPendingActions[i].mAction->MatchesCacheId(aCacheId)) {
       mPendingActions.RemoveElementAt(i);
     }
   }
-  for (uint32_t i = 0; i < mActionRunnables.Length(); ++i) {
-    nsRefPtr<ActionRunnable> runnable = mActionRunnables[i];
-    if (runnable->MatchesCacheId(aCacheId)) {
-      runnable->Cancel();
+
+  // Cancel activities and let them remove themselves
+  ActivityList::ForwardIterator iter(mActivityList);
+  while (iter.HasMore()) {
+    Activity* activity = iter.GetNext();
+    if (activity->MatchesCacheId(aCacheId)) {
+      activity->Cancel();
     }
   }
 }
 
 Context::~Context()
 {
   NS_ASSERT_OWNINGTHREAD(Context);
   MOZ_ASSERT(mManager);
 
-  // Unlock the quota dir as we go out of scope.
-  nsCOMPtr<nsIRunnable> runnable =
-    new QuotaReleaseRunnable(mQuotaInfo, NS_LITERAL_CSTRING("Cache"));
-  nsresult rv = NS_DispatchToMainThread(runnable, nsIThread::DISPATCH_NORMAL);
-  if (NS_FAILED(rv)) {
-    // Shutdown must be delayed until all Contexts are destroyed.  Crash
-    // for this invariant violation.
-    MOZ_CRASH("Failed to dispatch QuotaReleaseRunnable to main thread.");
+  if (mThreadsafeHandle) {
+    mThreadsafeHandle->ContextDestroyed(this);
   }
 
   mManager->RemoveContext(this);
 }
 
 void
 Context::DispatchAction(nsIEventTarget* aTarget, Action* aAction)
 {
@@ -584,47 +721,75 @@ Context::DispatchAction(nsIEventTarget* 
   nsRefPtr<ActionRunnable> runnable =
     new ActionRunnable(this, aTarget, aAction, mQuotaInfo);
   nsresult rv = runnable->Dispatch();
   if (NS_FAILED(rv)) {
     // Shutdown must be delayed until all Contexts are destroyed.  Crash
     // for this invariant violation.
     MOZ_CRASH("Failed to dispatch ActionRunnable to target thread.");
   }
-  mActionRunnables.AppendElement(runnable);
+  AddActivity(runnable);
 }
 
 void
-Context::OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo)
+Context::OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo,
+                     nsMainThreadPtrHandle<OfflineStorage>& aOfflineStorage)
 {
   NS_ASSERT_OWNINGTHREAD(Context);
 
   mQuotaInfo = aQuotaInfo;
 
+  // Always save the offline storage to ensure QuotaManager does not shutdown
+  // before the Context has gone away.
+  MOZ_ASSERT(!mOfflineStorage);
+  mOfflineStorage = aOfflineStorage;
+
   if (mState == STATE_CONTEXT_CANCELED || NS_FAILED(aRv)) {
     for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
       mPendingActions[i].mAction->CompleteOnInitiatingThread(aRv);
     }
     mPendingActions.Clear();
+    mThreadsafeHandle->AllowToClose();
     // Context will destruct after return here and last ref is released.
     return;
   }
 
   MOZ_ASSERT(mState == STATE_CONTEXT_INIT);
   mState = STATE_CONTEXT_READY;
 
   for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
     DispatchAction(mPendingActions[i].mTarget, mPendingActions[i].mAction);
   }
   mPendingActions.Clear();
 }
 
 void
-Context::OnActionRunnableComplete(ActionRunnable* aActionRunnable)
+Context::AddActivity(Activity* aActivity)
+{
+  NS_ASSERT_OWNINGTHREAD(Context);
+  MOZ_ASSERT(aActivity);
+  MOZ_ASSERT(!mActivityList.Contains(aActivity));
+  mActivityList.AppendElement(aActivity);
+}
+
+void
+Context::RemoveActivity(Activity* aActivity)
 {
   NS_ASSERT_OWNINGTHREAD(Context);
-  MOZ_ASSERT(aActionRunnable);
-  MOZ_ALWAYS_TRUE(mActionRunnables.RemoveElement(aActionRunnable));
+  MOZ_ASSERT(aActivity);
+  MOZ_ALWAYS_TRUE(mActivityList.RemoveElement(aActivity));
+  MOZ_ASSERT(!mActivityList.Contains(aActivity));
+}
+
+already_AddRefed<Context::ThreadsafeHandle>
+Context::CreateThreadsafeHandle()
+{
+  NS_ASSERT_OWNINGTHREAD(Context);
+  if (!mThreadsafeHandle) {
+    mThreadsafeHandle = new ThreadsafeHandle(this);
+  }
+  nsRefPtr<ThreadsafeHandle> ref = mThreadsafeHandle;
+  return ref.forget();
 }
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
--- a/dom/cache/Context.h
+++ b/dom/cache/Context.h
@@ -6,71 +6,150 @@
 
 #ifndef mozilla_dom_cache_Context_h
 #define mozilla_dom_cache_Context_h
 
 #include "mozilla/dom/cache/Types.h"
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
 #include "nsISupportsImpl.h"
+#include "nsProxyRelease.h"
 #include "nsString.h"
 #include "nsTArray.h"
+#include "nsTObserverArray.h"
 
 class nsIEventTarget;
+class nsIThread;
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
 class Action;
 class Manager;
+class OfflineStorage;
 
 // The Context class is RAII-style class for managing IO operations within the
 // Cache.
 //
 // When a Context is created it performs the complicated steps necessary to
 // initialize the QuotaManager.  Action objects dispatched on the Context are
 // delayed until this initialization is complete.  They are then allow to
 // execute on any specified thread.  Once all references to the Context are
 // gone, then the steps necessary to release the QuotaManager are performed.
-// Since pending Action objects reference the Context, this allows overlapping
-// IO to opportunistically run without re-initializing the QuotaManager again.
+// After initialization the Context holds a self reference, so it will stay
+// alive until one of three conditions occur:
+//
+//  1) The Manager will call Context::AllowToClose() when all of the actors
+//     have removed themselves as listener.  This means an idle context with
+//     no active DOM objects will close gracefully.
+//  2) The QuotaManager invalidates the storage area so it can delete the
+//     files.  In this case the OfflineStorage calls Cache::Invalidate() which
+//     in turn cancels all existing Action objects and then marks the Manager
+//     as invalid.
+//  3) Browser shutdown occurs and the Manager calls Context::CancelAll().
+//
+// In either case, though, the Action objects must be destroyed first to
+// allow the Context to be destroyed.
 //
 // While the Context performs operations asynchronously on threads, all of
 // methods in its public interface must be called on the same thread
 // originally used to create the Context.
 //
 // As an invariant, all Context objects must be destroyed before permitting
 // the "profile-before-change" shutdown event to complete.  This is ensured
 // via the code in ShutdownObserver.cpp.
 class Context final
 {
 public:
+  // Define a class allowing other threads to hold the Context alive.  This also
+  // allows these other threads to safely close or cancel the Context.
+  class ThreadsafeHandle final
+  {
+    friend class Context;
+  public:
+    void AllowToClose();
+    void InvalidateAndAllowToClose();
+  private:
+    explicit ThreadsafeHandle(Context* aContext);
+    ~ThreadsafeHandle();
+
+    // disallow copying
+    ThreadsafeHandle(const ThreadsafeHandle&) = delete;
+    ThreadsafeHandle& operator=(const ThreadsafeHandle&) = delete;
+
+    void AllowToCloseOnOwningThread();
+    void InvalidateAndAllowToCloseOnOwningThread();
+
+    void ContextDestroyed(Context* aContext);
+
+    // Cleared to allow the Context to close.  Only safe to access on
+    // owning thread.
+    nsRefPtr<Context> mStrongRef;
+
+    // Used to support cancelation even while the Context is already allowed
+    // to close.  Cleared by ~Context() calling ContextDestroyed().  Only
+    // safe to access on owning thread.
+    Context* mWeakRef;
+
+    nsCOMPtr<nsIThread> mOwningThread;
+
+    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(cache::Context::ThreadsafeHandle)
+  };
+
+  // Different objects hold references to the Context while some work is being
+  // performed asynchronously.  These objects must implement the Activity
+  // interface and register themselves with the AddActivity().  When they are
+  // destroyed they must call RemoveActivity().  This allows the Context to
+  // cancel any outstanding Activity work when the Context is cancelled.
+  class Activity
+  {
+  public:
+    virtual void Cancel() = 0;
+    virtual bool MatchesCacheId(CacheId aCacheId) const = 0;
+  };
+
   static already_AddRefed<Context>
   Create(Manager* aManager, Action* aQuotaIOThreadAction);
 
   // Execute given action on the target once the quota manager has been
   // initialized.
   //
   // Only callable from the thread that created the Context.
   void Dispatch(nsIEventTarget* aTarget, Action* aAction);
 
   // Cancel any Actions running or waiting to run.  This should allow the
   // Context to be released and Listener::RemoveContext() will be called
   // when complete.
   //
   // Only callable from the thread that created the Context.
   void CancelAll();
 
+  // Like CancelAll(), but also marks the Manager as "invalid".
+  void Invalidate();
+
+  // Remove any self references and allow the Context to be released when
+  // there are no more Actions to process.
+  void AllowToClose();
+
   // Cancel any Actions running or waiting to run that operate on the given
   // cache ID.
   //
   // Only callable from the thread that created the Context.
   void CancelForCacheId(CacheId aCacheId);
 
+  void AddActivity(Activity* aActivity);
+  void RemoveActivity(Activity* aActivity);
+
+  const QuotaInfo&
+  GetQuotaInfo() const
+  {
+    return mQuotaInfo;
+  }
+
 private:
   class QuotaInitRunnable;
   class ActionRunnable;
 
   enum State
   {
     STATE_CONTEXT_INIT,
     STATE_CONTEXT_READY,
@@ -81,26 +160,38 @@ private:
   {
     nsCOMPtr<nsIEventTarget> mTarget;
     nsRefPtr<Action> mAction;
   };
 
   explicit Context(Manager* aManager);
   ~Context();
   void DispatchAction(nsIEventTarget* aTarget, Action* aAction);
-  void OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo);
-  void OnActionRunnableComplete(ActionRunnable* const aAction);
+  void OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo,
+                   nsMainThreadPtrHandle<OfflineStorage>& aOfflineStorage);
+
+  already_AddRefed<ThreadsafeHandle>
+  CreateThreadsafeHandle();
 
   nsRefPtr<Manager> mManager;
   State mState;
   QuotaInfo mQuotaInfo;
   nsTArray<PendingAction> mPendingActions;
 
-  // weak refs since ~ActionRunnable() removes itself from this list
-  nsTArray<ActionRunnable*> mActionRunnables;
+  // Weak refs since activites must remove themselves from this list before
+  // being destroyed by calling RemoveActivity().
+  typedef nsTObserverArray<Activity*> ActivityList;
+  ActivityList mActivityList;
+
+  // The ThreadsafeHandle may have a strong ref back to us.  This creates
+  // a ref-cycle that keeps the Context alive.  The ref-cycle is broken
+  // when ThreadsafeHandle::AllowToClose() is called.
+  nsRefPtr<ThreadsafeHandle> mThreadsafeHandle;
+
+  nsMainThreadPtrHandle<OfflineStorage> mOfflineStorage;
 
 public:
   NS_INLINE_DECL_REFCOUNTING(cache::Context)
 };
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
--- a/dom/cache/Manager.cpp
+++ b/dom/cache/Manager.cpp
@@ -172,17 +172,20 @@ public:
     mozilla::ipc::AssertIsOnBackgroundThread();
 
     nsresult rv = MaybeCreateInstance();
     if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; }
 
     ManagerList::ForwardIterator iter(sFactory->mManagerList);
     while (iter.HasMore()) {
       nsRefPtr<Manager> manager = iter.GetNext();
-      if (*manager->mManagerId == *aManagerId) {
+      // If there is an invalid Manager finishing up and a new Manager
+      // is created for the same origin, then the new Manager will
+      // be blocked until QuotaManager finishes clearing the origin.
+      if (manager->IsValid() && *manager->mManagerId == *aManagerId) {
         return manager.forget();
       }
     }
 
     return nullptr;
   }
 
   static void
@@ -1399,16 +1402,19 @@ void
 Manager::RemoveListener(Listener* aListener)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   // There may not be a listener here in the case where an actor is killed
   // before it can perform any actual async requests on Manager.
   mListeners.RemoveElement(aListener, ListenerEntryListenerComparator());
   MOZ_ASSERT(!mListeners.Contains(aListener,
                                   ListenerEntryListenerComparator()));
+  if (mListeners.IsEmpty() && mContext) {
+    mContext->AllowToClose();
+  }
 }
 
 void
 Manager::RemoveContext(Context* aContext)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(mContext);
   MOZ_ASSERT(mContext == aContext);
@@ -1417,16 +1423,31 @@ Manager::RemoveContext(Context* aContext
   // If we're trying to shutdown, then note that we're done.  This is the
   // delayed case from Manager::Shutdown().
   if (mShuttingDown) {
     Factory::Remove(this);
   }
 }
 
 void
+Manager::Invalidate()
+{
+  NS_ASSERT_OWNINGTHREAD(Manager);
+  // QuotaManager can trigger this more than once.
+  mValid = false;
+}
+
+bool
+Manager::IsValid() const
+{
+  NS_ASSERT_OWNINGTHREAD(Manager);
+  return mValid;
+}
+
+void
 Manager::AddRefCacheId(CacheId aCacheId)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   for (uint32_t i = 0; i < mCacheIdRefs.Length(); ++i) {
     if (mCacheIdRefs[i].mCacheId == aCacheId) {
       mCacheIdRefs[i].mCount += 1;
       return;
     }
@@ -1445,17 +1466,17 @@ Manager::ReleaseCacheId(CacheId aCacheId
     if (mCacheIdRefs[i].mCacheId == aCacheId) {
       DebugOnly<uint32_t> oldRef = mCacheIdRefs[i].mCount;
       mCacheIdRefs[i].mCount -= 1;
       MOZ_ASSERT(mCacheIdRefs[i].mCount < oldRef);
       if (mCacheIdRefs[i].mCount == 0) {
         bool orphaned = mCacheIdRefs[i].mOrphaned;
         mCacheIdRefs.RemoveElementAt(i);
         // TODO: note that we need to check this cache for staleness on startup (bug 1110446)
-        if (orphaned && !mShuttingDown) {
+        if (orphaned && !mShuttingDown && mValid) {
           nsRefPtr<Context> context = CurrentContext();
           context->CancelForCacheId(aCacheId);
           nsRefPtr<Action> action = new DeleteOrphanedCacheAction(this,
                                                                   aCacheId);
           context->Dispatch(mIOThread, action);
         }
       }
       return;
@@ -1488,17 +1509,17 @@ Manager::ReleaseBodyId(const nsID& aBody
     if (mBodyIdRefs[i].mBodyId == aBodyId) {
       DebugOnly<uint32_t> oldRef = mBodyIdRefs[i].mCount;
       mBodyIdRefs[i].mCount -= 1;
       MOZ_ASSERT(mBodyIdRefs[i].mCount < oldRef);
       if (mBodyIdRefs[i].mCount < 1) {
         bool orphaned = mBodyIdRefs[i].mOrphaned;
         mBodyIdRefs.RemoveElementAt(i);
         // TODO: note that we need to check this body for staleness on startup (bug 1110446)
-        if (orphaned && !mShuttingDown) {
+        if (orphaned && !mShuttingDown && mValid) {
           nsRefPtr<Action> action = new DeleteOrphanedBodyAction(aBodyId);
           nsRefPtr<Context> context = CurrentContext();
           context->Dispatch(mIOThread, action);
         }
       }
       return;
     }
   }
@@ -1530,19 +1551,18 @@ Manager::RemoveStreamList(StreamList* aS
 
 void
 Manager::CacheMatch(Listener* aListener, RequestId aRequestId, CacheId aCacheId,
                     const PCacheRequest& aRequest,
                     const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCacheMatch(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
-                            nullptr, nullptr);
+  if (mShuttingDown || !mValid) {
+    aListener->OnCacheMatch(aRequestId, NS_ERROR_FAILURE, nullptr, nullptr);
     return;
   }
   nsRefPtr<Context> context = CurrentContext();
   nsRefPtr<StreamList> streamList = new StreamList(this, context);
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CacheMatchAction(this, listenerId, aRequestId,
                                                  aCacheId, aRequest, aParams,
                                                  streamList);
@@ -1551,18 +1571,18 @@ Manager::CacheMatch(Listener* aListener,
 
 void
 Manager::CacheMatchAll(Listener* aListener, RequestId aRequestId,
                        CacheId aCacheId, const PCacheRequestOrVoid& aRequest,
                        const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCacheMatchAll(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnCacheMatchAll(aRequestId, NS_ERROR_FAILURE,
                                nsTArray<SavedResponse>(), nullptr);
     return;
   }
   nsRefPtr<Context> context = CurrentContext();
   nsRefPtr<StreamList> streamList = new StreamList(this, context);
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CacheMatchAllAction(this, listenerId, aRequestId,
                                                     aCacheId, aRequest, aParams,
@@ -1573,18 +1593,18 @@ Manager::CacheMatchAll(Listener* aListen
 void
 Manager::CachePutAll(Listener* aListener, RequestId aRequestId, CacheId aCacheId,
                      const nsTArray<CacheRequestResponse>& aPutList,
                      const nsTArray<nsCOMPtr<nsIInputStream>>& aRequestStreamList,
                      const nsTArray<nsCOMPtr<nsIInputStream>>& aResponseStreamList)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCachePutAll(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+  if (mShuttingDown || !mValid) {
+    aListener->OnCachePutAll(aRequestId, NS_ERROR_FAILURE);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CachePutAllAction(this, listenerId, aRequestId,
                                                   aCacheId, aPutList,
                                                   aRequestStreamList,
                                                   aResponseStreamList);
   nsRefPtr<Context> context = CurrentContext();
@@ -1593,36 +1613,36 @@ Manager::CachePutAll(Listener* aListener
 
 void
 Manager::CacheDelete(Listener* aListener, RequestId aRequestId,
                      CacheId aCacheId, const PCacheRequest& aRequest,
                      const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCacheDelete(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN, false);
+  if (mShuttingDown || !mValid) {
+    aListener->OnCacheDelete(aRequestId, NS_ERROR_FAILURE, false);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CacheDeleteAction(this, listenerId, aRequestId,
                                                   aCacheId, aRequest, aParams);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 void
 Manager::CacheKeys(Listener* aListener, RequestId aRequestId,
                    CacheId aCacheId, const PCacheRequestOrVoid& aRequestOrVoid,
                    const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCacheKeys(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnCacheKeys(aRequestId, NS_ERROR_FAILURE,
                            nsTArray<SavedRequest>(), nullptr);
     return;
   }
   nsRefPtr<Context> context = CurrentContext();
   nsRefPtr<StreamList> streamList = new StreamList(this, context);
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CacheKeysAction(this, listenerId, aRequestId,
                                                 aCacheId, aRequestOrVoid,
@@ -1632,18 +1652,18 @@ Manager::CacheKeys(Listener* aListener, 
 
 void
 Manager::StorageMatch(Listener* aListener, RequestId aRequestId,
                       Namespace aNamespace, const PCacheRequest& aRequest,
                       const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageMatch(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageMatch(aRequestId, NS_ERROR_FAILURE,
                               nullptr, nullptr);
     return;
   }
   nsRefPtr<Context> context = CurrentContext();
   nsRefPtr<StreamList> streamList = new StreamList(this, context);
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageMatchAction(this, listenerId, aRequestId,
                                                    aNamespace, aRequest,
@@ -1652,86 +1672,87 @@ Manager::StorageMatch(Listener* aListene
 }
 
 void
 Manager::StorageHas(Listener* aListener, RequestId aRequestId,
                     Namespace aNamespace, const nsAString& aKey)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageHas(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageHas(aRequestId, NS_ERROR_FAILURE,
                             false);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageHasAction(this, listenerId, aRequestId,
                                                  aNamespace, aKey);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 void
 Manager::StorageOpen(Listener* aListener, RequestId aRequestId,
                      Namespace aNamespace, const nsAString& aKey)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageOpen(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN, 0);
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageOpen(aRequestId, NS_ERROR_FAILURE, 0);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageOpenAction(this, listenerId, aRequestId,
                                                   aNamespace, aKey);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 void
 Manager::StorageDelete(Listener* aListener, RequestId aRequestId,
                        Namespace aNamespace, const nsAString& aKey)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageDelete(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageDelete(aRequestId, NS_ERROR_FAILURE,
                                false);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageDeleteAction(this, listenerId, aRequestId,
                                                     aNamespace, aKey);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 void
 Manager::StorageKeys(Listener* aListener, RequestId aRequestId,
                      Namespace aNamespace)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageKeys(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageKeys(aRequestId, NS_ERROR_FAILURE,
                              nsTArray<nsString>());
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageKeysAction(this, listenerId, aRequestId,
                                                   aNamespace);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 Manager::Manager(ManagerId* aManagerId, nsIThread* aIOThread)
   : mManagerId(aManagerId)
   , mIOThread(aIOThread)
   , mContext(nullptr)
   , mShuttingDown(false)
+  , mValid(true)
 {
   MOZ_ASSERT(mManagerId);
   MOZ_ASSERT(mIOThread);
 }
 
 Manager::~Manager()
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
@@ -1760,21 +1781,16 @@ Manager::Shutdown()
     return;
   }
 
   // Set a flag to prevent any new requests from coming in and creating
   // a new Context.  We must ensure all Contexts and IO operations are
   // complete before shutdown proceeds.
   mShuttingDown = true;
 
-  for (uint32_t i = 0; i < mStreamLists.Length(); ++i) {
-    nsRefPtr<StreamList> streamList = mStreamLists[i];
-    streamList->CloseAll();
-  }
-
   // If there is a context, then we must wait for it to complete.  Cancel and
   // only note that we are done after its cleaned up.
   if (mContext) {
     nsRefPtr<Context> context = mContext;
     context->CancelAll();
     return;
   }
 
@@ -1784,16 +1800,17 @@ Manager::Shutdown()
 
 already_AddRefed<Context>
 Manager::CurrentContext()
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   nsRefPtr<Context> ref = mContext;
   if (!ref) {
     MOZ_ASSERT(!mShuttingDown);
+    MOZ_ASSERT(mValid);
     nsRefPtr<Action> setupAction = new SetupAction();
     ref = Context::Create(this, setupAction);
     mContext = ref;
   }
   return ref.forget();
 }
 
 Manager::ListenerId
--- a/dom/cache/Manager.h
+++ b/dom/cache/Manager.h
@@ -120,16 +120,21 @@ public:
   static void ShutdownAllOnMainThread();
 
   // Must be called by Listener objects before they are destroyed.
   void RemoveListener(Listener* aListener);
 
   // Must be called by Context objects before they are destroyed.
   void RemoveContext(Context* aContext);
 
+  // Marks the Manager "invalid".  Once the Context completes no new operations
+  // will be permitted with this Manager.  New actors will get a new Manager.
+  void Invalidate();
+  bool IsValid() const;
+
   // If an actor represents a long term reference to a cache or body stream,
   // then they must call AddRefCacheId() or AddRefBodyId().  This will
   // cause the Manager to keep the backing data store alive for the given
   // object.  The actor must then call ReleaseCacheId() or ReleaseBodyId()
   // exactly once for every AddRef*() call it made.  Any delayed deletion
   // will then be performed.
   void AddRefCacheId(CacheId aCacheId);
   void ReleaseCacheId(CacheId aCacheId);
@@ -209,18 +214,18 @@ private:
 
   // Weak reference cleared by RemoveContext() in Context destructor.
   Context* MOZ_NON_OWNING_REF mContext;
 
   // Weak references cleared by RemoveListener() in Listener destructors.
   struct ListenerEntry
   {
     ListenerEntry()
-      : mId(UINT64_MAX),
-      mListener(nullptr)
+      : mId(UINT64_MAX)
+      , mListener(nullptr)
     {
     }
 
     ListenerEntry(ListenerId aId, Listener* aListener)
       : mId(aId)
       , mListener(aListener)
     {
     }
@@ -250,16 +255,17 @@ private:
   typedef nsTArray<ListenerEntry> ListenerList;
   ListenerList mListeners;
   static ListenerId sNextListenerId;
 
   // Weak references cleared by RemoveStreamList() in StreamList destructors.
   nsTArray<StreamList*> mStreamLists;
 
   bool mShuttingDown;
+  bool mValid;
 
   struct CacheIdRefCounter
   {
     CacheId mCacheId;
     MozRefCountType mCount;
     bool mOrphaned;
   };
   nsTArray<CacheIdRefCounter> mCacheIdRefs;
new file mode 100644
--- /dev/null
+++ b/dom/cache/OfflineStorage.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/dom/cache/OfflineStorage.h"
+
+#include "mozilla/dom/cache/Context.h"
+#include "mozilla/dom/cache/QuotaClient.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace dom {
+namespace cache {
+
+using mozilla::dom::quota::Client;
+using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
+using mozilla::dom::quota::QuotaManager;
+
+NS_IMPL_ISUPPORTS(OfflineStorage, nsIOfflineStorage);
+
+// static
+already_AddRefed<OfflineStorage>
+OfflineStorage::Register(Context::ThreadsafeHandle* aContext,
+                         const QuotaInfo& aQuotaInfo)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  QuotaManager* qm = QuotaManager::Get();
+  if (NS_WARN_IF(!qm)) {
+    return nullptr;
+  }
+
+  nsRefPtr<Client> client = qm->GetClient(Client::DOMCACHE);
+
+  nsRefPtr<OfflineStorage> storage =
+    new OfflineStorage(aContext, aQuotaInfo, client);
+
+  if (NS_WARN_IF(!qm->RegisterStorage(storage))) {
+    return nullptr;
+  }
+
+  return storage.forget();
+}
+
+void
+OfflineStorage::AddDestroyCallback(nsIRunnable* aCallback)
+{
+  MOZ_ASSERT(aCallback);
+  MOZ_ASSERT(!mDestroyCallbacks.Contains(aCallback));
+  mDestroyCallbacks.AppendElement(aCallback);
+}
+
+OfflineStorage::OfflineStorage(Context::ThreadsafeHandle* aContext,
+                               const QuotaInfo& aQuotaInfo,
+                               Client* aClient)
+  : mContext(aContext)
+  , mQuotaInfo(aQuotaInfo)
+  , mClient(aClient)
+{
+  MOZ_ASSERT(mContext);
+  MOZ_ASSERT(mClient);
+
+  mPersistenceType = PERSISTENCE_TYPE_DEFAULT;
+  mGroup = mQuotaInfo.mGroup;
+}
+
+OfflineStorage::~OfflineStorage()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  QuotaManager* qm = QuotaManager::Get();
+  MOZ_ASSERT(qm);
+  qm->UnregisterStorage(this);
+  for (uint32_t i = 0; i < mDestroyCallbacks.Length(); ++i) {
+    mDestroyCallbacks[i]->Run();
+  }
+}
+
+NS_IMETHODIMP_(const nsACString&)
+OfflineStorage::Id()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  return mQuotaInfo.mStorageId;
+}
+
+NS_IMETHODIMP_(Client*)
+OfflineStorage::GetClient()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  return mClient;
+}
+
+NS_IMETHODIMP_(bool)
+OfflineStorage::IsOwnedByProcess(ContentParent* aOwner)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  // The Cache and Context can be shared by multiple client processes.  They
+  // are not exclusively owned by a single process.
+  //
+  // As far as I can tell this is used by QuotaManager to shutdown storages
+  // when a particular process goes away.  We definitely don't want this
+  // since we are shared.  Also, the Cache actor code already properly
+  // handles asynchronous actor destruction when the child process dies.
+  //
+  // Therefore, always return false here.
+  return false;
+}
+
+NS_IMETHODIMP_(const nsACString&)
+OfflineStorage::Origin()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  return mQuotaInfo.mOrigin;
+}
+
+NS_IMETHODIMP_(nsresult)
+OfflineStorage::Close()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mContext->AllowToClose();
+  return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+OfflineStorage::Invalidate()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mContext->InvalidateAndAllowToClose();
+}
+
+} // namespace cache
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/cache/OfflineStorage.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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_dom_cache_QuotaOfflineStorage_h
+#define mozilla_dom_cache_QuotaOfflineStorage_h
+
+#include "nsISupportsImpl.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/cache/Context.h"
+#include "nsIOfflineStorage.h"
+#include "nsTArray.h"
+
+class nsIThread;
+
+namespace mozilla {
+namespace dom {
+namespace cache {
+
+class OfflineStorage final : public nsIOfflineStorage
+{
+public:
+  static already_AddRefed<OfflineStorage>
+  Register(Context::ThreadsafeHandle* aContext, const QuotaInfo& aQuotaInfo);
+
+  void
+  AddDestroyCallback(nsIRunnable* aCallback);
+
+private:
+  OfflineStorage(Context::ThreadsafeHandle* aContext,
+                 const QuotaInfo& aQuotaInfo,
+                 Client* aClient);
+  ~OfflineStorage();
+
+  nsRefPtr<Context::ThreadsafeHandle> mContext;
+  const QuotaInfo mQuotaInfo;
+  nsRefPtr<Client> mClient;
+  nsTArray<nsCOMPtr<nsIRunnable>> mDestroyCallbacks;
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOFFLINESTORAGE
+};
+
+} // namespace cache
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_cache_QuotaOfflineStorage_h
--- a/dom/cache/QuotaClient.cpp
+++ b/dom/cache/QuotaClient.cpp
@@ -3,26 +3,28 @@
 /* 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 "mozilla/dom/cache/QuotaClient.h"
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/dom/cache/Manager.h"
+#include "mozilla/dom/cache/OfflineStorage.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "mozilla/dom/quota/UsageInfo.h"
 #include "nsIFile.h"
 #include "nsISimpleEnumerator.h"
 #include "nsThreadUtils.h"
 
 namespace {
 
 using mozilla::DebugOnly;
 using mozilla::dom::cache::Manager;
+using mozilla::dom::cache::OfflineStorage;
 using mozilla::dom::quota::Client;
 using mozilla::dom::quota::PersistenceType;
 using mozilla::dom::quota::QuotaManager;
 using mozilla::dom::quota::UsageInfo;
 
 static nsresult
 GetBodyUsage(nsIFile* aDir, UsageInfo* aUsageInfo)
 {
@@ -55,16 +57,51 @@ GetBodyUsage(nsIFile* aDir, UsageInfo* a
     MOZ_ASSERT(fileSize >= 0);
 
     aUsageInfo->AppendToFileUsage(fileSize);
   }
 
   return NS_OK;
 }
 
+class StoragesDestroyedRunnable final : public nsRunnable
+{
+  uint32_t mExpectedCalls;
+  nsCOMPtr<nsIRunnable> mCallback;
+
+public:
+  StoragesDestroyedRunnable(uint32_t aExpectedCalls, nsIRunnable* aCallback)
+    : mExpectedCalls(aExpectedCalls)
+    , mCallback(aCallback)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mExpectedCalls);
+    MOZ_ASSERT(mCallback);
+  }
+
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mExpectedCalls);
+    mExpectedCalls -= 1;
+    if (!mExpectedCalls) {
+      mCallback->Run();
+    }
+    return NS_OK;
+  }
+
+private:
+  ~StoragesDestroyedRunnable()
+  {
+    // This is a callback runnable and not used for thread dispatch.  It should
+    // always be destroyed on the main thread.
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+};
+
 class CacheQuotaClient final : public Client
 {
 public:
   virtual Type
   GetType() override
   {
     return DOMCACHE;
   }
@@ -146,59 +183,73 @@ public:
 
     return NS_OK;
   }
 
   virtual void
   OnOriginClearCompleted(PersistenceType aPersistenceType,
                          const nsACString& aOrigin) override
   {
-    // nothing to do
+    // Nothing to do here.
   }
 
   virtual void
   ReleaseIOThreadObjects() override
   {
-    // nothing to do
+    // Nothing to do here as the Context handles cleaning everything up
+    // automatically.
   }
 
   virtual bool
   IsFileServiceUtilized() override
   {
     return false;
   }
 
   virtual bool
   IsTransactionServiceActivated() override
   {
-    // TODO: implement nsIOfflineStorage interface (bug 1110487)
-    return false;
+    return true;
   }
 
   virtual void
   WaitForStoragesToComplete(nsTArray<nsIOfflineStorage*>& aStorages,
                             nsIRunnable* aCallback) override
   {
-    // TODO: implement nsIOfflineStorage interface (bug 1110487)
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(!aStorages.IsEmpty());
+
+    nsCOMPtr<nsIRunnable> callback =
+      new StoragesDestroyedRunnable(aStorages.Length(), aCallback);
+
+    for (uint32_t i = 0; i < aStorages.Length(); ++i) {
+      MOZ_ASSERT(aStorages[i]->GetClient());
+      MOZ_ASSERT(aStorages[i]->GetClient()->GetType() == Client::DOMCACHE);
+      nsRefPtr<OfflineStorage> storage =
+        static_cast<OfflineStorage*>(aStorages[i]);
+      storage->AddDestroyCallback(callback);
+    }
   }
 
 
   virtual void
   ShutdownTransactionService() override
   {
     MOZ_ASSERT(NS_IsMainThread());
 
     // spins the event loop and synchronously shuts down all Managers
     Manager::ShutdownAllOnMainThread();
   }
 
 private:
-  ~CacheQuotaClient() { }
+  ~CacheQuotaClient()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
 
-public:
   NS_INLINE_DECL_REFCOUNTING(CacheQuotaClient, override)
 };
 
 } // anonymous namespace;
 
 namespace mozilla {
 namespace dom {
 namespace cache {
--- a/dom/cache/QuotaClient.h
+++ b/dom/cache/QuotaClient.h
@@ -9,15 +9,16 @@
 
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/quota/Client.h"
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
-already_AddRefed<quota::Client> CreateQuotaClient();
+already_AddRefed<quota::Client>
+CreateQuotaClient();
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_cache_QuotaClient_h
--- a/dom/cache/StreamList.cpp
+++ b/dom/cache/StreamList.cpp
@@ -18,17 +18,17 @@ namespace cache {
 StreamList::StreamList(Manager* aManager, Context* aContext)
   : mManager(aManager)
   , mContext(aContext)
   , mCacheId(0)
   , mStreamControl(nullptr)
   , mActivated(false)
 {
   MOZ_ASSERT(mManager);
-  MOZ_ASSERT(mContext);
+  mContext->AddActivity(this);
 }
 
 void
 StreamList::SetStreamControl(CacheStreamControlParent* aStreamControl)
 {
   NS_ASSERT_OWNINGTHREAD(StreamList);
   MOZ_ASSERT(aStreamControl);
 
@@ -137,24 +137,39 @@ void
 StreamList::CloseAll()
 {
   NS_ASSERT_OWNINGTHREAD(StreamList);
   if (mStreamControl) {
     mStreamControl->CloseAll();
   }
 }
 
+void
+StreamList::Cancel()
+{
+  NS_ASSERT_OWNINGTHREAD(StreamList);
+  CloseAll();
+}
+
+bool
+StreamList::MatchesCacheId(CacheId aCacheId) const
+{
+  NS_ASSERT_OWNINGTHREAD(StreamList);
+  return aCacheId == mCacheId;
+}
+
 StreamList::~StreamList()
 {
   NS_ASSERT_OWNINGTHREAD(StreamList);
   MOZ_ASSERT(!mStreamControl);
   if (mActivated) {
     mManager->RemoveStreamList(this);
     for (uint32_t i = 0; i < mList.Length(); ++i) {
       mManager->ReleaseBodyId(mList[i].mId);
     }
     mManager->ReleaseCacheId(mCacheId);
   }
+  mContext->RemoveActivity(this);
 }
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
--- a/dom/cache/StreamList.h
+++ b/dom/cache/StreamList.h
@@ -2,31 +2,31 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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_dom_cache_StreamList_h
 #define mozilla_dom_cache_StreamList_h
 
+#include "mozilla/dom/cache/Context.h"
 #include "mozilla/dom/cache/Types.h"
 #include "nsRefPtr.h"
 #include "nsTArray.h"
 
 class nsIInputStream;
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
 class CacheStreamControlParent;
-class Context;
 class Manager;
 
-class StreamList final
+class StreamList final : public Context::Activity
 {
 public:
   StreamList(Manager* aManager, Context* aContext);
 
   void SetStreamControl(CacheStreamControlParent* aStreamControl);
   void RemoveStreamControl(CacheStreamControlParent* aStreamControl);
 
   void Activate(CacheId aCacheId);
@@ -34,16 +34,20 @@ public:
   void Add(const nsID& aId, nsIInputStream* aStream);
   already_AddRefed<nsIInputStream> Extract(const nsID& aId);
 
   void NoteClosed(const nsID& aId);
   void NoteClosedAll();
   void Close(const nsID& aId);
   void CloseAll();
 
+  // Context::Activity methods
+  virtual void Cancel() override;
+  virtual bool MatchesCacheId(CacheId aCacheId) const override;
+
 private:
   ~StreamList();
   struct Entry
   {
     nsID mId;
     nsCOMPtr<nsIInputStream> mStream;
   };
   nsRefPtr<Manager> mManager;
--- a/dom/cache/Types.h
+++ b/dom/cache/Types.h
@@ -29,16 +29,17 @@ static const RequestId INVALID_REQUEST_I
 typedef int32_t CacheId;
 
 struct QuotaInfo
 {
   QuotaInfo() : mIsApp(false) { }
   nsCOMPtr<nsIFile> mDir;
   nsCString mGroup;
   nsCString mOrigin;
+  nsCString mStorageId;
   bool mIsApp;
 };
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_cache_Types_h
--- a/dom/cache/moz.build
+++ b/dom/cache/moz.build
@@ -23,16 +23,17 @@ EXPORTS.mozilla.dom.cache += [
     'DBAction.h',
     'DBSchema.h',
     'Feature.h',
     'FetchPut.h',
     'FileUtils.h',
     'IPCUtils.h',
     'Manager.h',
     'ManagerId.h',
+    'OfflineStorage.h',
     'PrincipalVerifier.h',
     'QuotaClient.h',
     'ReadStream.h',
     'SavedTypes.h',
     'StreamControl.h',
     'StreamList.h',
     'StreamUtils.h',
     'Types.h',
@@ -56,16 +57,17 @@ UNIFIED_SOURCES += [
     'Context.cpp',
     'DBAction.cpp',
     'DBSchema.cpp',
     'Feature.cpp',
     'FetchPut.cpp',
     'FileUtils.cpp',
     'Manager.cpp',
     'ManagerId.cpp',
+    'OfflineStorage.cpp',
     'PrincipalVerifier.cpp',
     'QuotaClient.cpp',
     'ReadStream.cpp',
     'StreamControl.cpp',
     'StreamList.cpp',
     'StreamUtils.cpp',
     'TypeUtils.cpp',
 ]
--- a/dom/cache/test/mochitest/driver.js
+++ b/dom/cache/test/mochitest/driver.js
@@ -25,16 +25,32 @@ function runTests(testFile, order) {
                 ["dom.serviceWorkers.testing.enabled", true],
                 ["dom.serviceWorkers.exemptFromPerDomainMax", true]]
       }, function() {
         resolve();
       });
     });
   }
 
+  // adapted from dom/indexedDB/test/helpers.js
+  function clearStorage() {
+    return new Promise(function(resolve, reject) {
+      var principal = SpecialPowers.wrap(document).nodePrincipal;
+      var appId, inBrowser;
+      var nsIPrincipal = SpecialPowers.Components.interfaces.nsIPrincipal;
+      if (principal.appId != nsIPrincipal.UNKNOWN_APP_ID &&
+          principal.appId != nsIPrincipal.NO_APP_ID) {
+        appId = principal.appId;
+        inBrowser = principal.isInBrowserElement;
+      }
+      SpecialPowers.clearStorageForURI(document.documentURI, resolve, appId,
+                                       inBrowser);
+    });
+  }
+
   function loadScript(script) {
     return new Promise(function(resolve, reject) {
       var s = document.createElement("script");
       s.src = script;
       s.onerror = reject;
       s.onload = resolve;
       document.body.appendChild(s);
     });
@@ -95,22 +111,26 @@ function runTests(testFile, order) {
           info("Running tests in parallel mode");
           return runTests(testFile, "parallel");
         });
   }
   if (order == "sequential") {
     return setupPrefs()
         .then(importDrivers)
         .then(runWorkerTest)
+        .then(clearStorage)
         .then(runServiceWorkerTest)
+        .then(clearStorage)
         .then(runFrameTest)
+        .then(clearStorage)
         .catch(function(e) {
           ok(false, "A promise was rejected during test execution: " + e);
         });
   }
   return setupPrefs()
       .then(importDrivers)
       .then(() => Promise.all([runWorkerTest(), runServiceWorkerTest(), runFrameTest()]))
+      .then(clearStorage)
       .catch(function(e) {
         ok(false, "A promise was rejected during test execution: " + e);
       });
 }