Bug 1151017 - Support the 'close' Event on Databases. r=khuey
authorBevis Tseng <btseng@mozilla.com>
Tue, 31 May 2016 18:08:20 +0800
changeset 346554 035505ea31497d0e07d51cda4711cebb385ca987
parent 346553 28cd5a9882d85758c15f171112c2cbefae9b91d1
child 346555 bb4686084769fedfc0562a62c92343f591401a99
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskhuey
bugs1151017
milestone50.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 1151017 - Support the 'close' Event on Databases. r=khuey Outline of this patch: 1. Define a new ipdl message called |CloseAfterInvalidationComplete| to trigger the close event after all transactions are complete only if the database is invalidated by the user agent. 2. Make sure the following event sequence is consistent during invalidation according to the steps in |5.2. Closing a database| by the following 2 solutions: IDBRequest.onerror -> IDBTransaction.onerror -> IDBTransaction.onabort -> IDBDatabase.onclose. 2.1. In parent process, do not force to abort the transactions after invalidation but wait for all the transactions in its child process are complete. 2.2. In child process, make sure that each IDBTransaction will notify its completion to the parent after all its pending IDBRequests are finished. 3. Add test_database_onclose.js to test the close event especially when read/write operation is ongoing. 4. Add test_database_close_without_onclose.js as a XPCShell test because setTimeout() is not preferred in Mochitest to ensure that the IDBDatabase.onclose event won't be sent after closed normally.
dom/indexedDB/ActorsChild.cpp
dom/indexedDB/ActorsChild.h
dom/indexedDB/ActorsParent.cpp
dom/indexedDB/IDBDatabase.cpp
dom/indexedDB/IDBDatabase.h
dom/indexedDB/IDBEvents.cpp
dom/indexedDB/IDBEvents.h
dom/indexedDB/IDBTransaction.cpp
dom/indexedDB/PBackgroundIDBDatabase.ipdl
dom/indexedDB/test/helpers.js
dom/indexedDB/test/mochitest.ini
dom/indexedDB/test/test_database_onclose.html
dom/indexedDB/test/unit/test_database_close_without_onclose.js
dom/indexedDB/test/unit/test_database_onclose.js
dom/indexedDB/test/unit/xpcshell-parent-process.ini
dom/webidl/IDBDatabase.webidl
testing/web-platform/tests/IndexedDB/interfaces.idl
--- a/dom/indexedDB/ActorsChild.cpp
+++ b/dom/indexedDB/ActorsChild.cpp
@@ -1867,16 +1867,30 @@ BackgroundDatabaseChild::RecvInvalidate(
 
   if (mDatabase) {
     mDatabase->Invalidate();
   }
 
   return true;
 }
 
+bool
+BackgroundDatabaseChild::RecvCloseAfterInvalidationComplete()
+{
+  AssertIsOnOwningThread();
+
+  MaybeCollectGarbageOnIPCMessage();
+
+  if (mDatabase) {
+    mDatabase->DispatchTrustedEvent(nsDependentString(kCloseEventType));
+  }
+
+  return true;
+}
+
 /*******************************************************************************
  * BackgroundDatabaseRequestChild
  ******************************************************************************/
 
 BackgroundDatabaseRequestChild::BackgroundDatabaseRequestChild(
                                                          IDBDatabase* aDatabase,
                                                          IDBRequest* aRequest)
   : BackgroundRequestChildBase(aRequest)
--- a/dom/indexedDB/ActorsChild.h
+++ b/dom/indexedDB/ActorsChild.h
@@ -414,16 +414,19 @@ private:
   virtual bool
   RecvVersionChange(const uint64_t& aOldVersion,
                     const NullableVersion& aNewVersion)
                     override;
 
   virtual bool
   RecvInvalidate() override;
 
+  virtual bool
+  RecvCloseAfterInvalidationComplete() override;
+
   bool
   SendDeleteMe() = delete;
 };
 
 class BackgroundDatabaseRequestChild final
   : public BackgroundRequestChildBase
   , public PBackgroundIDBDatabaseRequestChild
 {
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -13806,16 +13806,24 @@ Database::ConnectionClosedCallback()
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(mClosed);
   MOZ_ASSERT(!mTransactions.Count());
   MOZ_ASSERT(!mActiveMutableFileCount);
 
   mDirectoryLock = nullptr;
 
   CleanupMetadata();
+
+  if (IsInvalidated() && IsActorAlive()) {
+    // Step 3 and 4 of "5.2 Closing a Database":
+    // 1. Wait for all transactions to complete.
+    // 2. Fire a close event if forced flag is set, i.e., IsInvalidated() in our
+    //    implementation.
+    Unused << SendCloseAfterInvalidationComplete();
+  }
 }
 
 void
 Database::CleanupMetadata()
 {
   AssertIsOnBackgroundThread();
 
   if (!mMetadataCleanedUp) {
@@ -14932,17 +14940,17 @@ TransactionBase::Invalidate()
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(mInvalidated == mInvalidatedOnAnyThread);
 
   if (!mInvalidated) {
     mInvalidated = true;
     mInvalidatedOnAnyThread = true;
 
-    Abort(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, /* aForce */ true);
+    Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR, /* aForce */ false);
   }
 }
 
 PBackgroundIDBRequestParent*
 TransactionBase::AllocRequest(const RequestParams& aParams, bool aTrustParams)
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aParams.type() != RequestParams::T__None);
@@ -16181,20 +16189,16 @@ Cursor::Start(const OpenCursorParams& aP
     mType == OpenCursorParams::TObjectStoreOpenCursorParams ?
       aParams.get_ObjectStoreOpenCursorParams().optionalKeyRange() :
     mType == OpenCursorParams::TObjectStoreOpenKeyCursorParams ?
       aParams.get_ObjectStoreOpenKeyCursorParams().optionalKeyRange() :
     mType == OpenCursorParams::TIndexOpenCursorParams ?
       aParams.get_IndexOpenCursorParams().optionalKeyRange() :
       aParams.get_IndexOpenKeyCursorParams().optionalKeyRange();
 
-  if (mTransaction->IsInvalidated()) {
-    return true;
-  }
-
   RefPtr<OpenOp> openOp = new OpenOp(this, optionalKeyRange);
 
   if (NS_WARN_IF(!openOp->Init(mTransaction))) {
     openOp->Cleanup();
     return false;
   }
 
   openOp->DispatchToConnectionPool();
@@ -16344,20 +16348,16 @@ Cursor::RecvContinue(const CursorRequest
     return false;
   }
 
   if (NS_WARN_IF(mTransaction->mCommitOrAbortReceived)) {
     ASSERT_UNLESS_FUZZING();
     return false;
   }
 
-  if (mTransaction->IsInvalidated()) {
-    return true;
-  }
-
   RefPtr<ContinueOp> continueOp = new ContinueOp(this, aParams, aKey);
   if (NS_WARN_IF(!continueOp->Init(mTransaction))) {
     continueOp->Cleanup();
     return false;
   }
 
   continueOp->DispatchToConnectionPool();
   mCurrentlyRunningOp = continueOp;
@@ -22739,22 +22739,19 @@ TransactionDatabaseOperationBase::RunOnC
   MOZ_ASSERT(NS_SUCCEEDED(mResultCode));
 
   PROFILER_LABEL("IndexedDB",
                  "TransactionDatabaseOperationBase::RunOnConnectionThread",
                  js::ProfileEntry::Category::STORAGE);
 
   // There are several cases where we don't actually have to to any work here.
 
-  if (mTransactionIsAborted) {
-    // This transaction is already set to be aborted.
+  if (mTransactionIsAborted || mTransaction->IsInvalidatedOnAnyThread()) {
+    // This transaction is already set to be aborted or invalidated.
     mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR;
-  } else if (mTransaction->IsInvalidatedOnAnyThread()) {
-    // This transaction is being invalidated.
-    mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
   } else if (!OperationMayProceed()) {
     // The operation was canceled in some way, likely because the child process
     // has crashed.
     IDB_REPORT_INTERNAL_ERR();
     mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
   } else {
     Database* database = mTransaction->GetDatabase();
     MOZ_ASSERT(database);
@@ -22815,19 +22812,17 @@ TransactionDatabaseOperationBase::RunOnO
 
   if (NS_WARN_IF(IsActorDestroyed())) {
     // Don't send any notifications if the actor was destroyed already.
     if (NS_SUCCEEDED(mResultCode)) {
       IDB_REPORT_INTERNAL_ERR();
       mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
     }
   } else {
-    if (mTransaction->IsInvalidated()) {
-      mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
-    } else if (mTransaction->IsAborted()) {
+    if (mTransaction->IsInvalidated() || mTransaction->IsAborted()) {
       // Aborted transactions always see their requests fail with ABORT_ERR,
       // even if the request succeeded or failed with another error.
       mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR;
     } else if (NS_SUCCEEDED(mResultCode)) {
       // This may release the IPDL reference.
       mResultCode = SendSuccessResult();
     }
 
@@ -26736,16 +26731,24 @@ CursorOpBase::SendFailureResult(nsresult
   MOZ_ASSERT(NS_FAILED(aResultCode));
   MOZ_ASSERT(mCursor);
   MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this);
   MOZ_ASSERT(!mResponseSent);
 
   if (!IsActorDestroyed()) {
     mResponse = ClampResultCode(aResultCode);
 
+    // This is an expected race when the transaction is invalidated after
+    // data is retrieved from database. We clear the retrieved files to prevent
+    // the assertion failure in SendResponseInternal when mResponse.type() is
+    // CursorResponse::Tnsresult.
+    if (Transaction()->IsInvalidated() && !mFiles.IsEmpty()) {
+      mFiles.Clear();
+    }
+
     mCursor->SendResponseInternal(mResponse, mFiles);
   }
 
 #ifdef DEBUG
   mResponseSent = true;
 #endif
   return false;
 }
--- a/dom/indexedDB/IDBDatabase.cpp
+++ b/dom/indexedDB/IDBDatabase.cpp
@@ -868,17 +868,17 @@ IDBDatabase::AbortTransactions(bool aSho
               transactionsThatNeedWarning.AppendElement(transaction);
               break;
 
             default:
               MOZ_CRASH("Unknown mode!");
           }
         }
 
-        transaction->Abort(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+        transaction->Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR);
       }
 
       static const char kWarningMessage[] =
         "IndexedDBTransactionAbortNavigation";
 
       for (IDBTransaction* transaction : transactionsThatNeedWarning) {
         MOZ_ASSERT(transaction);
 
--- a/dom/indexedDB/IDBDatabase.h
+++ b/dom/indexedDB/IDBDatabase.h
@@ -243,16 +243,17 @@ public:
               const StringOrStringSequence& aStoreNames,
               IDBTransactionMode aMode,
               IDBTransaction** aTransaction);
 
   StorageType
   Storage() const;
 
   IMPL_EVENT_HANDLER(abort)
+  IMPL_EVENT_HANDLER(close)
   IMPL_EVENT_HANDLER(error)
   IMPL_EVENT_HANDLER(versionchange)
 
   already_AddRefed<IDBRequest>
   CreateMutableFile(JSContext* aCx,
                     const nsAString& aName,
                     const Optional<nsAString>& aType,
                     ErrorResult& aRv);
--- a/dom/indexedDB/IDBEvents.cpp
+++ b/dom/indexedDB/IDBEvents.cpp
@@ -21,16 +21,17 @@ namespace indexedDB {
 
 const char16_t* kAbortEventType = MOZ_UTF16("abort");
 const char16_t* kBlockedEventType = MOZ_UTF16("blocked");
 const char16_t* kCompleteEventType = MOZ_UTF16("complete");
 const char16_t* kErrorEventType = MOZ_UTF16("error");
 const char16_t* kSuccessEventType = MOZ_UTF16("success");
 const char16_t* kUpgradeNeededEventType = MOZ_UTF16("upgradeneeded");
 const char16_t* kVersionChangeEventType = MOZ_UTF16("versionchange");
+const char16_t* kCloseEventType = MOZ_UTF16("close");
 
 already_AddRefed<nsIDOMEvent>
 CreateGenericEvent(EventTarget* aOwner,
                    const nsDependentString& aType,
                    Bubbles aBubbles,
                    Cancelable aCancelable)
 {
   RefPtr<Event> event = new Event(aOwner, nullptr, nullptr);
--- a/dom/indexedDB/IDBEvents.h
+++ b/dom/indexedDB/IDBEvents.h
@@ -42,16 +42,17 @@ enum Cancelable {
 
 extern const char16_t* kAbortEventType;
 extern const char16_t* kBlockedEventType;
 extern const char16_t* kCompleteEventType;
 extern const char16_t* kErrorEventType;
 extern const char16_t* kSuccessEventType;
 extern const char16_t* kUpgradeNeededEventType;
 extern const char16_t* kVersionChangeEventType;
+extern const char16_t* kCloseEventType;
 
 already_AddRefed<nsIDOMEvent>
 CreateGenericEvent(EventTarget* aOwner,
                    const nsDependentString& aType,
                    Bubbles aBubbles,
                    Cancelable aCancelable);
 
 } // namespace indexedDB
--- a/dom/indexedDB/IDBTransaction.cpp
+++ b/dom/indexedDB/IDBTransaction.cpp
@@ -371,17 +371,17 @@ IDBTransaction::OnNewRequest()
 void
 IDBTransaction::OnRequestFinished(bool aActorDestroyedNormally)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mPendingRequestCount);
 
   --mPendingRequestCount;
 
-  if (!mPendingRequestCount && !mDatabase->IsInvalidated()) {
+  if (!mPendingRequestCount) {
     mReadyState = COMMITTING;
 
     if (aActorDestroyedNormally) {
       if (NS_SUCCEEDED(mAbortCode)) {
         SendCommit();
       } else {
         SendAbort(mAbortCode);
       }
@@ -636,25 +636,16 @@ IDBTransaction::AbortInternal(nsresult a
   MOZ_ASSERT(!IsCommittingOrDone());
 
   RefPtr<DOMError> error = aError;
 
   const bool isVersionChange = mMode == VERSION_CHANGE;
   const bool isInvalidated = mDatabase->IsInvalidated();
   bool needToSendAbort = mReadyState == INITIAL && !isInvalidated;
 
-  if (isInvalidated) {
-#ifdef DEBUG
-    mSentCommitOrAbort = true;
-#endif
-    // Increment the serial number counter here to account for the aborted
-    // transaction and keep the parent in sync.
-    IDBRequest::NextSerialNumber();
-  }
-
   mAbortCode = aAbortCode;
   mReadyState = DONE;
   mError = error.forget();
 
   if (isVersionChange) {
     // If a version change transaction is aborted, we must revert the world
     // back to its previous state unless we're being invalidated after the
     // transaction already completed.
--- a/dom/indexedDB/PBackgroundIDBDatabase.ipdl
+++ b/dom/indexedDB/PBackgroundIDBDatabase.ipdl
@@ -67,16 +67,18 @@ parent:
 
 child:
   async __delete__();
 
   async VersionChange(uint64_t oldVersion, NullableVersion newVersion);
 
   async Invalidate();
 
+  async CloseAfterInvalidationComplete();
+
   async PBackgroundIDBVersionChangeTransaction(uint64_t currentVersion,
                                                uint64_t requestedVersion,
                                                int64_t nextObjectStoreId,
                                                int64_t nextIndexId);
 
   async PBackgroundMutableFile(nsString name, nsString type);
 };
 
--- a/dom/indexedDB/test/helpers.js
+++ b/dom/indexedDB/test/helpers.js
@@ -152,16 +152,22 @@ function testHarnessSteps() {
           ok(true, "Worker finished");
           nextTestHarnessStep();
           break;
 
         case "expectUncaughtException":
           worker._expectingUncaughtException = message.expecting;
           break;
 
+        case "clearAllDatabases":
+          clearAllDatabases(function(){
+            worker.postMessage({ op: "clearAllDatabasesDone" });
+          });
+          break;
+
         default:
           ok(false,
              "Received a bad message from worker: " + JSON.stringify(message));
           nextTestHarnessStep();
       }
     };
 
     URL.revokeObjectURL(workerScriptURL);
@@ -506,16 +512,22 @@ function workerScript() {
   };
 
   self._expectingUncaughtException = false;
   self.expectUncaughtException = function(_expecting_) {
     self._expectingUncaughtException = !!_expecting_;
     self.postMessage({ op: "expectUncaughtException", expecting: !!_expecting_ });
   };
 
+  self._clearAllDatabasesCallback = undefined;
+  self.clearAllDatabases = function(_callback_) {
+    self._clearAllDatabasesCallback = _callback_;
+    self.postMessage({ op: "clearAllDatabases" });
+  }
+
   self.onerror = function(_message_, _file_, _line_) {
     if (self._expectingUncaughtException) {
       self._expectingUncaughtException = false;
       ok(true, "Worker: expected exception [" + _file_ + ":" + _line_ + "]: '" +
          _message_ + "'");
       return;
     }
     ok(false,
@@ -537,16 +549,23 @@ function workerScript() {
 
       case "start":
         executeSoon(function() {
           info("Worker: starting tests");
           testGenerator.next();
         });
         break;
 
+      case "clearAllDatabasesDone":
+        info("Worker: all databases are cleared");
+        if (self._clearAllDatabasesCallback) {
+          self._clearAllDatabasesCallback();
+        }
+        break;
+
       default:
         throw new Error("Received a bad message from parent: " +
                         JSON.stringify(message));
     }
   };
 
   self.postMessage({ op: "ready" });
 }
--- a/dom/indexedDB/test/mochitest.ini
+++ b/dom/indexedDB/test/mochitest.ini
@@ -31,16 +31,17 @@ support-files =
   unit/test_count.js
   unit/test_create_index.js
   unit/test_create_index_with_integer_keys.js
   unit/test_create_locale_aware_index.js
   unit/test_create_objectStore.js
   unit/test_cursor_mutation.js
   unit/test_cursor_update_updates_indexes.js
   unit/test_cursors.js
+  unit/test_database_onclose.js
   unit/test_deleteDatabase.js
   unit/test_deleteDatabase_interactions.js
   unit/test_deleteDatabase_onblocked.js
   unit/test_deleteDatabase_onblocked_duringVersionChange.js
   unit/test_event_source.js
   unit/test_filehandle_append_read_data.js
   unit/test_getAll.js
   unit/test_globalObjects_ipc.js
@@ -164,16 +165,18 @@ skip-if = (buildapp == 'b2g' && toolkit 
 [test_create_objectStore.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_cursor_mutation.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_cursor_update_updates_indexes.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_cursors.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
+[test_database_onclose.html]
+skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_deleteDatabase.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_deleteDatabase_interactions.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_deleteDatabase_onblocked.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_deleteDatabase_onblocked_duringVersionChange.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/test_database_onclose.html
@@ -0,0 +1,19 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>Indexed Database DeleteDatabase Test</title>
+
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+  <script type="text/javascript;version=1.7" src="unit/test_database_onclose.js"></script>
+  <script type="text/javascript;version=1.7" src="helpers.js"></script>
+
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/unit/test_database_close_without_onclose.js
@@ -0,0 +1,49 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var testGenerator = testSteps();
+
+function testSteps()
+{
+  const name = this.window ? window.location.pathname :
+                             "test_database_close_without_onclose.js";
+
+  const checkpointSleepTimeSec = 10;
+
+  let openRequest = indexedDB.open(name, 1);
+  openRequest.onerror = errorHandler;
+  openRequest.onsuccess = unexpectedSuccessHandler;
+  openRequest.onupgradeneeded = grabEventAndContinueHandler;
+
+  ok(openRequest instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest");
+
+  let event = yield undefined;
+
+  is(event.type, "upgradeneeded", "Expect an upgradeneeded event");
+  ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event");
+
+  let db = event.target.result;
+  db.createObjectStore("store");
+
+  openRequest.onsuccess = grabEventAndContinueHandler;
+
+  event = yield undefined;
+
+  is(event.type, "success", "Expect a success event");
+  is(event.target, openRequest, "Event has right target");
+  ok(event.target.result instanceof IDBDatabase, "Result should be a database");
+  is(db.objectStoreNames.length, 1, "Expect an objectStore here");
+
+  db.onclose = errorHandler;
+
+  db.close();
+  setTimeout(continueToNextStepSync, checkpointSleepTimeSec * 1000);
+  yield undefined;
+
+  ok(true, "The close event should not be fired after closed normally!");
+
+  finishTest();
+  yield undefined;
+}
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/unit/test_database_onclose.js
@@ -0,0 +1,245 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var testGenerator = testSteps();
+
+function testSteps()
+{
+  function testInvalidStateError(aDb, aTxn) {
+    try {
+      info("The db shall become invalid after closed.");
+      aDb.transaction("store");
+      ok(false, "InvalidStateError shall be thrown.");
+    } catch (e) {
+      ok(e instanceof DOMException, "got a database exception");
+      is(e.name, "InvalidStateError", "correct error");
+    }
+
+    try {
+      info("The txn shall become invalid after closed.");
+      aTxn.objectStore("store");
+      ok(false, "InvalidStateError shall be thrown.");
+    } catch (e) {
+      ok(e instanceof DOMException, "got a database exception");
+      is(e.name, "InvalidStateError", "correct error");
+    }
+  }
+
+  const name = this.window ? window.location.pathname :
+                             "test_database_onclose.js";
+
+  info("#1: Verifying IDBDatabase.onclose after cleared by the agent.");
+  let openRequest = indexedDB.open(name, 1);
+  openRequest.onerror = errorHandler;
+  openRequest.onsuccess = unexpectedSuccessHandler;
+  openRequest.onupgradeneeded = grabEventAndContinueHandler;
+
+  ok(openRequest instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest");
+
+  let event = yield undefined;
+
+  is(event.type, "upgradeneeded", "Expect an upgradeneeded event");
+  ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event");
+
+  let db = event.target.result;
+  db.createObjectStore("store");
+
+  openRequest.onsuccess = grabEventAndContinueHandler;
+
+  event = yield undefined;
+
+  is(event.type, "success", "Expect a success event");
+  is(event.target, openRequest, "Event has right target");
+  ok(event.target.result instanceof IDBDatabase, "Result should be a database");
+  is(db.objectStoreNames.length, 1, "Expect an objectStore here");
+
+  let txn = db.transaction("store", "readwrite");
+  let objectStore = txn.objectStore("store");
+
+  clearAllDatabases(continueToNextStep);
+
+  db.onclose = grabEventAndContinueHandler;
+  event = yield undefined;
+  is(event.type, "close", "Expect a close event");
+  is(event.target, db, "Correct target");
+
+  info("Wait for callback of clearAllDatabases().");
+  yield undefined;
+
+  testInvalidStateError(db, txn);
+
+  info("#2: Verifying IDBDatabase.onclose && IDBTransaction.onerror " +
+  		 "in *write* operation after cleared by the agent.");
+  openRequest = indexedDB.open(name, 1);
+  openRequest.onerror = errorHandler;
+  openRequest.onsuccess = unexpectedSuccessHandler;
+  openRequest.onupgradeneeded = grabEventAndContinueHandler;
+
+  ok(openRequest instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest");
+
+  event = yield undefined;
+
+  is(event.type, "upgradeneeded", "Expect an upgradeneeded event");
+  ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event");
+
+  db = event.target.result;
+  db.createObjectStore("store");
+
+  openRequest.onsuccess = grabEventAndContinueHandler;
+
+  event = yield undefined;
+
+  is(event.type, "success", "Expect a success event");
+  is(event.target, openRequest, "Event has right target");
+  ok(event.target.result instanceof IDBDatabase, "Result should be a database");
+  is(db.objectStoreNames.length, 1, "Expect an objectStore here");
+
+  txn = db.transaction("store", "readwrite");
+  objectStore = txn.objectStore("store");
+
+  let objectId = 0;
+  while(true) {
+    let addRequest = objectStore.add({foo: "foo"}, objectId);
+    addRequest.onerror = function(event) {
+      info("addRequest.onerror, objectId: " + objectId);
+      txn.onerror = grabEventAndContinueHandler;
+      testGenerator.send(true);
+    }
+    addRequest.onsuccess = function() {
+      testGenerator.send(false);
+    }
+
+    if (objectId == 0) {
+      clearAllDatabases(() => {
+        info("clearAllDatabases is done.");
+        continueToNextStep();
+      });
+    }
+
+    objectId++;
+
+    let aborted = yield undefined;
+    if (aborted) {
+      break;
+    }
+  }
+
+  event = yield undefined;
+  is(event.type, "error", "Got an error event");
+  is(event.target.error.name, "AbortError", "Expected AbortError was thrown.");
+  event.preventDefault();
+
+  txn.onabort = grabEventAndContinueHandler;
+  event = yield undefined;
+  is(event.type, "abort", "Got an abort event");
+  is(event.target.error.name, "AbortError", "Expected AbortError was thrown.");
+
+  db.onclose = grabEventAndContinueHandler;
+  event = yield undefined;
+  is(event.type, "close", "Expect a close event");
+  is(event.target, db, "Correct target");
+  testInvalidStateError(db, txn);
+
+  info("Wait for the callback of clearAllDatabases().");
+  yield undefined;
+
+  info("#3: Verifying IDBDatabase.onclose && IDBTransaction.onerror " +
+  "in *read* operation after cleared by the agent.");
+  openRequest = indexedDB.open(name, 1);
+  openRequest.onerror = errorHandler;
+  openRequest.onsuccess = unexpectedSuccessHandler;
+  openRequest.onupgradeneeded = grabEventAndContinueHandler;
+
+  ok(openRequest instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest");
+
+  event = yield undefined;
+
+  is(event.type, "upgradeneeded", "Expect an upgradeneeded event");
+  ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event");
+
+  db = event.target.result;
+  objectStore =
+    db.createObjectStore("store", { keyPath: "id", autoIncrement: true });
+  // The number of read records varies between 1~2000 before the db is cleared
+  // during testing.
+  let numberOfObjects = 3000;
+  objectId = 0;
+  while(true) {
+    let addRequest = objectStore.add({foo: "foo"});
+    addRequest.onsuccess = function() {
+      objectId++;
+      testGenerator.send(objectId == numberOfObjects);
+    }
+    addRequest.onerror = errorHandler;
+
+    let done = yield undefined;
+    if (done) {
+      break;
+    }
+  }
+
+  openRequest.onsuccess = grabEventAndContinueHandler;
+
+  event = yield undefined;
+
+  is(event.type, "success", "Expect a success event");
+  is(event.target, openRequest, "Event has right target");
+  ok(event.target.result instanceof IDBDatabase, "Result should be a database");
+  is(db.objectStoreNames.length, 1, "Expect an objectStore here");
+
+  txn = db.transaction("store");
+  objectStore = txn.objectStore("store");
+
+  let numberOfReadObjects = 0;
+  let readRequest = objectStore.openCursor();
+  readRequest.onerror = function(event) {
+    info("readRequest.onerror, numberOfReadObjects: " + numberOfReadObjects);
+    testGenerator.send(true);
+  }
+  readRequest.onsuccess = function(event) {
+    let cursor = event.target.result;
+    if (cursor) {
+      numberOfReadObjects++;
+      event.target.result.continue();
+    } else {
+      info("Cursor is invalid, numberOfReadObjects: " + numberOfReadObjects);
+      todo(false, "All records are iterated before database is cleared!");
+      testGenerator.send(false);
+    }
+  }
+
+  clearAllDatabases(() => {
+    info("clearAllDatabases is done.");
+    continueToNextStep();
+  });
+
+  readRequestError = yield undefined;
+  if (readRequestError) {
+    txn.onerror = grabEventAndContinueHandler;
+
+    event = yield undefined;
+    is(event.type, "error", "Got an error event");
+    is(event.target.error.name, "AbortError", "Expected AbortError was thrown.");
+    event.preventDefault();
+
+    txn.onabort = grabEventAndContinueHandler;
+    event = yield undefined;
+    is(event.type, "abort", "Got an abort event");
+    is(event.target.error.name, "AbortError", "Expected AbortError was thrown.");
+
+    db.onclose = grabEventAndContinueHandler;
+    event = yield undefined;
+    is(event.type, "close", "Expect a close event");
+    is(event.target, db, "Correct target");
+
+    testInvalidStateError(db, txn);
+  }
+
+  info("Wait for the callback of clearAllDatabases().");
+  yield undefined;
+
+  finishTest();
+  yield undefined;
+}
--- a/dom/indexedDB/test/unit/xpcshell-parent-process.ini
+++ b/dom/indexedDB/test/unit/xpcshell-parent-process.ini
@@ -27,16 +27,18 @@ support-files =
   storagePersistentUpgrade_profile.zip
   xpcshell-shared.ini
 
 [include:xpcshell-shared.ini]
 
 [test_blob_file_backed.js]
 [test_bug1056939.js]
 [test_cleanup_transaction.js]
+[test_database_close_without_onclose.js]
+[test_database_onclose.js]
 [test_defaultStorageUpgrade.js]
 [test_idbSubdirUpgrade.js]
 [test_globalObjects_ipc.js]
 skip-if = toolkit == 'android'
 [test_idle_maintenance.js]
 [test_invalidate.js]
 # disabled for the moment.
 skip-if = true
--- a/dom/webidl/IDBDatabase.webidl
+++ b/dom/webidl/IDBDatabase.webidl
@@ -25,16 +25,17 @@ interface IDBDatabase : EventTarget {
 
     [Throws]
     IDBTransaction transaction ((DOMString or sequence<DOMString>) storeNames,
                                 optional IDBTransactionMode mode = "readonly");
 
     void           close ();
 
                 attribute EventHandler       onabort;
+                attribute EventHandler       onclose;
                 attribute EventHandler       onerror;
                 attribute EventHandler       onversionchange;
 };
 
 partial interface IDBDatabase {
     [Func="mozilla::dom::IndexedDatabaseManager::ExperimentalFeaturesEnabled"]
     readonly    attribute StorageType        storage;
 
--- a/testing/web-platform/tests/IndexedDB/interfaces.idl
+++ b/testing/web-platform/tests/IndexedDB/interfaces.idl
@@ -78,16 +78,17 @@ interface IDBDatabase : EventTarget {
     readonly    attribute DOMString          name;
     readonly    attribute unsigned long long version;
     readonly    attribute DOMStringList      objectStoreNames;
     IDBObjectStore createObjectStore (DOMString name, optional IDBObjectStoreParameters optionalParameters);
     void           deleteObjectStore (DOMString name);
     IDBTransaction transaction ((DOMString or sequence<DOMString>) storeNames, optional IDBTransactionMode mode = "readonly");
     void           close ();
                 attribute EventHandler       onabort;
+                attribute EventHandler       onclose;
                 attribute EventHandler       onerror;
                 attribute EventHandler       onversionchange;
 };
 
 interface IDBObjectStore {
     attribute DOMString name;
     readonly    attribute any            keyPath;
     readonly    attribute DOMStringList  indexNames;