0

Indexed DB: Prevent invalid transaction timeouts

A transaction watchdog timer intended to guard against hung renderers
would be scheduled even if the transaction backend still had tasks to
perform, which could be blocked on other backend work. The watchdog
timer should only be started if the backend is only waiting on the
front end. Add the extra condition.

Bug: 1045585
Change-Id: I86e978faa9735e1b7a89a10660f1c747237b3c9c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2026109
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Commit-Queue: Joshua Bell <jsbell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#736211}
This commit is contained in:
Joshua Bell
2020-01-29 03:22:31 +00:00
committed by Commit Bot
parent 53d66de76f
commit 76a2f7b88d
3 changed files with 69 additions and 1 deletions

@ -514,7 +514,8 @@ IndexedDBTransaction::RunTasks() {
// Otherwise, start a timer in case the front-end gets wedged and
// never requests further activity. Read-only transactions don't
// block other transactions, so don't time those out.
if (mode_ != blink::mojom::IDBTransactionMode::ReadOnly &&
if (!HasPendingTasks() &&
mode_ != blink::mojom::IDBTransactionMode::ReadOnly &&
state_ == STARTED) {
timeout_timer_.Start(FROM_HERE, GetInactivityTimeout(),
base::BindOnce(&IndexedDBTransaction::Timeout,

@ -46,6 +46,7 @@ FORWARD_DECLARE_TEST(IndexedDBTransactionTest, SchedulePreemptiveTask);
FORWARD_DECLARE_TEST(IndexedDBTransactionTestMode, ScheduleNormalTask);
FORWARD_DECLARE_TEST(IndexedDBTransactionTestMode, TaskFails);
FORWARD_DECLARE_TEST(IndexedDBTransactionTest, Timeout);
FORWARD_DECLARE_TEST(IndexedDBTransactionTest, TimeoutPreemptive);
FORWARD_DECLARE_TEST(IndexedDBTransactionTest, IndexedDBObserver);
} // namespace indexed_db_transaction_unittest
@ -196,6 +197,9 @@ class CONTENT_EXPORT IndexedDBTransaction {
FRIEND_TEST_ALL_PREFIXES(
indexed_db_transaction_unittest::IndexedDBTransactionTest,
Timeout);
FRIEND_TEST_ALL_PREFIXES(
indexed_db_transaction_unittest::IndexedDBTransactionTest,
TimeoutPreemptive);
FRIEND_TEST_ALL_PREFIXES(
indexed_db_transaction_unittest::IndexedDBTransactionTest,
IndexedDBObserver);

@ -198,6 +198,69 @@ TEST_F(IndexedDBTransactionTest, Timeout) {
EXPECT_EQ(1, transaction->diagnostics().tasks_completed);
}
TEST_F(IndexedDBTransactionTest, TimeoutPreemptive) {
const int64_t id = 0;
const std::set<int64_t> scope;
const leveldb::Status commit_success = leveldb::Status::OK();
std::unique_ptr<IndexedDBConnection> connection = CreateConnection();
IndexedDBTransaction* transaction = connection->CreateTransaction(
id, scope, blink::mojom::IDBTransactionMode::ReadWrite,
new IndexedDBFakeBackingStore::FakeTransaction(commit_success));
db_->RegisterAndScheduleTransaction(transaction);
// No conflicting transactions, so coordinator will start it immediately:
EXPECT_EQ(IndexedDBTransaction::STARTED, transaction->state());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(0, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
// Add a preemptive task.
transaction->ScheduleTask(
blink::mojom::IDBTaskType::Preemptive,
base::BindOnce(&IndexedDBTransactionTest::DummyOperation,
base::Unretained(this), leveldb::Status::OK()));
transaction->AddPreemptiveEvent();
EXPECT_TRUE(transaction->HasPendingTasks());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_FALSE(transaction->preemptive_task_queue_.empty());
// Pump the message loop so that the transaction completes all pending tasks,
// otherwise it will defer the commit.
RunPostedTasks();
EXPECT_TRUE(transaction->HasPendingTasks());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
// Schedule a task - timer won't be started until preemptive tasks are done.
transaction->ScheduleTask(
base::BindOnce(&IndexedDBTransactionTest::DummyOperation,
base::Unretained(this), leveldb::Status::OK()));
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
// This shouldn't do anything - the preemptive task is still lurking.
RunPostedTasks();
EXPECT_TRUE(transaction->HasPendingTasks());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
// Finish the preemptive task, which unblocks regular tasks.
transaction->DidCompletePreemptiveEvent();
// TODO(dmurph): Should this explicit call be necessary?
transaction->RunTasks();
// The task's completion should start the timer.
EXPECT_FALSE(transaction->HasPendingTasks());
EXPECT_TRUE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(1, transaction->diagnostics().tasks_completed);
}
TEST_F(IndexedDBTransactionTest, NoTimeoutReadOnly) {
const int64_t id = 0;
const std::set<int64_t> scope;