Bug 1078438 - Change the way IndexedDB uses SQLite table locks. r=janv.
authorBen Turner <bent.mozilla@gmail.com>
Tue, 11 Nov 2014 10:47:51 -0800
changeset 240827 577bc6a54c1daa6060c960de2ad083a8d817c877
parent 240826 73ea3aa3ccec755656cd0f40a5491eca05a492d3
child 240828 3f5f0844f2f45389a3cd884e2f69e748d4219c9f
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanv
bugs1078438
milestone36.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 1078438 - Change the way IndexedDB uses SQLite table locks. r=janv.
dom/indexedDB/ActorsParent.cpp
dom/indexedDB/test/mochitest.ini
dom/indexedDB/test/test_table_locks.html
dom/indexedDB/test/test_table_rollback.html
dom/indexedDB/test/unit/test_table_locks.js
dom/indexedDB/test/unit/test_table_rollback.js
dom/indexedDB/test/unit/xpcshell-shared.ini
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -2193,17 +2193,23 @@ SetDefaultPragmas(mozIStorageConnection*
 #endif
     // We use foreign keys in lots of places.
     "PRAGMA foreign_keys = ON; "
     // The "INSERT OR REPLACE" statement doesn't fire the update trigger,
     // instead it fires only the insert trigger. This confuses the update
     // refcount function. This behavior changes with enabled recursive triggers,
     // so the statement fires the delete trigger first and then the insert
     // trigger.
-    "PRAGMA recursive_triggers = ON;";
+    "PRAGMA recursive_triggers = ON;"
+    // We don't need SQLite's table locks because we manage transaction ordering
+    // ourselves and we know we will never allow a write transaction to modify
+    // an object store that a read transaction is in the process of using.
+    "PRAGMA read_uncommitted = TRUE;"
+    // No more PRAGMAs.
+    ;
 
   nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(query));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   if (IndexedDatabaseManager::FullSynchronous()) {
     rv = aConnection->ExecuteSimpleSQL(
--- a/dom/indexedDB/test/mochitest.ini
+++ b/dom/indexedDB/test/mochitest.ini
@@ -73,16 +73,18 @@ support-files =
   unit/test_remove_index.js
   unit/test_remove_objectStore.js
   unit/test_request_readyState.js
   unit/test_setVersion.js
   unit/test_setVersion_abort.js
   unit/test_setVersion_events.js
   unit/test_setVersion_exclusion.js
   unit/test_success_events_after_abort.js
+  unit/test_table_locks.js
+  unit/test_table_rollback.js
   unit/test_temporary_storage.js
   unit/test_traffic_jam.js
   unit/test_transaction_abort.js
   unit/test_transaction_abort_hang.js
   unit/test_transaction_duplicate_store_names.js
   unit/test_transaction_error.js
   unit/test_transaction_lifetimes.js
   unit/test_transaction_lifetimes_nested.js
@@ -334,16 +336,20 @@ skip-if = (buildapp == 'b2g' && toolkit 
 [test_setVersion_abort.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_setVersion_events.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_setVersion_exclusion.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_success_events_after_abort.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
+[test_table_locks.html]
+skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
+[test_table_rollback.html]
+skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_third_party.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_traffic_jam.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_transaction_abort.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_transaction_abort_hang.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/test_table_locks.html
@@ -0,0 +1,18 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>IndexedDB 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_table_locks.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/test_table_rollback.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 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_table_rollback.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_table_locks.js
@@ -0,0 +1,116 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const dbName = ("window" in this) ? window.location.pathname : "test";
+const dbVersion = 1;
+const objName1 = "o1";
+const objName2 = "o2";
+const idxName1 = "i1";
+const idxName2 = "i2";
+const idxKeyPathProp = "idx";
+const objDataProp = "data";
+const objData = "1234567890";
+const objDataCount = 5;
+const loopCount = 100;
+
+let testGenerator = testSteps();
+
+function testSteps()
+{
+  let req = indexedDB.open(dbName, dbVersion);
+  req.onerror = errorHandler;
+  req.onupgradeneeded = grabEventAndContinueHandler;
+  req.onsuccess = grabEventAndContinueHandler;
+
+  let event = yield undefined;
+
+  is(event.type, "upgradeneeded", "Got upgradeneeded event");
+
+  let db = event.target.result;
+
+  let objectStore1 = db.createObjectStore(objName1);
+  objectStore1.createIndex(idxName1, idxKeyPathProp);
+
+  let objectStore2 = db.createObjectStore(objName2);
+  objectStore2.createIndex(idxName2, idxKeyPathProp);
+
+  for (let i = 0; i < objDataCount; i++) {
+    var data = { };
+    data[objDataProp] = objData;
+    data[idxKeyPathProp] = objDataCount - i - 1;
+
+    objectStore1.add(data, i);
+    objectStore2.add(data, i);
+  }
+
+  event = yield undefined;
+
+  is(event.type, "success", "Got success event");
+
+  doReadOnlyTransaction(db, 0, loopCount);
+  doReadWriteTransaction(db, 0, loopCount);
+
+  // Wait for readonly and readwrite transaction loops to complete.
+  yield undefined;
+  yield undefined;
+
+  finishTest();
+  yield undefined;
+}
+
+function doReadOnlyTransaction(db, key, remaining)
+{
+  if (!remaining) {
+    info("Finished all readonly transactions");
+    continueToNextStep();
+    return;
+  }
+
+  info("Starting readonly transaction for key " + key + ", " + remaining +
+       " loops left");
+
+  let objectStore = db.transaction(objName1, "readonly").objectStore(objName1);
+  let index = objectStore.index(idxName1);
+
+  index.openKeyCursor(key, "prev").onsuccess = function(event) {
+    let cursor = event.target.result;
+    ok(cursor, "Got readonly cursor");
+
+    objectStore.get(cursor.primaryKey).onsuccess = function(event) {
+      if (++key == objDataCount) {
+        key = 0;
+      }
+      doReadOnlyTransaction(db, key, remaining - 1);
+    }
+  };
+}
+
+function doReadWriteTransaction(db, key, remaining)
+{
+  if (!remaining) {
+    info("Finished all readwrite transactions");
+    continueToNextStep();
+    return;
+  }
+
+  info("Starting readwrite transaction for key " + key + ", " + remaining +
+       " loops left");
+
+  let objectStore = db.transaction(objName2, "readwrite").objectStore(objName2);
+  objectStore.openCursor(key).onsuccess = function(event) {
+    let cursor = event.target.result;
+    ok(cursor, "Got readwrite cursor");
+
+    let value = cursor.value;
+    value[idxKeyPathProp]++;
+
+    cursor.update(value).onsuccess = function(event) {
+      if (++key == objDataCount) {
+        key = 0;
+      }
+      doReadWriteTransaction(db, key, remaining - 1);
+    }
+  };
+}
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/unit/test_table_rollback.js
@@ -0,0 +1,115 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+let testGenerator = testSteps();
+
+function testSteps()
+{
+  const dbName = ("window" in this) ? window.location.pathname : "test";
+  const objName1 = "foo";
+  const objName2 = "bar";
+  const data1 = "1234567890";
+  const data2 = "0987654321";
+  const dataCount = 500;
+
+  let request = indexedDB.open(dbName, 1);
+  request.onerror = errorHandler;
+  request.onupgradeneeded = grabEventAndContinueHandler;
+
+  let event = yield undefined;
+
+  is(event.type, "upgradeneeded", "Got upgradeneeded");
+
+  request.onupgradeneeded = errorHandler;
+  request.onsuccess = grabEventAndContinueHandler;
+
+  let db = request.result;
+
+  let objectStore1 = db.createObjectStore(objName1, { autoIncrement: true });
+  let objectStore2 = db.createObjectStore(objName2, { autoIncrement: true });
+
+  info("Created object stores, adding data");
+
+  for (let i = 0; i < dataCount; i++) {
+    objectStore1.add(data1);
+    objectStore2.add(data2);
+  }
+
+  info("Done adding data");
+
+  event = yield undefined;
+
+  is(event.type, "success", "Got success");
+
+  let readResult = null;
+  let readError = null;
+  let writeAborted = false;
+
+  info("Creating readwrite transaction");
+
+  objectStore1 = db.transaction(objName1, "readwrite").objectStore(objName1);
+  objectStore1.openCursor().onsuccess = grabEventAndContinueHandler;
+
+  event = yield undefined;
+
+  let cursor = event.target.result;
+  is(cursor.value, data1, "Got correct data for readwrite transaction");
+
+  info("Modifying object store on readwrite transaction");
+
+  cursor.update(data2);
+  cursor.continue();
+
+  event = yield undefined;
+
+  info("Done modifying object store on readwrite transaction, creating " +
+       "readonly transaction");
+
+  objectStore2 = db.transaction(objName2, "readonly").objectStore(objName2);
+  request = objectStore2.getAll();
+  request.onsuccess = function(event) {
+    readResult = event.target.result;
+    is(readResult.length,
+       dataCount,
+       "Got correct number of results on readonly transaction");
+    for (let i = 0; i < readResult.length; i++) {
+      is(readResult[i], data2, "Got correct data for readonly transaction");
+    }
+    if (writeAborted) {
+      continueToNextStep();
+    }
+  };
+  request.onerror = function(event) {
+    readResult = null;
+    readError = event.target.error;
+
+    ok(false, "Got read error: " + readError.name);
+    event.preventDefault();
+
+    if (writeAborted) {
+      continueToNextStep();
+    }
+  }
+
+  cursor = event.target.result;
+  is(cursor.value, data1, "Got correct data for readwrite transaction");
+
+  info("Aborting readwrite transaction");
+
+  cursor.source.transaction.abort();
+  writeAborted = true;
+
+  if (!readError && !readResult) {
+    info("Waiting for readonly transaction to complete");
+    yield undefined;
+  }
+
+  ok(readResult, "Got result from readonly transaction");
+  is(readError, null, "No read error");
+  is(writeAborted, true, "Aborted readwrite transaction");
+
+  finishTest();
+  yield undefined;
+}
--- a/dom/indexedDB/test/unit/xpcshell-shared.ini
+++ b/dom/indexedDB/test/unit/xpcshell-shared.ini
@@ -59,16 +59,18 @@ skip-if = toolkit == 'android' # bug 107
 [test_remove_index.js]
 [test_remove_objectStore.js]
 [test_request_readyState.js]
 [test_setVersion.js]
 [test_setVersion_abort.js]
 [test_setVersion_events.js]
 [test_setVersion_exclusion.js]
 [test_success_events_after_abort.js]
+[test_table_locks.js]
+[test_table_rollback.js]
 [test_traffic_jam.js]
 [test_transaction_abort.js]
 [test_transaction_abort_hang.js]
 [test_transaction_duplicate_store_names.js]
 [test_transaction_error.js]
 [test_transaction_lifetimes.js]
 [test_transaction_lifetimes_nested.js]
 [test_transaction_ordering.js]