blob: e0d433c42e2086742e13d1650b52929f8879b750 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/indexed_db/instance/transaction.h"
#include <stdint.h>
#include <memory>
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/updateable_sequenced_task_runner.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "components/services/storage/indexed_db/locks/partitioned_lock_manager.h"
#include "content/browser/indexed_db/indexed_db_database_error.h"
#include "content/browser/indexed_db/indexed_db_leveldb_coding.h"
#include "content/browser/indexed_db/instance/bucket_context.h"
#include "content/browser/indexed_db/instance/connection.h"
#include "content/browser/indexed_db/instance/database_callbacks.h"
#include "content/browser/indexed_db/instance/fake_transaction.h"
#include "content/public/common/content_features.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/test/mock_quota_manager.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
namespace content::indexed_db {
namespace {
void SetToTrue(bool* value) {
*value = true;
}
} // namespace
class AbortObserver {
public:
AbortObserver() = default;
AbortObserver(const AbortObserver&) = delete;
AbortObserver& operator=(const AbortObserver&) = delete;
void AbortTask() { abort_task_called_ = true; }
bool abort_task_called() const { return abort_task_called_; }
private:
bool abort_task_called_ = false;
};
class TransactionTest : public testing::Test {
public:
TransactionTest()
: task_environment_(std::make_unique<base::test::TaskEnvironment>()) {}
TransactionTest(const TransactionTest&) = delete;
TransactionTest& operator=(const TransactionTest&) = delete;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
quota_manager_ = base::MakeRefCounted<storage::MockQuotaManager>(
/*is_incognito=*/false, temp_dir_.GetPath(),
base::SingleThreadTaskRunner::GetCurrentDefault(),
/*special_storage_policy=*/nullptr);
BucketContext::Delegate delegate;
delegate.on_ready_for_destruction = base::BindOnce(
&TransactionTest::OnDbReadyForDestruction, base::Unretained(this));
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
bucket_context_ = std::make_unique<BucketContext>(
GetOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(storage_key)),
temp_dir_.GetPath(), std::move(delegate),
scoped_refptr<base::UpdateableSequencedTaskRunner>(),
quota_manager_->proxy(),
/*blob_storage_context=*/mojo::NullRemote(),
/*file_system_access_context=*/mojo::NullRemote());
bucket_context_->InitBackingStoreIfNeeded(true);
SetDatabaseUnderTest(u"db");
}
void TearDown() override { db_ = nullptr; }
void SetDatabaseUnderTest(std::u16string name) {
db_ = bucket_context_->CreateAndAddDatabase(name);
}
storage::BucketInfo GetOrCreateBucket(
const storage::BucketInitParams& params) {
base::test::TestFuture<storage::QuotaErrorOr<storage::BucketInfo>> future;
quota_manager_->proxy()->UpdateOrCreateBucket(
params, base::SingleThreadTaskRunner::GetCurrentDefault(),
future.GetCallback());
return future.Take().value();
}
void OnDbReadyForDestruction() { bucket_context_.reset(); }
void RunPostedTasks() { base::RunLoop().RunUntilIdle(); }
Status DummyOperation(Status result, Transaction* transaction) {
return result;
}
std::unique_ptr<Connection> CreateConnection(int priority = 0) {
mojo::Remote<storage::mojom::IndexedDBClientStateChecker> remote;
auto connection = std::make_unique<Connection>(
*bucket_context_, db_->AsWeakPtr(), base::DoNothing(),
base::DoNothing(),
std::make_unique<DatabaseCallbacks>(mojo::NullAssociatedRemote()),
std::move(remote), base::UnguessableToken::Create(), priority);
db_->AddConnectionForTesting(connection.get());
return connection;
}
Transaction* CreateTransaction(Connection* connection,
const int64_t id,
const std::vector<int64_t>& object_store_ids,
blink::mojom::IDBTransactionMode mode) {
connection->CreateTransaction(
mojo::NullAssociatedReceiver(), id, object_store_ids, mode,
blink::mojom::IDBTransactionDurability::Relaxed);
Transaction* transaction = connection->GetTransaction(id);
// `CreateTransaction()` must not fail in this unit test environment.
CHECK_NE(transaction, nullptr);
return transaction;
}
// Creates a new transaction and adds it to `connection` using
// Connection private members. This enables the use of a fake
// backing transaction to simulate errors. Prefer CreateConnection() above
// for tests that do not need to simulate errors because it uses
// publicly exposed functionality.
Transaction* CreateFakeTransactionWithCommitPhaseTwoError(
Connection* connection,
const int64_t id,
const std::set<int64_t>& object_store_ids,
blink::mojom::IDBTransactionMode mode,
Status commit_phase_two_error_status) {
// Use fake transactions to simulate errors only.
CHECK(!commit_phase_two_error_status.ok());
std::unique_ptr<Transaction> transaction = std::make_unique<Transaction>(
id, connection, object_store_ids, mode,
blink::mojom::IDBTransactionDurability::Relaxed,
BucketContextHandle(*bucket_context_),
std::make_unique<FakeTransaction>(
commit_phase_two_error_status,
db_->backing_store_db()->CreateTransaction(
blink::mojom::IDBTransactionDurability::Relaxed, mode)));
Transaction* transaction_reference = transaction.get();
connection->transactions_[id] = std::move(transaction);
db_->RegisterAndScheduleTransaction(transaction_reference);
return transaction_reference;
}
PartitionedLockManager& lock_manager() {
return bucket_context_->lock_manager();
}
protected:
base::ScopedTempDir temp_dir_;
std::unique_ptr<base::test::TaskEnvironment> task_environment_;
std::unique_ptr<BucketContext> bucket_context_;
raw_ptr<Database> db_;
scoped_refptr<storage::MockQuotaManager> quota_manager_;
};
class TransactionTestMode
: public TransactionTest,
public testing::WithParamInterface<blink::mojom::IDBTransactionMode> {
public:
TransactionTestMode() = default;
TransactionTestMode(const TransactionTestMode&) = delete;
TransactionTestMode& operator=(const TransactionTestMode&) = delete;
};
TEST_F(TransactionTest, Timeout) {
const std::vector<int64_t> object_store_ids{1};
std::unique_ptr<Connection> connection = CreateConnection();
Transaction* transaction =
CreateTransaction(connection.get(), /*id=*/0, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
// No conflicting transactions, so coordinator will start it immediately:
EXPECT_EQ(Transaction::STARTED, transaction->state());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(0, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
// Schedule a task - timer won't be started until it's processed.
transaction->ScheduleTask(base::BindOnce(
&TransactionTest::DummyOperation, base::Unretained(this), Status::OK()));
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
RunPostedTasks();
EXPECT_TRUE(transaction->IsTimeoutTimerRunning());
// Since the transaction isn't blocking another transaction, it's expected to
// do nothing when the timeout fires.
transaction->TimeoutFired();
EXPECT_EQ(0, transaction->timeout_strikes_);
EXPECT_EQ(Transaction::STARTED, transaction->state());
// Create a second transaction that's blocked on the first.
std::unique_ptr<Connection> connection2 = CreateConnection();
CreateTransaction(connection2.get(),
/*id=*/1, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
// Now firing the timeout starts racking up strikes.
for (int i = 1; i < Transaction::kMaxTimeoutStrikes; ++i) {
transaction->TimeoutFired();
EXPECT_EQ(Transaction::STARTED, transaction->state());
EXPECT_EQ(i, transaction->timeout_strikes_);
}
// ... and eventually causes the transaction to abort.
transaction->TimeoutFired();
EXPECT_EQ(Transaction::FINISHED, transaction->state());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(1, transaction->diagnostics().tasks_completed);
// This task will be ignored.
transaction->ScheduleTask(base::BindOnce(
&TransactionTest::DummyOperation, base::Unretained(this), Status::OK()));
EXPECT_EQ(Transaction::FINISHED, transaction->state());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(1, transaction->diagnostics().tasks_completed);
}
TEST_F(TransactionTest, TimeoutPreemptive) {
std::unique_ptr<Connection> connection = CreateConnection();
Transaction* transaction =
CreateTransaction(connection.get(), /*id=*/0, /*object_store_ids=*/{},
blink::mojom::IDBTransactionMode::ReadWrite);
// No conflicting transactions, so coordinator will start it immediately:
EXPECT_EQ(Transaction::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(&TransactionTest::DummyOperation, base::Unretained(this),
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(
&TransactionTest::DummyOperation, base::Unretained(this), 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?
EXPECT_TRUE(transaction->RunTasks().has_value());
// 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(TransactionTest, TimeoutWithPriorities) {
struct {
int pri_1; // The priority of a running transaction.
int pri_2; // The priority of a transaction blocked on the running
// transaction.
bool can_timeout; // Whether the running transaction is a candidate for
// timeout.
} const test_cases[] = {
{0, 0, true}, {0, 1, true}, {1, 1, false}, {1, 0, true}, {2, 1, true}};
const std::vector<int64_t> object_store_ids{1};
int txn_id = 0;
int i = 0;
for (auto test_case : test_cases) {
SetDatabaseUnderTest(base::ASCIIToUTF16(base::StringPrintf("db_%d", i++)));
std::unique_ptr<Connection> connection = CreateConnection(test_case.pri_1);
Transaction* transaction =
CreateTransaction(connection.get(), txn_id++, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(Transaction::STARTED, transaction->state());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
// Schedule a task - timer won't be started until it's processed.
transaction->ScheduleTask(base::BindOnce(&TransactionTest::DummyOperation,
base::Unretained(this),
Status::OK()));
EXPECT_TRUE(base::test::RunUntil(
[&]() { return transaction->IsTimeoutTimerRunning(); }));
// Since the transaction isn't blocking another transaction, it's expected
// to do nothing when the timeout fires.
transaction->TimeoutFired();
EXPECT_EQ(0, transaction->timeout_strikes_);
EXPECT_EQ(Transaction::STARTED, transaction->state());
// Create a second transaction that's blocked on the first.
std::unique_ptr<Connection> connection2 = CreateConnection(test_case.pri_2);
CreateTransaction(connection2.get(),
/*id=*/txn_id++, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
// Now firing the timeout starts racking up strikes.
transaction->TimeoutFired();
EXPECT_EQ(test_case.can_timeout ? 1 : 0, transaction->timeout_strikes_);
// Clean up for the next iteration.
db_->ForceCloseAndRunTasks("The database is force-closed for testing.");
}
}
TEST_F(TransactionTest, WithoutPrioritization) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
features::kIdbPrioritizeForegroundClients);
const std::vector<int64_t> object_store_ids{1};
std::unique_ptr<Connection> low_pri_connection = CreateConnection(1);
std::unique_ptr<Connection> high_pri_connection = CreateConnection(0);
// Create transaction that is incidentally low priority.
// No conflicting transactions, so coordinator will start it immediately:
Transaction* low_pri_transaction =
CreateTransaction(low_pri_connection.get(), /*id=*/0, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(Transaction::STARTED, low_pri_transaction->state());
// Create second transaction, which is blocked.
Transaction* low_pri_transaction2 =
CreateTransaction(low_pri_connection.get(), /*id=*/1, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(Transaction::CREATED, low_pri_transaction2->state());
// Create a high priority transaction, which also queues up.
Transaction* high_pri_transaction =
CreateTransaction(high_pri_connection.get(), /*id=*/2, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(Transaction::CREATED, high_pri_transaction->state());
// Finish the first low priority transaction. Verify the order of queueing of
// other transactions.
low_pri_transaction->Abort(DatabaseError(
blink::mojom::IDBException::kAbortError, "Transaction aborted by user."));
EXPECT_EQ(Transaction::FINISHED, low_pri_transaction->state());
ASSERT_TRUE(base::test::RunUntil(
[&]() { return Transaction::STARTED == low_pri_transaction2->state(); }));
EXPECT_EQ(Transaction::CREATED, high_pri_transaction->state());
}
TEST_F(TransactionTest, WithPrioritization) {
base::test::ScopedFeatureList scoped_feature_list{
features::kIdbPrioritizeForegroundClients};
const std::vector<int64_t> object_store_ids{1};
std::unique_ptr<Connection> low_pri_connection = CreateConnection(1);
std::unique_ptr<Connection> high_pri_connection = CreateConnection(0);
// Create transaction that is incidentally low priority.
// No conflicting transactions, so coordinator will start it immediately:
Transaction* low_pri_transaction =
CreateTransaction(low_pri_connection.get(), /*id=*/0, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(Transaction::STARTED, low_pri_transaction->state());
// Create second transaction, which is blocked.
Transaction* low_pri_transaction2 =
CreateTransaction(low_pri_connection.get(), /*id=*/1, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(Transaction::CREATED, low_pri_transaction2->state());
// Create a couple high priority transactions, which skip ahead in the queue.
Transaction* high_pri_transaction =
CreateTransaction(high_pri_connection.get(), /*id=*/2, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(Transaction::CREATED, high_pri_transaction->state());
Transaction* high_pri_transaction2 =
CreateTransaction(high_pri_connection.get(), /*id=*/3, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(Transaction::CREATED, high_pri_transaction2->state());
// Finish the first low priority transaction. Verify the order of queueing of
// other transactions.
low_pri_transaction->Abort(DatabaseError(
blink::mojom::IDBException::kAbortError, "Transaction aborted by user."));
EXPECT_EQ(Transaction::FINISHED, low_pri_transaction->state());
ASSERT_TRUE(base::test::RunUntil(
[&]() { return Transaction::STARTED == high_pri_transaction->state(); }));
EXPECT_EQ(Transaction::CREATED, high_pri_transaction2->state());
EXPECT_EQ(Transaction::CREATED, low_pri_transaction2->state());
high_pri_transaction->Abort(DatabaseError(
blink::mojom::IDBException::kAbortError, "Transaction aborted by user."));
EXPECT_EQ(Transaction::FINISHED, high_pri_transaction->state());
ASSERT_TRUE(base::test::RunUntil([&]() {
return Transaction::STARTED == high_pri_transaction2->state();
}));
EXPECT_EQ(Transaction::CREATED, low_pri_transaction2->state());
high_pri_transaction2->Abort(DatabaseError(
blink::mojom::IDBException::kAbortError, "Transaction aborted by user."));
EXPECT_EQ(Transaction::FINISHED, high_pri_transaction2->state());
ASSERT_TRUE(base::test::RunUntil(
[&]() { return Transaction::STARTED == low_pri_transaction2->state(); }));
}
TEST_P(TransactionTestMode, ScheduleNormalTask) {
std::unique_ptr<Connection> connection = CreateConnection();
Transaction* transaction =
CreateTransaction(connection.get(), /*id=*/0, /*object_store_ids=*/{},
/*mode=*/GetParam());
EXPECT_FALSE(transaction->HasPendingTasks());
EXPECT_TRUE(transaction->IsTaskQueueEmpty());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
EXPECT_EQ(0, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
transaction->ScheduleTask(
blink::mojom::IDBTaskType::Normal,
base::BindOnce(&TransactionTest::DummyOperation, base::Unretained(this),
Status::OK()));
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
EXPECT_TRUE(transaction->HasPendingTasks());
EXPECT_FALSE(transaction->IsTaskQueueEmpty());
EXPECT_FALSE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
// Pump the message loop so that the transaction completes all pending tasks,
// otherwise it will defer the commit.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(transaction->HasPendingTasks());
EXPECT_TRUE(transaction->IsTaskQueueEmpty());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
EXPECT_EQ(Transaction::STARTED, transaction->state());
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(1, transaction->diagnostics().tasks_completed);
transaction->SetCommitFlag();
RunPostedTasks();
EXPECT_EQ(0UL, connection->transactions().size());
}
TEST_P(TransactionTestMode, TaskFails) {
std::unique_ptr<Connection> connection = CreateConnection();
Transaction* transaction =
CreateTransaction(connection.get(), /*id=*/0, /*object_store_ids=*/{},
/*mode=*/GetParam());
EXPECT_FALSE(transaction->HasPendingTasks());
EXPECT_TRUE(transaction->IsTaskQueueEmpty());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
EXPECT_EQ(0, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
db_ = nullptr;
transaction->ScheduleTask(
blink::mojom::IDBTaskType::Normal,
base::BindOnce(&TransactionTest::DummyOperation, base::Unretained(this),
Status::IOError("error")));
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
EXPECT_TRUE(transaction->HasPendingTasks());
EXPECT_FALSE(transaction->IsTaskQueueEmpty());
EXPECT_FALSE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
// Pump the message loop so that the transaction completes all pending tasks,
// otherwise it will defer the commit.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(transaction->HasPendingTasks());
EXPECT_TRUE(transaction->IsTaskQueueEmpty());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
/// Transaction aborted due to the error.
EXPECT_EQ(Transaction::FINISHED, transaction->state());
transaction->SetCommitFlag();
EXPECT_EQ(1, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(1, transaction->diagnostics().tasks_completed);
// An error was reported which deletes the bucket context.
EXPECT_FALSE(bucket_context_);
}
TEST_F(TransactionTest, SchedulePreemptiveTask) {
std::unique_ptr<Connection> connection = CreateConnection();
Transaction* transaction = CreateFakeTransactionWithCommitPhaseTwoError(
connection.get(), /*id=*/0, /*object_store_ids=*/{},
blink::mojom::IDBTransactionMode::ReadWrite, Status::Corruption("Ouch."));
db_ = nullptr;
EXPECT_FALSE(transaction->HasPendingTasks());
EXPECT_TRUE(transaction->IsTaskQueueEmpty());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
EXPECT_EQ(0, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
transaction->ScheduleTask(
blink::mojom::IDBTaskType::Preemptive,
base::BindOnce(&TransactionTest::DummyOperation, base::Unretained(this),
Status::OK()));
transaction->AddPreemptiveEvent();
EXPECT_TRUE(transaction->HasPendingTasks());
EXPECT_FALSE(transaction->IsTaskQueueEmpty());
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.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(transaction->HasPendingTasks());
EXPECT_TRUE(transaction->IsTaskQueueEmpty());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
EXPECT_EQ(Transaction::STARTED, transaction->state());
EXPECT_EQ(0, transaction->diagnostics().tasks_scheduled);
EXPECT_EQ(0, transaction->diagnostics().tasks_completed);
transaction->DidCompletePreemptiveEvent();
transaction->SetCommitFlag();
RunPostedTasks();
// The bucket context should have been destroyed via
// `OnDbReadyForDestruction`.
EXPECT_FALSE(bucket_context_);
}
TEST_P(TransactionTestMode, AbortPreemptive) {
std::unique_ptr<Connection> connection = CreateConnection();
Transaction* transaction =
CreateTransaction(connection.get(), /*id=*/0, /*object_store_ids=*/{},
/*mode=*/GetParam());
// No conflicting transactions, so coordinator will start it immediately:
EXPECT_EQ(Transaction::STARTED, transaction->state());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
transaction->ScheduleTask(
blink::mojom::IDBTaskType::Preemptive,
base::BindOnce(&TransactionTest::DummyOperation, base::Unretained(this),
Status::OK()));
EXPECT_EQ(0, transaction->pending_preemptive_events_);
transaction->AddPreemptiveEvent();
EXPECT_EQ(1, transaction->pending_preemptive_events_);
RunPostedTasks();
transaction->Abort(DatabaseError(blink::mojom::IDBException::kAbortError,
"Transaction aborted by user."));
EXPECT_EQ(Transaction::FINISHED, transaction->state());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_EQ(0, transaction->pending_preemptive_events_);
EXPECT_TRUE(transaction->preemptive_task_queue_.empty());
EXPECT_TRUE(transaction->task_queue_.empty());
EXPECT_FALSE(transaction->HasPendingTasks());
EXPECT_EQ(transaction->diagnostics().tasks_completed,
transaction->diagnostics().tasks_scheduled);
EXPECT_TRUE(transaction->backing_store_transaction_begun_);
EXPECT_TRUE(transaction->used_);
EXPECT_FALSE(transaction->is_commit_pending_);
// This task will be ignored.
transaction->ScheduleTask(base::BindOnce(
&TransactionTest::DummyOperation, base::Unretained(this), Status::OK()));
EXPECT_EQ(Transaction::FINISHED, transaction->state());
EXPECT_FALSE(transaction->IsTimeoutTimerRunning());
EXPECT_FALSE(transaction->HasPendingTasks());
EXPECT_EQ(transaction->diagnostics().tasks_completed,
transaction->diagnostics().tasks_scheduled);
}
static const blink::mojom::IDBTransactionMode kTestModes[] = {
blink::mojom::IDBTransactionMode::ReadOnly,
blink::mojom::IDBTransactionMode::ReadWrite};
INSTANTIATE_TEST_SUITE_P(Transactions,
TransactionTestMode,
::testing::ValuesIn(kTestModes));
TEST_F(TransactionTest, AbortCancelsLockRequest) {
std::unique_ptr<Connection> connection = CreateConnection();
const int64_t object_store_id = 1ll;
// Acquire a lock to block the transaction's lock acquisition.
std::vector<PartitionedLockManager::PartitionedLockRequest> lock_requests =
connection->database()->BuildLockRequestsForTransaction(
blink::mojom::IDBTransactionMode::ReadWrite, {object_store_id});
bool locks_received = false;
PartitionedLockHolder temp_lock_receiver;
lock_manager().AcquireLocks(lock_requests, temp_lock_receiver,
base::BindOnce(SetToTrue, &locks_received));
EXPECT_TRUE(locks_received);
// Create and register the transaction, which should request locks and wait
// for `temp_lock_receiver` to release the locks.
Transaction* transaction = CreateTransaction(
connection.get(), /*transaction_id=*/0, {object_store_id},
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(transaction->state(), Transaction::CREATED);
// Abort the transaction, which should cancel the
// RegisterAndScheduleTransaction() pending lock request.
transaction->Abort(DatabaseError(blink::mojom::IDBException::kUnknownError));
EXPECT_EQ(transaction->state(), Transaction::FINISHED);
// Clear `temp_lock_receiver` so we can test later that all locks have
// cleared.
temp_lock_receiver.locks.clear();
// Verify that the locks are available for acquisition again, as the
// transaction should have cancelled its lock request.
locks_received = false;
lock_manager().AcquireLocks(lock_requests, temp_lock_receiver,
base::BindOnce(SetToTrue, &locks_received));
EXPECT_TRUE(locks_received);
}
TEST_F(TransactionTest, PostedStartTaskRunAfterAbort) {
std::unique_ptr<Connection> connection = CreateConnection();
int64_t id = 0;
const std::vector<int64_t> object_store_ids = {1ll};
Transaction* transaction1 =
CreateTransaction(connection.get(), id, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(transaction1->state(), Transaction::STARTED);
// Register another transaction, which will block on the first transaction.
Transaction* transaction2 =
CreateTransaction(connection.get(), ++id, object_store_ids,
blink::mojom::IDBTransactionMode::ReadWrite);
EXPECT_EQ(transaction2->state(), Transaction::CREATED);
// Flush posted tasks before making the Abort calls since there are
// posted RunTasksForDatabase() tasks which, if we waited to run them
// until after Abort is called, would destroy our transactions and mask
// a potential race condition.
RunPostedTasks();
// Abort all of the transactions, which should cause the second transaction's
// posted Start() task to run.
connection->AbortAllTransactions(
DatabaseError(blink::mojom::IDBException::kUnknownError));
EXPECT_EQ(transaction2->state(), Transaction::FINISHED);
// Run tasks to ensure Start() is called but does not DCHECK.
RunPostedTasks();
// It's not safe to check the state of the transaction at this point since it
// is freed when the Database::RunTasks call happens via the posted
// RunTasksForDatabase task.
}
TEST_F(TransactionTest, IsTransactionBlockingOtherClients) {
std::unique_ptr<Connection> connection = CreateConnection();
const std::vector<int64_t> object_store_ids = {1ll};
Transaction* transaction = CreateTransaction(
connection.get(),
/*id=*/0, object_store_ids, blink::mojom::IDBTransactionMode::ReadWrite);
// Register a transaction with ReadWrite mode to object store 1.
// The transaction should be started and it's not blocking any others.
EXPECT_EQ(transaction->state(), Transaction::STARTED);
EXPECT_FALSE(transaction->IsTransactionBlockingOtherClients());
Transaction* transaction2 = CreateTransaction(
connection.get(),
/*id=*/1, object_store_ids, blink::mojom::IDBTransactionMode::ReadWrite);
// Register another transaction with ReadWrite mode to the same object store.
// The transaction should be blocked in `CREATED` state, but the previous
// transaction is *not* blocking other clients because it's the same client.
EXPECT_EQ(transaction2->state(), Transaction::CREATED);
EXPECT_FALSE(transaction->IsTransactionBlockingOtherClients());
// Register a very similar connection, but with a *different* client. Now this
// one is blocking and `IsTransactionBlockingOtherClients` should be true.
auto connection2 = CreateConnection();
Transaction* transaction3 = CreateTransaction(
connection2.get(),
/*id=*/1, object_store_ids, blink::mojom::IDBTransactionMode::ReadWrite);
RunPostedTasks();
// Abort the blocked transaction, and the previous transaction should not be
// blocking others anymore.
transaction3->Abort(DatabaseError(blink::mojom::IDBException::kUnknownError));
EXPECT_EQ(transaction3->state(), Transaction::FINISHED);
RunPostedTasks();
EXPECT_FALSE(transaction->IsTransactionBlockingOtherClients());
}
} // namespace content::indexed_db