Bug 1182987 - Part 5: Change mode of "readwrite" transaction to "cleanup" after QuotaExceeded is fired; r=baku
authorJan Varga <jan.varga@gmail.com>
Thu, 21 Apr 2016 06:53:30 +0200
changeset 332116 72023950385695172b8c84e3d804adccfe14f843
parent 332115 583cdf2d877c00526826108c2a18eb4079200280
child 332117 3ad1be09eddb899b0f6a662814942e466f4a20ce
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1182987
milestone48.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 1182987 - Part 5: Change mode of "readwrite" transaction to "cleanup" after QuotaExceeded is fired; r=baku
dom/indexedDB/IDBDatabase.cpp
dom/indexedDB/IDBDatabase.h
dom/indexedDB/IDBTransaction.cpp
dom/indexedDB/test/unit/test_cleanup_transaction.js
dom/indexedDB/test/unit/test_quotaExceeded_recovery.js
dom/indexedDB/test/unit/xpcshell-parent-process.ini
--- a/dom/indexedDB/IDBDatabase.cpp
+++ b/dom/indexedDB/IDBDatabase.cpp
@@ -170,16 +170,17 @@ IDBDatabase::IDBDatabase(IDBOpenDBReques
                          DatabaseSpec* aSpec)
   : IDBWrapperCache(aRequest)
   , mFactory(aFactory)
   , mSpec(aSpec)
   , mBackgroundActor(aActor)
   , mFileHandleDisabled(aRequest->IsFileHandleDisabled())
   , mClosed(false)
   , mInvalidated(false)
+  , mQuotaExceeded(false)
 {
   MOZ_ASSERT(aRequest);
   MOZ_ASSERT(aFactory);
   aFactory->AssertIsOnOwningThread();
   MOZ_ASSERT(aActor);
   MOZ_ASSERT(aSpec);
 }
 
@@ -660,23 +661,29 @@ IDBDatabase::Transaction(JSContext* aCx,
   }
 
   IDBTransaction::Mode mode;
   switch (aMode) {
     case IDBTransactionMode::Readonly:
       mode = IDBTransaction::READ_ONLY;
       break;
     case IDBTransactionMode::Readwrite:
-      mode = IDBTransaction::READ_WRITE;
+      if (mQuotaExceeded) {
+        mode = IDBTransaction::CLEANUP;
+        mQuotaExceeded = false;
+      } else {
+        mode = IDBTransaction::READ_WRITE;
+      }
       break;
     case IDBTransactionMode::Readwriteflush:
       mode = IDBTransaction::READ_WRITE_FLUSH;
       break;
     case IDBTransactionMode::Cleanup:
       mode = IDBTransaction::CLEANUP;
+      mQuotaExceeded = false;
       break;
     case IDBTransactionMode::Versionchange:
       return NS_ERROR_DOM_INVALID_ACCESS_ERR;
 
     default:
       MOZ_CRASH("Unknown mode!");
   }
 
@@ -700,17 +707,17 @@ IDBDatabase::Transaction(JSContext* aCx,
 
   MOZ_ALWAYS_TRUE(
     mBackgroundActor->SendPBackgroundIDBTransactionConstructor(actor,
                                                                sortedStoreNames,
                                                                mode));
 
   transaction->SetBackgroundActor(actor);
 
-  if (aMode == IDBTransactionMode::Cleanup) {
+  if (mode == IDBTransaction::CLEANUP) {
     ExpireFileActors(/* aExpireAll */ true);
   }
 
   transaction.forget(aTransaction);
   return NS_OK;
 }
 
 StorageType
--- a/dom/indexedDB/IDBDatabase.h
+++ b/dom/indexedDB/IDBDatabase.h
@@ -78,16 +78,17 @@ class IDBDatabase final
   RefPtr<Observer> mObserver;
 
   // Weak refs, IDBMutableFile strongly owns this IDBDatabase object.
   nsTArray<IDBMutableFile*> mLiveMutableFiles;
 
   const bool mFileHandleDisabled;
   bool mClosed;
   bool mInvalidated;
+  bool mQuotaExceeded;
 
 public:
   static already_AddRefed<IDBDatabase>
   Create(IDBOpenDBRequest* aRequest,
          IDBFactory* aFactory,
          indexedDB::BackgroundDatabaseChild* aActor,
          DatabaseSpec* aSpec);
 
@@ -145,16 +146,22 @@ public:
   IsInvalidated() const
   {
     AssertIsOnOwningThread();
 
     return mInvalidated;
   }
 
   void
+  SetQuotaExceeded()
+  {
+    mQuotaExceeded = true;
+  }
+
+  void
   EnterSetVersionTransaction(uint64_t aNewVersion);
 
   void
   ExitSetVersionTransaction();
 
   // Called when a versionchange transaction is aborted to reset the
   // DatabaseInfo.
   void
--- a/dom/indexedDB/IDBTransaction.cpp
+++ b/dom/indexedDB/IDBTransaction.cpp
@@ -759,16 +759,20 @@ IDBTransaction::FireCompleteOrAbortEvent
   nsCOMPtr<nsIDOMEvent> event;
   if (NS_SUCCEEDED(aResult)) {
     event = CreateGenericEvent(this,
                                nsDependentString(kCompleteEventType),
                                eDoesNotBubble,
                                eNotCancelable);
     MOZ_ASSERT(event);
   } else {
+    if (aResult == NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR) {
+      mDatabase->SetQuotaExceeded();
+    }
+
     if (!mError && !mAbortedByScript) {
       mError = new DOMError(GetOwner(), aResult);
     }
 
     event = CreateGenericEvent(this,
                                nsDependentString(kAbortEventType),
                                eDoesBubble,
                                eNotCancelable);
copy from dom/indexedDB/test/unit/test_quotaExceeded_recovery.js
copy to dom/indexedDB/test/unit/test_cleanup_transaction.js
--- a/dom/indexedDB/test/unit/test_quotaExceeded_recovery.js
+++ b/dom/indexedDB/test/unit/test_cleanup_transaction.js
@@ -9,17 +9,17 @@ var testGenerator = testSteps();
 
 function testSteps()
 {
   const spec = "http://foo.com";
   const name =
     this.window ? window.location.pathname : "test_quotaExceeded_recovery";
   const objectStoreName = "foo";
 
-  // We want 32 MB database file, but there's the group limit so we need to
+  // We want 32 MB database, but there's the group limit so we need to
   // multiply by 5.
   const tempStorageLimitKB = 32 * 1024 * 5;
 
   // Store in 1 MB chunks.
   const dataSize = 1024 * 1024;
 
   for (let blobs of [false, true]) {
     setTemporaryStorageLimit(tempStorageLimitKB);
@@ -79,33 +79,35 @@ function testSteps()
         j++;
         testGenerator.send(true);
       }
       trans.onabort = function(event) {
         is(trans.error.name, "QuotaExceededError", "Reached quota limit");
         testGenerator.send(false);
       }
 
-      let shouldContinue = yield undefined;
-      if (shouldContinue) {
+      let completeFired = yield undefined;
+      if (completeFired) {
         ok(true, "Got complete event");
       } else {
         ok(true, "Got abort event");
 
-        if (j==1) {
+        if (j == 1) {
+          // Plain cleanup transaction (just vacuuming and checkpointing)
+          // couldn't shrink database any further.
           break;
-        } else {
-          j = 1;
+        }
 
-          trans = db.transaction(objectStoreName, "cleanup");
-          trans.onabort = unexpectedSuccessHandler;;
-          trans.oncomplete = grabEventAndContinueHandler;
+        j = 1;
 
-          yield undefined;
-        }
+        trans = db.transaction(objectStoreName, "cleanup");
+        trans.onabort = unexpectedSuccessHandler;;
+        trans.oncomplete = grabEventAndContinueHandler;
+
+        yield undefined;
       }
     }
 
     info("Reopening database");
 
     db.close();
 
     request = indexedDB.openForPrincipal(getPrincipal(spec), name);
--- a/dom/indexedDB/test/unit/test_quotaExceeded_recovery.js
+++ b/dom/indexedDB/test/unit/test_quotaExceeded_recovery.js
@@ -9,23 +9,27 @@ var testGenerator = testSteps();
 
 function testSteps()
 {
   const spec = "http://foo.com";
   const name =
     this.window ? window.location.pathname : "test_quotaExceeded_recovery";
   const objectStoreName = "foo";
 
-  // We want 32 MB database file, but there's the group limit so we need to
-  // multiply by 5.
-  const tempStorageLimitKB = 32 * 1024 * 5;
+  // We want 8 MB database on Android and 32 MB database on other platforms.
+  const groupLimitMB = mozinfo.os == "android" ? 8 : 32;
+
+  // The group limit is calculated as 20% of the global temporary storage limit.
+  const tempStorageLimitKB = groupLimitMB * 5 * 1024;
 
   // Store in 1 MB chunks.
   const dataSize = 1024 * 1024;
 
+  const maxIter = 10;
+
   for (let blobs of [false, true]) {
     setTemporaryStorageLimit(tempStorageLimitKB);
 
     clearAllDatabases(continueToNextStepSync);
     yield undefined;
 
     info("Opening database");
 
@@ -37,117 +41,98 @@ function testSteps()
     yield undefined;
 
     // upgradeneeded
     request.onupgradeneeded = unexpectedSuccessHandler;
     request.onsuccess = grabEventAndContinueHandler;
 
     info("Creating objectStore");
 
-    request.result.createObjectStore(objectStoreName);
+    request.result.createObjectStore(objectStoreName, { autoIncrement: true });
 
     yield undefined;
 
     // success
     let db = request.result;
     db.onerror = errorHandler;
 
-    ok(true, "Adding data until quota is reached");
+    ok(true, "Filling database");
 
     let obj = {
       name: "foo"
     }
 
     if (!blobs) {
       obj.data = getRandomView(dataSize);
     }
 
+    let iter = 1;
     let i = 1;
     let j = 1;
     while (true) {
       if (blobs) {
         obj.data = getBlob(getView(dataSize));
       }
 
       let trans = db.transaction(objectStoreName, "readwrite");
-      request = trans.objectStore(objectStoreName).add(obj, i);
+      request = trans.objectStore(objectStoreName).add(obj);
       request.onerror = function(event)
       {
         event.stopPropagation();
       }
 
       trans.oncomplete = function(event) {
-        i++;
+        if (iter == 1) {
+          i++;
+        }
         j++;
         testGenerator.send(true);
       }
       trans.onabort = function(event) {
         is(trans.error.name, "QuotaExceededError", "Reached quota limit");
         testGenerator.send(false);
       }
 
-      let shouldContinue = yield undefined;
-      if (shouldContinue) {
+      let completeFired = yield undefined;
+      if (completeFired) {
         ok(true, "Got complete event");
-      } else {
-        ok(true, "Got abort event");
+        continue;
+      }
+
+      ok(true, "Got abort event");
+
+      if (iter++ == maxIter) {
+        break;
+      }
 
-        if (j==1) {
-          break;
-        } else {
-          j = 1;
+      if (iter > 1) {
+        ok(i == j, "Recycled entire database");
+        j = 1;
+      }
+
+      trans = db.transaction(objectStoreName, "readwrite");
 
-          trans = db.transaction(objectStoreName, "cleanup");
-          trans.onabort = unexpectedSuccessHandler;;
-          trans.oncomplete = grabEventAndContinueHandler;
-
-          yield undefined;
+      // Don't use a cursor for deleting stored blobs (Cursors prolong live
+      // of stored files since each record must be fetched from the database
+      // first which creates a memory reference to the stored blob.)
+      if (blobs) {
+        request = trans.objectStore(objectStoreName).clear();
+      } else {
+        request = trans.objectStore(objectStoreName).openCursor();
+        request.onsuccess = function(event) {
+          let cursor = event.target.result;
+          if (cursor) {
+            cursor.delete();
+            cursor.continue();
+          }
         }
       }
-    }
-
-    info("Reopening database");
-
-    db.close();
-
-    request = indexedDB.openForPrincipal(getPrincipal(spec), name);
-    request.onerror = errorHandler;
-    request.onsuccess = grabEventAndContinueHandler;
-
-    yield undefined;
-
-    db = request.result;
-    db.onerror = errorHandler;
-
-    info("Deleting some data")
-
-    let trans = db.transaction(objectStoreName, "cleanup");
-    trans.objectStore(objectStoreName).delete(1);
 
-    trans.onabort = unexpectedSuccessHandler;;
-    trans.oncomplete = grabEventAndContinueHandler;
-
-    yield undefined;
-
-    info("Adding data again")
-
-    trans = db.transaction(objectStoreName, "readwrite");
-    trans.objectStore(objectStoreName).add(obj, 1);
+      trans.onabort = unexpectedSuccessHandler;;
+      trans.oncomplete = grabEventAndContinueHandler;
 
-    trans.onabort = unexpectedSuccessHandler;
-    trans.oncomplete = grabEventAndContinueHandler;
-
-    yield undefined;
-
-    info("Deleting database");
-
-    db.close();
-
-    request = indexedDB.deleteForPrincipal(getPrincipal(spec), name);
-    request.onerror = errorHandler;
-    request.onsuccess = grabEventAndContinueHandler;
-
-    yield undefined;
+      yield undefined;
+    }
   }
 
   finishTest();
   yield undefined;
 }
--- a/dom/indexedDB/test/unit/xpcshell-parent-process.ini
+++ b/dom/indexedDB/test/unit/xpcshell-parent-process.ini
@@ -20,16 +20,17 @@ support-files =
   schema18upgrade_profile.zip
   schema21upgrade_profile.zip
   xpcshell-shared.ini
 
 [include:xpcshell-shared.ini]
 
 [test_blob_file_backed.js]
 [test_bug1056939.js]
+[test_cleanup_transaction.js]
 [test_defaultStorageUpgrade.js]
 [test_globalObjects_ipc.js]
 skip-if = toolkit == 'android'
 [test_idle_maintenance.js]
 [test_invalidate.js]
 # disabled for the moment.
 skip-if = true
 [test_lowDiskSpace.js]