Commit a69701fa authored by Andreas Butler's avatar Andreas Butler Committed by Commit Bot

[IndexedDB]: Add commit check to aborting txns.

A check is added to ensure that transactions that have already been sent
a commit signal from the front end (for whatever reason) are committed
instead of aborted in the event of an abort.

Spec Change: https://github.com/w3c/IndexedDB/pull/242

Explainer: https://andreas-butler.github.io/idb-transaction-commit/EXPLAINER
Change-Id: I942136c1bbfb8a5347bf7cf1f9702f5cbff66521
Reviewed-on: https://chromium-review.googlesource.com/c/1330692
Commit-Queue: Andreas Butler <andreasbutler@google.com>
Reviewed-by: default avatarVictor Costan <pwnall@chromium.org>
Reviewed-by: default avatarDaniel Murphy <dmurph@chromium.org>
Reviewed-by: default avatarChase Phillips <cmp@chromium.org>
Cr-Commit-Position: refs/heads/master@{#619859}
parent 877be192
...@@ -336,6 +336,20 @@ class IndexedDBBrowserTestWithGCExposed : public IndexedDBBrowserTest { ...@@ -336,6 +336,20 @@ class IndexedDBBrowserTestWithGCExposed : public IndexedDBBrowserTest {
DISALLOW_COPY_AND_ASSIGN(IndexedDBBrowserTestWithGCExposed); DISALLOW_COPY_AND_ASSIGN(IndexedDBBrowserTestWithGCExposed);
}; };
class IndexedDBBrowserTestWithExperimentalWebFeatures
: public IndexedDBBrowserTest {
public:
IndexedDBBrowserTestWithExperimentalWebFeatures() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
}
private:
DISALLOW_COPY_AND_ASSIGN(IndexedDBBrowserTestWithExperimentalWebFeatures);
};
IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithGCExposed, IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithGCExposed,
DatabaseCallbacksTest) { DatabaseCallbacksTest) {
SimpleTest(GetTestUrl("indexeddb", "database_callbacks_first.html")); SimpleTest(GetTestUrl("indexeddb", "database_callbacks_first.html"));
...@@ -925,6 +939,34 @@ IN_PROC_BROWSER_TEST_F( ...@@ -925,6 +939,34 @@ IN_PROC_BROWSER_TEST_F(
EXPECT_EQ(expected_title16, title_watcher.WaitAndGetTitle()); EXPECT_EQ(expected_title16, title_watcher.WaitAndGetTitle());
} }
// Testing abort ordering for commit. Verifies that an explicitly committed
// transaction blocked by another pending transaction will be committed rather
// than aborted in the event that the page crashes before its committed.
IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithExperimentalWebFeatures,
CommitContinuesOnAbort) {
// Sets up a database, opens four transactions against it such that two end
// up indefinitely blocked, one of which is explicitly committed.
NavigateAndWaitForTitle(shell(), "blocked_explicit_commit.html", "#tab1",
"transactions registered");
// Crashes the tab to cause the database set up above to force close with the
// blocked transactions still open.
shell()->web_contents()->GetMainFrame()->GetProcess()->Shutdown(0);
// Reopens the same page that was just crashed and inspects the database to
// see the results of the transactions that were open at time of crash.
Shell* new_shell = CreateBrowser();
GURL url = GetTestUrl("indexeddb", "blocked_explicit_commit.html");
url = GURL(url.spec() + "#tab2");
base::string16 expected_title16(
ASCIIToUTF16("transactions aborted and committed as expected"));
TitleWatcher title_watcher(new_shell->web_contents(), expected_title16);
title_watcher.AlsoWaitForTitle(
ASCIIToUTF16("fail - transactions did not abort and commit as expected"));
NavigateToURL(new_shell, url);
EXPECT_EQ(expected_title16, title_watcher.WaitAndGetTitle());
}
// Verify that a "close" event is fired at database connections when // Verify that a "close" event is fired at database connections when
// the backing store is deleted. // the backing store is deleted.
IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTest, ForceCloseEventTest) { IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTest, ForceCloseEventTest) {
......
...@@ -123,13 +123,21 @@ void IndexedDBConnection::AbortTransaction( ...@@ -123,13 +123,21 @@ void IndexedDBConnection::AbortTransaction(
transaction->Abort(error); transaction->Abort(error);
} }
void IndexedDBConnection::AbortAllTransactions( void IndexedDBConnection::FinishAllTransactions(
const IndexedDBDatabaseError& error) { const IndexedDBDatabaseError& error) {
std::unordered_map<int64_t, std::unique_ptr<IndexedDBTransaction>> temp_map; std::unordered_map<int64_t, std::unique_ptr<IndexedDBTransaction>> temp_map;
std::swap(temp_map, transactions_); std::swap(temp_map, transactions_);
for (const auto& pair : temp_map) { for (const auto& pair : temp_map) {
IDB_TRACE1("IndexedDBDatabase::Abort(error)", "txn.id", pair.second->id()); auto& transaction = pair.second;
pair.second->Abort(error); if (transaction->is_commit_pending()) {
IDB_TRACE1("IndexedDBDatabase::Commit", "transaction.id",
transaction->id());
transaction->ForcePendingCommit();
} else {
IDB_TRACE1("IndexedDBDatabase::Abort(error)", "transaction.id",
transaction->id());
transaction->Abort(error);
}
} }
} }
......
...@@ -66,7 +66,11 @@ class CONTENT_EXPORT IndexedDBConnection { ...@@ -66,7 +66,11 @@ class CONTENT_EXPORT IndexedDBConnection {
void AbortTransaction(IndexedDBTransaction* transaction, void AbortTransaction(IndexedDBTransaction* transaction,
const IndexedDBDatabaseError& error); const IndexedDBDatabaseError& error);
void AbortAllTransactions(const IndexedDBDatabaseError& error); // Aborts or commits each transaction owned by this connection depending on
// the transaction's current state. Any transaction with is_commit_pending_
// false is aborted, and any transaction with is_commit_pending_ true is
// committed.
void FinishAllTransactions(const IndexedDBDatabaseError& error);
IndexedDBTransaction* GetTransaction(int64_t id) const; IndexedDBTransaction* GetTransaction(int64_t id) const;
......
...@@ -1892,7 +1892,7 @@ void IndexedDBDatabase::Close(IndexedDBConnection* connection, bool forced) { ...@@ -1892,7 +1892,7 @@ void IndexedDBDatabase::Close(IndexedDBConnection* connection, bool forced) {
// happen if the close is requested by the connection itself as the // happen if the close is requested by the connection itself as the
// front-end defers the close until all transactions are complete, but can // front-end defers the close until all transactions are complete, but can
// occur on process termination or forced close. // occur on process termination or forced close.
connection->AbortAllTransactions(IndexedDBDatabaseError( connection->FinishAllTransactions(IndexedDBDatabaseError(
blink::kWebIDBDatabaseExceptionUnknownError, "Connection is closing.")); blink::kWebIDBDatabaseExceptionUnknownError, "Connection is closing."));
// Abort transactions before removing the connection; aborting may complete // Abort transactions before removing the connection; aborting may complete
...@@ -1947,7 +1947,7 @@ void IndexedDBDatabase::AbortAllTransactionsForConnections() { ...@@ -1947,7 +1947,7 @@ void IndexedDBDatabase::AbortAllTransactionsForConnections() {
IDB_TRACE("IndexedDBDatabase::AbortAllTransactionsForConnections"); IDB_TRACE("IndexedDBDatabase::AbortAllTransactionsForConnections");
for (IndexedDBConnection* connection : connections_) { for (IndexedDBConnection* connection : connections_) {
connection->AbortAllTransactions( connection->FinishAllTransactions(
IndexedDBDatabaseError(blink::kWebIDBDatabaseExceptionUnknownError, IndexedDBDatabaseError(blink::kWebIDBDatabaseExceptionUnknownError,
"Database is compacting.")); "Database is compacting."));
} }
......
...@@ -178,9 +178,27 @@ void IndexedDBTransaction::RunTasksIfStarted() { ...@@ -178,9 +178,27 @@ void IndexedDBTransaction::RunTasksIfStarted() {
ptr_factory_.GetWeakPtr())); ptr_factory_.GetWeakPtr()));
} }
void IndexedDBTransaction::ForcePendingCommit() {
IDB_TRACE1("IndexedDBTransaction::ForceCommit", "txn.id", id());
DCHECK(is_commit_pending_);
if (state_ == FINISHED)
return;
should_process_queue_ = true;
state_ = STARTED;
if (!task_queue_.empty()) {
// Commits when completed.
ProcessTaskQueue();
} else {
leveldb::Status result = Commit();
if (!result.ok())
database_->ReportError(result);
}
}
void IndexedDBTransaction::Abort(const IndexedDBDatabaseError& error) { void IndexedDBTransaction::Abort(const IndexedDBDatabaseError& error) {
IDB_TRACE1("IndexedDBTransaction::Abort", "txn.id", id());
DCHECK(!processing_event_queue_); DCHECK(!processing_event_queue_);
DCHECK(!is_commit_pending_);
if (state_ == FINISHED) if (state_ == FINISHED)
return; return;
...@@ -254,7 +272,7 @@ void IndexedDBTransaction::Start() { ...@@ -254,7 +272,7 @@ void IndexedDBTransaction::Start() {
diagnostics_.start_time = base::Time::Now(); diagnostics_.start_time = base::Time::Now();
if (!used_) { if (!used_) {
if (commit_pending_) { if (is_commit_pending_) {
// The transaction has never had requests issued against it, but the // The transaction has never had requests issued against it, but the
// front-end previously requested a commit; do the commit now, but not // front-end previously requested a commit; do the commit now, but not
// re-entrantly as that may renter the coordinator. // re-entrantly as that may renter the coordinator.
...@@ -328,7 +346,7 @@ leveldb::Status IndexedDBTransaction::Commit() { ...@@ -328,7 +346,7 @@ leveldb::Status IndexedDBTransaction::Commit() {
return leveldb::Status::OK(); return leveldb::Status::OK();
DCHECK_NE(state_, COMMITTING); DCHECK_NE(state_, COMMITTING);
commit_pending_ = true; is_commit_pending_ = true;
// Front-end has requested a commit, but this transaction is blocked by // Front-end has requested a commit, but this transaction is blocked by
// other transactions. The commit will be initiated when the transaction // other transactions. The commit will be initiated when the transaction
...@@ -500,7 +518,7 @@ void IndexedDBTransaction::ProcessTaskQueue() { ...@@ -500,7 +518,7 @@ void IndexedDBTransaction::ProcessTaskQueue() {
// If there are no pending tasks, we haven't already committed/aborted, // If there are no pending tasks, we haven't already committed/aborted,
// and the front-end requested a commit, it is now safe to do so. // and the front-end requested a commit, it is now safe to do so.
if (!HasPendingTasks() && state_ != FINISHED && commit_pending_) { if (!HasPendingTasks() && state_ != FINISHED && is_commit_pending_) {
processing_event_queue_ = false; processing_event_queue_ = false;
// This can delete |this|. // This can delete |this|.
leveldb::Status result = Commit(); leveldb::Status result = Commit();
......
...@@ -62,6 +62,11 @@ class CONTENT_EXPORT IndexedDBTransaction { ...@@ -62,6 +62,11 @@ class CONTENT_EXPORT IndexedDBTransaction {
leveldb::Status Commit(); leveldb::Status Commit();
// If is_commit_pending_ is true this method does the necessary state
// manipulation to prepare the transaction to be committed, processes its
// task_queue_, and commits the transaction.
void ForcePendingCommit();
// This object is destroyed by this method. // This object is destroyed by this method.
void Abort(const IndexedDBDatabaseError& error); void Abort(const IndexedDBDatabaseError& error);
...@@ -104,6 +109,7 @@ class CONTENT_EXPORT IndexedDBTransaction { ...@@ -104,6 +109,7 @@ class CONTENT_EXPORT IndexedDBTransaction {
IndexedDBDatabase* database() const { return database_.get(); } IndexedDBDatabase* database() const { return database_.get(); }
IndexedDBDatabaseCallbacks* callbacks() const { return callbacks_.get(); } IndexedDBDatabaseCallbacks* callbacks() const { return callbacks_.get(); }
IndexedDBConnection* connection() const { return connection_.get(); } IndexedDBConnection* connection() const { return connection_.get(); }
bool is_commit_pending() const { return is_commit_pending_; }
State state() const { return state_; } State state() const { return state_; }
bool IsTimeoutTimerRunning() const { return timeout_timer_.IsRunning(); } bool IsTimeoutTimerRunning() const { return timeout_timer_.IsRunning(); }
...@@ -182,7 +188,7 @@ class CONTENT_EXPORT IndexedDBTransaction { ...@@ -182,7 +188,7 @@ class CONTENT_EXPORT IndexedDBTransaction {
bool used_ = false; bool used_ = false;
State state_ = CREATED; State state_ = CREATED;
bool commit_pending_ = false; bool is_commit_pending_ = false;
// We are owned by the connection object, but during force closes sometimes // We are owned by the connection object, but during force closes sometimes
// there are issues if there is a pending OpenRequest. So use a WeakPtr. // there are issues if there is a pending OpenRequest. So use a WeakPtr.
base::WeakPtr<IndexedDBConnection> connection_; base::WeakPtr<IndexedDBConnection> connection_;
......
...@@ -435,7 +435,7 @@ TEST_P(IndexedDBTransactionTestMode, AbortPreemptive) { ...@@ -435,7 +435,7 @@ TEST_P(IndexedDBTransactionTestMode, AbortPreemptive) {
EXPECT_FALSE(transaction->should_process_queue_); EXPECT_FALSE(transaction->should_process_queue_);
EXPECT_TRUE(transaction->backing_store_transaction_begun_); EXPECT_TRUE(transaction->backing_store_transaction_begun_);
EXPECT_TRUE(transaction->used_); EXPECT_TRUE(transaction->used_);
EXPECT_FALSE(transaction->commit_pending_); EXPECT_FALSE(transaction->is_commit_pending_);
// This task will be ignored. // This task will be ignored.
transaction->ScheduleTask( transaction->ScheduleTask(
......
<html>
<head>
<title>IndexedDB regression test for bug 98562</title>
<script type="text/javascript" src="shared.js"></script>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="blocked_explicit_commit.js"></script>
</head>
<body onLoad="test()">
<div id="status">Starting...</div>
</body>
</html>
// Copyright (c) 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Javascript test code for verifying proper committing
* behaviour for transactions that are blocked when a tab is force
* closed.
*/
function test() {
if (document.location.hash === '#tab1') {
setUpBlockedTransactions();
} else if (document.location.hash === '#tab2') {
checkForCommit();
} else {
result('fail - unexpected hash');
}
}
function upgradeCallback() {
db = event.target.result;
deleteAllObjectStores(db);
db.createObjectStore('store');
}
// We register four transactions, all on the same object store and all readwrite
// so as to incur blocking, and verify the expected results after a crash.
async function setUpBlockedTransactions() {
const db = await
promiseDeleteThenOpenDb('blocked-explicit-commit', upgradeCallback);
// An auto-committed put that we expect to be committed.
db.transaction('store', 'readwrite').objectStore('store')
.put('auto', 'auto-key');
// A transaction with a put request on it that we keep alive with a request
// loop and which we expect to never commit.
const blockingTransaction = db.transaction('store', 'readwrite');
const blockingRequest = blockingTransaction.objectStore('store')
.put('blocking', 'blocking-key');
blockingRequest.onsuccess = () => { result('transactions registered'); };
keepAlive(blockingTransaction, 'store');
// A transaction with a put request on it that is blocked by the previous
// transaction. We call an explicit commit on this transaction and so expect
// its data to be committed after tab1 crashes and the blocking transaction is
// aborted.
const commitTransaction = db.transaction('store', 'readwrite');
commitTransaction.objectStore('store').put('explicit', 'explicit-key');
commitTransaction.commit();
// A transaction with a put request on it that is blocked by the explicit
// commit transaction. It is expected to be aborted and thus never commit
// because it was not explicitly committed.
db.transaction('store', 'readwrite').objectStore('store')
.put('blocked', 'blocked-key');
}
async function checkForCommit() {
const db = await promiseOpenDb('blocked-explicit-commit');
const transaction = db.transaction('store', 'readonly');
const objectStore = transaction.objectStore('store');
const autoRequest = objectStore.get('auto-key');
const blockingRequest = objectStore.get('blocking-key');
const explicitRequest = objectStore.get('explicit-key');
const blockedRequest = objectStore.get('blocked-key');
for (const request of [autoRequest, blockingRequest, explicitRequest,
blockedRequest]) {
request.onerror = unexpectedErrorCallback;
request.onblocked = unexpectedBlockedCallback;
}
transaction.oncomplete = () => {
if (autoRequest.result == 'auto' && blockingRequest.result == undefined
&& explicitRequest.result == 'explicit'
&& blockedRequest.result == undefined) {
result('transactions aborted and committed as expected');
} else {
result('fail - transactions did not abort and commit as expected');
}
};
}
...@@ -147,6 +147,75 @@ function indexedDBTest(upgradeCallback, optionalOpenCallback) { ...@@ -147,6 +147,75 @@ function indexedDBTest(upgradeCallback, optionalOpenCallback) {
}; };
} }
function promiseDeleteThenOpenDb(dbName, upgradeCallback) {
return new Promise((resolve, reject) => {
const deleteRequest = indexedDB.deleteDatabase(dbName);
deleteRequest.onerror = () => {
reject(new Error('An error occurred on deleting database ${dbName}'));
};
deleteRequest.onsuccess = () => {
const openRequest = indexedDB.open(dbName);
openRequest.onerror = () => {
reject(new Error('An error occurred on opening database ${dbName}'));
};
openRequest.onblocked = () => {
reject(new Error('Opening database ${dbName} was blocked'));
};
openRequest.onupgradeneeded = () => {
upgradeCallback();
};
openRequest.onsuccess = () => {
resolve(event.target.result);
};
}
});
}
function promiseOpenDb(dbName, optionalUpgradeCallback) {
return new Promise((resolve, reject) => {
const openRequest = indexedDB.open(dbName);
openRequest.onerror = () => {
const e = new Error('Error opening database ${dbName}');
unexepectedErrorCallback(e);
reject(e);
};
openRequest.onblocked = () => {
const e = new Error('Opening database ${dbName}');
unexpectedBlockedCallback(e);
reject(e);
};
if (optionalUpgradeCallback) {
openRequest.onupgradeneeded = () => {
const db = event.target.result;
optionalUpgradeCallback(db);
};
}
openRequest.onsuccess = () => {
db = event.target.result;
resolve(db);
};
});
}
function keepAlive(transaction, storeName) {
let completed = false;
transaction.addEventListener('complete', () => { completed = true; });
let pin = true;
function spin() {
if (!pin)
return;
transaction.objectStore(storeName).get(0).onsuccess = spin;
}
spin();
return () => {
shouldBeFalse(completed);
pin = false;
};
}
if (typeof String.prototype.startsWith !== 'function') { if (typeof String.prototype.startsWith !== 'function') {
String.prototype.startsWith = function (str) { String.prototype.startsWith = function (str) {
return this.indexOf(str) === 0; return this.indexOf(str) === 0;
......
...@@ -54,6 +54,8 @@ ...@@ -54,6 +54,8 @@
const result = request.result; const result = request.result;
assert_key_equals(result[testcase.property], key, assert_key_equals(result[testcase.property], key,
'Property should be used as key'); 'Property should be used as key');
});
tx.oncomplete = t.step_func(function() {
t.done(); t.done();
}); });
}, },
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment