// Copyright (c) 2020-present, Facebook, Inc. All rights reserved. // This source code is licensed under both the GPLv2 (found in the // COPYING file in the root directory) and Apache 2.0 License // (found in the LICENSE.Apache file in the root directory). #include "utilities/transactions/lock/point/point_lock_manager_test.h" #include "utilities/transactions/lock/point/any_lock_manager_test.h" namespace ROCKSDB_NAMESPACE { struct SpotLockManagerTestParam { bool use_per_key_point_lock_manager; int deadlock_timeout_us; }; // Define operator<< for SpotLockManagerTestParam to stop valgrind from // complaining uinitialized value when printing SpotLockManagerTestParam. std::ostream& operator<<(std::ostream& os, const SpotLockManagerTestParam& param) { os << "use_per_key_point_lock_manager: " << param.use_per_key_point_lock_manager << ", deadlock_timeout_us: " << param.deadlock_timeout_us; return os; } // including test for both PointLockManager and PerKeyPointLockManager class SpotLockManagerTest : public PointLockManagerTest, public testing::WithParamInterface { public: void SetUp() override { init(); // If a custom setup function was provided, use it. Otherwise, use what we // have inherited. auto param = GetParam(); if (param.use_per_key_point_lock_manager) { locker_.reset(new PerKeyPointLockManager( static_cast(db_), txndb_opt_)); } else { locker_.reset(new PointLockManager( static_cast(db_), txndb_opt_)); } deadlock_timeout_us = param.deadlock_timeout_us; } }; // This test is not applicable for Range Lock manager as Range Lock Manager // operates on Column Families, not their ids. TEST_P(SpotLockManagerTest, LockNonExistingColumnFamily) { MockColumnFamilyHandle cf(1024); locker_->RemoveColumnFamily(&cf); auto txn = NewTxn(); auto s = locker_->TryLock(txn, 1024, "k", env_, true); ASSERT_TRUE(s.IsInvalidArgument()); ASSERT_STREQ(s.getState(), "Column family id not found: 1024"); delete txn; } TEST_P(SpotLockManagerTest, LockStatus) { MockColumnFamilyHandle cf1(1024), cf2(2048); locker_->AddColumnFamily(&cf1); locker_->AddColumnFamily(&cf2); auto txn1 = NewTxn(); ASSERT_OK(locker_->TryLock(txn1, 1024, "k1", env_, true)); ASSERT_OK(locker_->TryLock(txn1, 2048, "k1", env_, true)); auto txn2 = NewTxn(); ASSERT_OK(locker_->TryLock(txn2, 1024, "k2", env_, false)); ASSERT_OK(locker_->TryLock(txn2, 2048, "k2", env_, false)); auto s = locker_->GetPointLockStatus(); ASSERT_EQ(s.size(), 4u); for (uint32_t cf_id : {1024, 2048}) { ASSERT_EQ(s.count(cf_id), 2u); auto range = s.equal_range(cf_id); for (auto it = range.first; it != range.second; it++) { ASSERT_TRUE(it->second.key == "k1" || it->second.key == "k2"); if (it->second.key == "k1") { ASSERT_EQ(it->second.exclusive, true); ASSERT_EQ(it->second.ids.size(), 1u); ASSERT_EQ(it->second.ids[0], txn1->GetID()); } else if (it->second.key == "k2") { ASSERT_EQ(it->second.exclusive, false); ASSERT_EQ(it->second.ids.size(), 1u); ASSERT_EQ(it->second.ids[0], txn2->GetID()); } } } // Cleanup locker_->UnLock(txn1, 1024, "k1", env_); locker_->UnLock(txn1, 2048, "k1", env_); locker_->UnLock(txn2, 1024, "k2", env_); locker_->UnLock(txn2, 2048, "k2", env_); delete txn1; delete txn2; } TEST_P(SpotLockManagerTest, UnlockExclusive) { MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); auto txn1 = NewTxn(); ASSERT_OK(locker_->TryLock(txn1, 1, "k", env_, true)); locker_->UnLock(txn1, 1, "k", env_); auto txn2 = NewTxn(); ASSERT_OK(locker_->TryLock(txn2, 1, "k", env_, true)); // Cleanup locker_->UnLock(txn2, 1, "k", env_); delete txn1; delete txn2; } TEST_P(SpotLockManagerTest, UnlockShared) { MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); auto txn1 = NewTxn(); ASSERT_OK(locker_->TryLock(txn1, 1, "k", env_, false)); locker_->UnLock(txn1, 1, "k", env_); auto txn2 = NewTxn(); ASSERT_OK(locker_->TryLock(txn2, 1, "k", env_, true)); // Cleanup locker_->UnLock(txn2, 1, "k", env_); delete txn1; delete txn2; } // This test doesn't work with Range Lock Manager, because Range Lock Manager // doesn't support deadlock_detect_depth. TEST_P(SpotLockManagerTest, DeadlockDepthExceeded) { // Tests that when detecting deadlock, if the detection depth is exceeded, // it's also viewed as deadlock. MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.deadlock_detect_depth = 1; txn_opt.lock_timeout = kLongTxnTimeoutMs; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); auto txn3 = NewTxn(txn_opt); auto txn4 = NewTxn(txn_opt); // "a ->(k) b" means transaction a is waiting for transaction b to release // the held lock on key k. // txn4 ->(k3) -> txn3 ->(k2) txn2 ->(k1) txn1 // txn3's deadlock detection will exceed the detection depth 1, // which will be viewed as a deadlock. // NOTE: // txn4 ->(k3) -> txn3 must be set up before // txn3 ->(k2) -> txn2, because to trigger deadlock detection for txn3, // it must have another txn waiting on it, which is txn4 in this case. ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); port::Thread t1; BlockUntilWaitingTxn(wait_sync_point_name_, t1, [&]() { ASSERT_OK(locker_->TryLock(txn2, 1, "k2", env_, true)); // block because txn1 is holding a lock on k1. ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, true)); }); ASSERT_OK(locker_->TryLock(txn3, 1, "k3", env_, true)); port::Thread t2; BlockUntilWaitingTxn(wait_sync_point_name_, t2, [&]() { // block because txn3 is holding a lock on k1. ASSERT_OK(locker_->TryLock(txn4, 1, "k3", env_, true)); }); auto s = locker_->TryLock(txn3, 1, "k2", env_, true); ASSERT_TRUE(s.IsBusy()); ASSERT_EQ(s.subcode(), Status::SubCode::kDeadlock); std::vector deadlock_paths = locker_->GetDeadlockInfoBuffer(); ASSERT_EQ(deadlock_paths.size(), 1u); ASSERT_TRUE(deadlock_paths[0].limit_exceeded); locker_->UnLock(txn1, 1, "k1", env_); locker_->UnLock(txn3, 1, "k3", env_); t1.join(); t2.join(); locker_->UnLock(txn2, 1, "k2", env_); locker_->UnLock(txn2, 1, "k1", env_); locker_->UnLock(txn4, 1, "k3", env_); delete txn4; delete txn3; delete txn2; delete txn1; } TEST_P(SpotLockManagerTest, PrioritizedLockUpgradeWithExclusiveLock) { // Tests that a lock upgrade request is prioritized over other lock requests. // txn1 acquires shared lock on k1. // txn2 acquires exclusive lock on k1. // txn1 acquires exclusive locks k1 successfully MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.lock_timeout = kLongTxnTimeoutMs; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, false)); // txn2 tries to lock k1 exclusively, will be blocked. port::Thread t; BlockUntilWaitingTxn(wait_sync_point_name_, t, [this, &txn2]() { // block because txn1 is holding a shared lock on k1. ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, true)); }); // verify lock upgrade successfully ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); // unlock txn1, so txn2 could proceed locker_->UnLock(txn1, 1, "k1", env_); // Cleanup t.join(); // Cleanup locker_->UnLock(txn2, 1, "k1", env_); delete txn2; delete txn1; } TEST_P(SpotLockManagerTest, PrioritizedLockUpgradeWithExclusiveLockAndSharedLock) { // Tests that lock upgrade is prioritized when mixed with shared and exclusive // locks requests // txn1 acquires shared lock on k1. // txn2 acquires shared lock on k1. // txn3 acquires exclusive lock on k1. // txn1 acquires exclusive locks k1 <- request granted after txn2 release the // lock MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.lock_timeout = kLongTxnTimeoutMs; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); auto txn3 = NewTxn(txn_opt); ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, false)); ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, false)); // txn3 tries to lock k1 exclusively, will be blocked. port::Thread txn3_thread; BlockUntilWaitingTxn(wait_sync_point_name_, txn3_thread, [this, &txn3]() { // block because txn1 and txn2 are holding a shared lock on k1. ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, true)); }); // Verify txn3 is blocked ASSERT_TRUE(txn3_thread.joinable()); // txn1 tries to lock k1 exclusively, will be blocked. port::Thread txn1_thread; BlockUntilWaitingTxn(wait_sync_point_name_, txn1_thread, [this, &txn1]() { // block because txn1 and txn2 are holding a shared lock on k1. ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); }); // Verify txn1 is blocked ASSERT_TRUE(txn1_thread.joinable()); // Unlock txn2, so txn1 could proceed locker_->UnLock(txn2, 1, "k1", env_); txn1_thread.join(); // Unlock txn1, so txn3 could proceed locker_->UnLock(txn1, 1, "k1", env_); txn3_thread.join(); // Cleanup locker_->UnLock(txn3, 1, "k1", env_); delete txn3; delete txn2; delete txn1; } TEST_P(SpotLockManagerTest, Deadlock_MultipleUpgrade) { // Tests that deadlock can be detected for shared locks and exclusive locks // mixed Deadlock scenario: // txn1 acquires shared lock on k1. // txn2 acquires shared lock on k1. // txn1 acquires exclusive locks k1 // txn2 acquires exclusive locks k1 <- dead lock detected MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.lock_timeout = kLongTxnTimeoutMs; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, false)); ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, false)); // txn1 tries to lock k1 exclusively, will be blocked. port::Thread t; BlockUntilWaitingTxn(wait_sync_point_name_, t, [this, &txn1]() { // block because txn2 is holding a shared lock on k1. ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); }); auto s = locker_->TryLock(txn2, 1, "k1", env_, true); ASSERT_TRUE(s.IsBusy()); ASSERT_EQ(s.subcode(), Status::SubCode::kDeadlock); std::vector deadlock_paths = locker_->GetDeadlockInfoBuffer(); ASSERT_EQ(deadlock_paths.size(), 1u); ASSERT_FALSE(deadlock_paths[0].limit_exceeded); std::vector deadlocks = deadlock_paths[0].path; ASSERT_EQ(deadlocks.size(), 2u); ASSERT_EQ(deadlocks[0].m_txn_id, txn1->GetID()); ASSERT_EQ(deadlocks[0].m_cf_id, 1u); ASSERT_TRUE(deadlocks[0].m_exclusive); ASSERT_EQ(deadlocks[0].m_waiting_key, "k1"); ASSERT_EQ(deadlocks[1].m_txn_id, txn2->GetID()); ASSERT_EQ(deadlocks[1].m_cf_id, 1u); ASSERT_TRUE(deadlocks[1].m_exclusive); ASSERT_EQ(deadlocks[1].m_waiting_key, "k1"); locker_->UnLock(txn2, 1, "k1", env_); t.join(); // Cleanup locker_->UnLock(txn1, 1, "k1", env_); delete txn2; delete txn1; } TEST_P(SpotLockManagerTest, Deadlock_MultipleUpgradeInterleaveExclusive) { // Tests that deadlock can be detected for shared locks and exclusive locks // mixed Deadlock scenario: // txn1 acquires shared lock on k1. // txn2 acquires shared lock on k1. // txn3 acquires exclusive lock on k1. // txn1 acquires exclusive locks k1 <- request granted after txn2 release the // lock. // txn2 acquires exclusive locks k1 <- dead lock detected MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.lock_timeout = kLongTxnTimeoutMs; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); auto txn3 = NewTxn(txn_opt); ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, false)); ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, false)); // txn3 tries to lock k1 exclusively, will be blocked. port::Thread txn3_thread; BlockUntilWaitingTxn(wait_sync_point_name_, txn3_thread, [this, &txn3]() { // block because txn1 and txn2 are holding a shared lock on k1. ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, true)); }); // Verify txn3 is blocked ASSERT_TRUE(txn3_thread.joinable()); // txn1 tries to lock k1 exclusively, will be blocked. port::Thread txn1_thread; BlockUntilWaitingTxn(wait_sync_point_name_, txn1_thread, [this, &txn1]() { // block because txn1 and txn2 are holding a shared lock on k1. ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); }); // Verify txn1 is blocked ASSERT_TRUE(txn1_thread.joinable()); auto s = locker_->TryLock(txn2, 1, "k1", env_, true); ASSERT_TRUE(s.IsBusy()); ASSERT_EQ(s.subcode(), Status::SubCode::kDeadlock); std::vector deadlock_paths = locker_->GetDeadlockInfoBuffer(); ASSERT_EQ(deadlock_paths.size(), 1u); ASSERT_FALSE(deadlock_paths[0].limit_exceeded); std::vector deadlocks = deadlock_paths[0].path; ASSERT_EQ(deadlocks.size(), 2u); ASSERT_EQ(deadlocks[0].m_txn_id, txn1->GetID()); ASSERT_EQ(deadlocks[0].m_cf_id, 1u); ASSERT_TRUE(deadlocks[0].m_exclusive); ASSERT_EQ(deadlocks[0].m_waiting_key, "k1"); ASSERT_EQ(deadlocks[1].m_txn_id, txn2->GetID()); ASSERT_EQ(deadlocks[1].m_cf_id, 1u); ASSERT_TRUE(deadlocks[1].m_exclusive); ASSERT_EQ(deadlocks[1].m_waiting_key, "k1"); // Unlock txn2, so txn1 could proceed locker_->UnLock(txn2, 1, "k1", env_); txn1_thread.join(); // Unlock txn1, so txn3 could proceed locker_->UnLock(txn1, 1, "k1", env_); txn3_thread.join(); // Cleanup locker_->UnLock(txn3, 1, "k1", env_); delete txn3; delete txn2; delete txn1; } class PerKeyPointLockManagerTest : public PointLockManagerTest { public: void SetUp() override { init(); cf_ = std::make_unique(1); txn_opt_.deadlock_detect = true; // by default use long timeout and disable expiration txn_opt_.lock_timeout = kLongTxnTimeoutMs; txn_opt_.expiration = -1; // CAUTION: This test creates a separate lock manager object (right, NOT // the one that the TransactionDB is using!), and runs tests on it. locker_.reset(new PerKeyPointLockManager( static_cast(db_), txndb_opt_)); locker_->AddColumnFamily(cf_.get()); } TransactionOptions txn_opt_; std::unique_ptr cf_; }; TEST_F(PerKeyPointLockManagerTest, LockEfficiency) { // Create multiple transactions, each acquire exclusive lock on the same key std::vector txns; std::vector blockingThreads; // Count the total number of wait sync point calls std::atomic_int wait_sync_point_times = 0; SyncPoint::GetInstance()->SetCallBack( wait_sync_point_name_, [&wait_sync_point_times](void* /*arg*/) { wait_sync_point_times++; }); SyncPoint::GetInstance()->EnableProcessing(); constexpr auto num_of_txn = 10; // create 10 transactions, each of them try to acquire exclusive lock on the // same key for (int i = 0; i < num_of_txn; i++) { auto txn = NewTxn(txn_opt_); txns.push_back(txn); if (i == 0) { // txn0 acquires the lock, so the rest of the transactions could block ASSERT_OK(locker_->TryLock(txn, 1, "k1", env_, true)); } else { blockingThreads.emplace_back([this, txn]() { // block because first txn is holding an exclusive lock on k1. ASSERT_OK(locker_->TryLock(txn, 1, "k1", env_, true)); }); } // wait for transaction i to be blocked while (wait_sync_point_times.load() < i) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } // unlock the key, so next transaction could take the lock. locker_->UnLock(txns[0], 1, "k1", env_); auto num_of_blocking_thread = num_of_txn - 1; for (int i = 0; i < num_of_blocking_thread; i++) { // validate the thread is finished blockingThreads[i].join(); auto num_of_threads_completed = i + 1; for (int j = 0; j < num_of_blocking_thread; j++) { if (j < num_of_threads_completed) { // validate the thread is no longer joinable ASSERT_FALSE(blockingThreads[j].joinable()); } else { // validate the rest of the threads are still joinable ASSERT_TRUE(blockingThreads[j].joinable()); } } // unlock the key, so next transaction could take the lock. locker_->UnLock(txns[i + 1], 1, "k1", env_); } ASSERT_EQ(wait_sync_point_times.load(), num_of_blocking_thread); SyncPoint::GetInstance()->DisableProcessing(); SyncPoint::GetInstance()->ClearAllCallBacks(); for (int i = 0; i < num_of_txn; i++) { delete txns[num_of_txn - i - 1]; } } TEST_F(PerKeyPointLockManagerTest, LockFairness) { // Create multiple transactions requesting locks on the same key, validate // that they are executed in FIFO order // txn0 acquires exclusive lock on k1. // txn1 acquires shared lock on k1. // txn2 acquires shared lock on k1. // txn3 acquires exclusive lock on k1. // txn4 acquires shared lock on k1. // txn5 acquires exclusive lock on k1. // txn6 acquires exclusive lock on k1. // txn7 acquires shared lock on k1. // txn8 acquires shared lock on k1. // txn9 acquires exclusive lock on k1. std::vector txns; std::vector blockingThreads; // Count the total number of wait sync point calls std::atomic_int wait_sync_point_times = 0; SyncPoint::GetInstance()->SetCallBack( wait_sync_point_name_, [&wait_sync_point_times](void* /*arg*/) { wait_sync_point_times++; }); SyncPoint::GetInstance()->EnableProcessing(); constexpr auto num_of_txn = 10; std::vector txn_lock_types = {true, false, false, true, false, true, true, false, false, true}; // create 10 transactions, each of them try to acquire exclusive lock on the // same key for (int i = 0; i < num_of_txn; i++) { auto txn = NewTxn(txn_opt_); txns.push_back(txn); if (i == 0) { // txn0 acquires the lock, so the rest of the transactions would block ASSERT_OK(locker_->TryLock(txn, 1, "k1", env_, txn_lock_types[0])); } else { blockingThreads.emplace_back([this, txn, type = txn_lock_types[i]]() { ASSERT_OK(locker_->TryLock(txn, 1, "k1", env_, type)); }); } // wait for transaction i to be blocked while (wait_sync_point_times.load() < i) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } auto num_of_blocking_thread = num_of_txn - 1; auto thread_idx = 0; auto txn_idx = 0; auto unlockTxn = [&]() { // unlock the key in transaction. locker_->UnLock(txns[txn_idx++], 1, "k1", env_); }; auto validateLockTakenByNextTxn = [&]() { // validate the thread is finished blockingThreads[thread_idx++].join(); }; auto stillWaitingForLock = [&]() { // validate the thread is no longer joinable ASSERT_TRUE(blockingThreads[thread_idx].joinable()); }; // unlock the key, so next group of transactions could take the lock. unlockTxn(); // txn1 acquires shared lock on k1. // txn2 acquires shared lock on k1. validateLockTakenByNextTxn(); validateLockTakenByNextTxn(); // txn3 acquires exclusive lock on k1. stillWaitingForLock(); unlockTxn(); unlockTxn(); validateLockTakenByNextTxn(); // txn4 acquires shared lock on k1. stillWaitingForLock(); unlockTxn(); validateLockTakenByNextTxn(); // txn5 acquires exclusive lock on k1. stillWaitingForLock(); unlockTxn(); validateLockTakenByNextTxn(); // txn6 acquires exclusive lock on k1. stillWaitingForLock(); unlockTxn(); validateLockTakenByNextTxn(); // txn7 acquires shared lock on k1. // txn8 acquires shared lock on k1. stillWaitingForLock(); unlockTxn(); validateLockTakenByNextTxn(); validateLockTakenByNextTxn(); // txn9 acquires exclusive lock on k1. stillWaitingForLock(); unlockTxn(); unlockTxn(); validateLockTakenByNextTxn(); // clean up unlockTxn(); ASSERT_EQ(wait_sync_point_times.load(), num_of_blocking_thread); SyncPoint::GetInstance()->DisableProcessing(); SyncPoint::GetInstance()->ClearAllCallBacks(); for (int i = 0; i < num_of_txn; i++) { delete txns[num_of_txn - i - 1]; } } TEST_F(PerKeyPointLockManagerTest, FIFO) { // validate S, X, S lock order would be executed in FIFO order // txn1 acquires shared lock on k1. // txn2 acquires exclusive lock on k1. // txn3 acquires shared lock on k1. std::vector txns; std::vector blockingThreads; // Count the total number of wait sync point calls std::atomic_int wait_sync_point_times = 0; SyncPoint::GetInstance()->SetCallBack( wait_sync_point_name_, [&wait_sync_point_times](void* /*arg*/) { wait_sync_point_times++; }); SyncPoint::GetInstance()->EnableProcessing(); constexpr auto num_of_txn = 3; std::vector txn_lock_types = {false, true, false}; // create 3 transactions, each of them try to acquire exclusive lock on the // same key for (int i = 0; i < num_of_txn; i++) { auto txn = NewTxn(txn_opt_); txns.push_back(txn); if (i == 0) { // txn0 acquires the lock, so the rest of the transactions would block ASSERT_OK(locker_->TryLock(txn, 1, "k1", env_, txn_lock_types[0])); } else { blockingThreads.emplace_back([this, txn, type = txn_lock_types[i]]() { ASSERT_OK(locker_->TryLock(txn, 1, "k1", env_, type)); }); } // wait for transaction i to be blocked while (wait_sync_point_times.load() < i) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } auto num_of_blocking_thread = num_of_txn - 1; auto thread_idx = 0; auto txn_idx = 0; auto unlockTxn = [&]() { // unlock the key in transaction. locker_->UnLock(txns[txn_idx++], 1, "k1", env_); }; auto validateLockTakenByNextTxn = [&]() { // validate the thread is finished blockingThreads[thread_idx++].join(); }; auto stillWaitingForLock = [&]() { // validate the thread is no longer joinable ASSERT_TRUE(blockingThreads[thread_idx].joinable()); }; // unlock the key, so next group of transactions could take the lock. stillWaitingForLock(); unlockTxn(); // txn1 acquires exclusive lock on k1. validateLockTakenByNextTxn(); // txn2 acquires shared lock on k1. stillWaitingForLock(); unlockTxn(); validateLockTakenByNextTxn(); // clean up unlockTxn(); ASSERT_EQ(wait_sync_point_times.load(), num_of_blocking_thread); SyncPoint::GetInstance()->DisableProcessing(); SyncPoint::GetInstance()->ClearAllCallBacks(); for (int i = 0; i < num_of_txn; i++) { delete txns[num_of_txn - i - 1]; } } TEST_P(SpotLockManagerTest, LockDownGradeWithOtherLockRequests) { // Test lock down grade always succeeds, even if there are other lock requests // waiting for the same lock. MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.lock_timeout = kLongTxnTimeoutMs; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); for (bool exclusive : {true, false}) { ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); port::Thread t; BlockUntilWaitingTxn(wait_sync_point_name_, t, [this, &txn2, exclusive]() { // block because txn1 is holding a exclusive lock on k1. ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, exclusive)); }); // txn1 downgrades the lock to shared lock, so txn2 could proceed ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, false)); locker_->UnLock(txn1, 1, "k1", env_); t.join(); locker_->UnLock(txn2, 1, "k1", env_); } // clean up delete txn2; delete txn1; } TEST_P(SpotLockManagerTest, LockTimeout) { // Test lock timeout // txn1 acquires an exclusive lock on k1 successfully. // txn2 try to acquire a lock on k1, but timedout. MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.lock_timeout = kShortTxnTimeoutMs; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); for (bool exclusive : {true, false}) { auto ret = locker_->TryLock(txn2, 1, "k1", env_, exclusive); ASSERT_TRUE(ret.IsTimedOut()); } // clean up locker_->UnLock(txn1, 1, "k1", env_); delete txn2; delete txn1; } TEST_P(SpotLockManagerTest, ExpiredLockStolenAfterTimeout) { // validate an expired lock can be stolen by another transaction that timed // out on the lock. // txn1 acquires an exclusive lock on k1 successfully with a short expiration // time. // txn2 try to acquire a shared lock on k1 with timeout that is slightly // longer than the txn1 expiration. // Validate txn2 will take the lock. MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.expiration = 1000; txn_opt.lock_timeout = 1000 * 2; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); port::Thread t1; BlockUntilWaitingTxn(wait_sync_point_name_, t1, [this, &txn2]() { // block because txn1 is holding an exclusive lock on k1. ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, false)); }); t1.join(); // clean up locker_->UnLock(txn2, 1, "k1", env_); locker_->UnLock(txn1, 1, "k1", env_); delete txn2; delete txn1; } // Try to block until transaction enters waiting state. // However due to timing, it could fail, so return true if succeeded, false // otherwise. bool TryBlockUntilWaitingTxn(const char* sync_point_name, port::Thread& t, std::function function) { std::atomic reached(false); ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack( sync_point_name, [&](void* /*arg*/) { reached.store(true); }); ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing(); // As the lifetime of the complete variable could go beyond the scope of this // function, so we wrap it in a shared_ptr, and copy it into the lambda std::shared_ptr> complete = std::make_shared>(false); t = port::Thread([complete, &function]() { function(); complete->store(true); }); auto ret = false; while (true) { if (complete->load()) { // function completed, before sync point was reached, return false t.join(); ret = false; break; } if (reached.load()) { // sync point was reached before function completed, return true ret = true; break; } } ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing(); ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks(); return ret; } TEST_F(PerKeyPointLockManagerTest, LockStealAfterExpirationExclusive) { // There are multiple transactions waiting for the same lock. // txn1 acquires an exclusive lock on k1 successfully with a short expiration // time. // txn2 try to acquire an exclusive lock on k1, before expiration time, // so it is blocked and waits for txn1 lock expired. // txn3 try to acquire an exclusive lock on k1 after txn1 lock expires, FIFO // order is respected. // txn2 is woken up and takes the lock. unlock txn2, txn3 should proceed. txn_opt_.expiration = 1000; auto txn1 = NewTxn(txn_opt_); txn_opt_.expiration = -1; auto txn2 = NewTxn(txn_opt_); auto txn3 = NewTxn(txn_opt_); port::Thread t1; auto retry_times = 10; // Use a loop to reduce test flakiness. // that the test is flaky because the txn2 thread start could be delayed until // txn1 lock expired. In that case, txn2 will not enter into wait state, which // will defeat the test purpose. Use a loop to retry a few times, until it is // able to enter into wait state. while (retry_times--) { ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); if (TryBlockUntilWaitingTxn(wait_sync_point_name_, t1, [this, &txn2]() { // block because txn1 is holding a shared lock on k1. ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, true)); })) { break; } // failed, retry again locker_->UnLock(txn1, 1, "k1", env_); locker_->UnLock(txn2, 1, "k1", env_); } // make sure txn2 is able to reach the wait state before proceed ASSERT_GT(retry_times, 0); // txn3 try to acquire an exclusive lock on k1, FIFO order is respected. port::Thread t2; BlockUntilWaitingTxn(wait_sync_point_name_, t2, [this, &txn3]() { // block because txn1 is holding an exclusive lock on k1. ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, true)); }); // validate txn2 is woken up and takes the lock t1.join(); // unlock txn2, txn3 should proceed locker_->UnLock(txn2, 1, "k1", env_); t2.join(); // clean up locker_->UnLock(txn3, 1, "k1", env_); delete txn3; delete txn2; delete txn1; } TEST_F(PerKeyPointLockManagerTest, LockStealAfterExpirationShared) { // There are multiple transactions waiting for the same lock. // txn1 acquires a shared lock on k1 successfully with a short expiration // time. // txn2 try to acquire an exclusive lock on k1, before expiration time, // so it is blocked and waits for txn1 lock expired. // txn3 try to acquire a shared lock on k1 after txn1 lock expires, FIFO // order is respected. // txn2 is woken up and takes the lock. unlock txn2, txn3 should proceed. txn_opt_.expiration = 1000; auto txn1 = NewTxn(txn_opt_); txn_opt_.expiration = -1; auto txn2 = NewTxn(txn_opt_); auto txn3 = NewTxn(txn_opt_); port::Thread t1; auto retry_times = 10; // Use a loop to reduce test flakiness. // that the test is flaky because the txn2 thread start could be delayed until // txn1 lock expired. In that case, txn2 will not enter into wait state, which // will defeat the test purpose. Use a loop to retry a few times, until it is // able to enter into wait state. while (retry_times--) { ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, false)); if (TryBlockUntilWaitingTxn(wait_sync_point_name_, t1, [this, &txn2]() { // block because txn1 is holding an exclusive lock on k1. ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, true)); })) { break; } // failed, retry again locker_->UnLock(txn1, 1, "k1", env_); locker_->UnLock(txn2, 1, "k1", env_); } // make sure txn2 is able to reach the wait state before proceed ASSERT_GT(retry_times, 0); // txn3 try to acquire an exclusive lock on k1, FIFO order is respected. port::Thread t2; BlockUntilWaitingTxn(wait_sync_point_name_, t2, [this, &txn3]() { // block because txn1 is holding an exclusive lock on k1. ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, false)); }); // validate txn2 is woken up and takes the lock t1.join(); // unlock txn2, txn3 should proceed locker_->UnLock(txn2, 1, "k1", env_); t2.join(); // clean up locker_->UnLock(txn3, 1, "k1", env_); delete txn3; delete txn2; delete txn1; } TEST_F(PerKeyPointLockManagerTest, DeadLockOnWaiter) { // Txn1 acquires exclusive lock on k1 // Txn3 acquires shared lock on k2 // Txn2 tries to acquire exclusive lock on k1, waiting in the waiter queue. // Txn3 tries to acquire exclusive lock on k1, waiting in the waiter queue. // Txn3 depends on both Txn1 and Txn2. Txn1 unlocks k1. // Txn2 takes the lock k1, and tries to acquire lock k2. // Now Txn2 depends on Txn3. // Deadlock is detected, and Txn2 is aborted. auto txn1 = NewTxn(txn_opt_); auto txn2 = NewTxn(txn_opt_); auto txn3 = NewTxn(txn_opt_); ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); ASSERT_OK(locker_->TryLock(txn3, 1, "k2", env_, false)); port::Thread t1; BlockUntilWaitingTxn(wait_sync_point_name_, t1, [this, &txn2]() { ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, true)); auto s = locker_->TryLock(txn2, 1, "k2", env_, true); ASSERT_TRUE(s.IsDeadlock()); }); port::Thread t2; BlockUntilWaitingTxn(wait_sync_point_name_, t2, [this, &txn3]() { ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, true)); }); locker_->UnLock(txn1, 1, "k1", env_); t1.join(); locker_->UnLock(txn2, 1, "k1", env_); t2.join(); // clean up locker_->UnLock(txn3, 1, "k1", env_); locker_->UnLock(txn3, 1, "k2", env_); delete txn3; delete txn2; delete txn1; } TEST_F(PerKeyPointLockManagerTest, SharedLockRaceCondition) { // Verify a shared lock race condition is handled properly. // When there are waiters in the queue, and all of them are shared waiters, // and no one has taken the lock and all of them just got woken up and not // yet taken the lock yet. A new shared lock request should be granted // directly, without wait in the queue. If it did, It would not be woken up // until the last shared lock is released. // Disable deadlock detection timeout to prevent test flakyness. deadlock_timeout_us = 0; auto txn1 = NewTxn(txn_opt_); auto txn2 = NewTxn(txn_opt_); auto txn3 = NewTxn(txn_opt_); SyncPoint::GetInstance()->DisableProcessing(); SyncPoint::GetInstance()->LoadDependency( {{"PerKeyPointLockManager::AcquireWithTimeout:AfterWokenUp", "PerKeyPointLockManagerTest::SharedLockRaceCondition:" "BeforeNewSharedLockRequest"}, {"PerKeyPointLockManagerTest::SharedLockRaceCondition:" "AfterNewSharedLockRequest", "PerKeyPointLockManager::AcquireWithTimeout:BeforeTakeLock"}}); std::atomic reached(false); SyncPoint::GetInstance()->SetCallBack( wait_sync_point_name_, [&reached](void* /*arg*/) { reached.store(true); }); SyncPoint::GetInstance()->EnableProcessing(); // txn1 acquires an exclusive lock on k1, so that the following shared lock // request would be blocked ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); // txn2 try to acquire a shared lock on k1, and get blocked auto t1 = port::Thread([this, &txn2]() { ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, false)); }); while (!reached.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } // unlock txn1, txn2 should be woken up, but txn2 stops on the sync point locker_->UnLock(txn1, 1, "k1", env_); // Use sync point to simulate the race condition. // txn3 tries to take the lock right after txn2 is woken up, but before it // takes the lock TEST_SYNC_POINT( "PerKeyPointLockManagerTest::SharedLockRaceCondition:" "BeforeNewSharedLockRequest"); // txn3 try to acquire a shared lock on k1, and get granted immediately ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, false)); TEST_SYNC_POINT( "PerKeyPointLockManagerTest::SharedLockRaceCondition:" "AfterNewSharedLockRequest"); // validate txn2 is woken up and takes the lock t1.join(); // cleanup locker_->UnLock(txn2, 1, "k1", env_); locker_->UnLock(txn3, 1, "k1", env_); delete txn3; delete txn2; delete txn1; } TEST_F(PerKeyPointLockManagerTest, UpgradeLockRaceCondition) { // Verify an upgrade lock race condition is handled properly. // When a key is locked in exlusive mode, shared lock waiters will be enqueued // as waiters. // When the exclusive lock holder release the lock. The shared lock waiters // are woken up to take the lock. At this point, when a new shared lock // requester comes in, it will take the lock directly without waiting or // queueing. This requester then immediately upgrade the lock to exclusive // lock. This request will be prioritized to the head of the queue. // Meantime, it should also depend on the shared lock waiters which are still // in the queue that are ready to take the lock. Later, when one of the reader // lock want to also upgrade its lock, it will detect a dead lock and abort. auto txn1 = NewTxn(txn_opt_); auto txn2 = NewTxn(txn_opt_); auto txn3 = NewTxn(txn_opt_); SyncPoint::GetInstance()->DisableProcessing(); SyncPoint::GetInstance()->LoadDependency( {{"PerKeyPointLockManager::AcquireWithTimeout:AfterWokenUp", "PerKeyPointLockManagerTest::UpgradeLockRaceCondition:" "BeforeNewSharedLockRequest"}, {"PerKeyPointLockManagerTest::UpgradeLockRaceCondition:" "AfterNewSharedLockRequest", "PerKeyPointLockManager::AcquireWithTimeout:BeforeTakeLock"}}); std::atomic reached(false); SyncPoint::GetInstance()->SetCallBack( wait_sync_point_name_, [&reached](void* /*arg*/) { reached.store(true); }); SyncPoint::GetInstance()->EnableProcessing(); // txn1 acquires an exclusive lock on k1, so that the following shared lock // request would be blocked ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); auto t1 = port::Thread([this, &txn2]() { // txn2 try to acquire a shared lock on k1, and get blocked ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, false)); }); while (!reached.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } // unlock txn1, txn2 should be woken up, but txn2 stops on the sync point locker_->UnLock(txn1, 1, "k1", env_); // Use sync point to simulate the race condition. // txn3 tries to take the lock right after txn2 is woken up, but before it // takes the lock TEST_SYNC_POINT( "PerKeyPointLockManagerTest::UpgradeLockRaceCondition:" "BeforeNewSharedLockRequest"); // txn3 try to acquire a shared lock on k1, and get granted immediately ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, false)); // txn3 try to upgrade its lock to exclusive lock and get blocked. reached = false; auto t2 = port::Thread([this, &txn3]() { ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, true)); }); while (!reached.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } TEST_SYNC_POINT( "PerKeyPointLockManagerTest::UpgradeLockRaceCondition:" "AfterNewSharedLockRequest"); // validate txn2 is woken up and takes the shared lock t1.join(); // validate txn2 would get deadlock when it try to upgrade its lock to // exclusive auto s = locker_->TryLock(txn2, 1, "k1", env_, true); ASSERT_TRUE(s.IsDeadlock()); // cleanup locker_->UnLock(txn2, 1, "k1", env_); t2.join(); locker_->UnLock(txn3, 1, "k1", env_); delete txn3; delete txn2; delete txn1; } TEST_P(SpotLockManagerTest, Catch22) { // Benchmark the overhead of one transaction depends on another in a circle // repeatedly MockColumnFamilyHandle cf(1); locker_->AddColumnFamily(&cf); TransactionOptions txn_opt; txn_opt.deadlock_detect = true; txn_opt.lock_timeout = kLongTxnTimeoutMs; txn_opt.expiration = kLongTxnTimeoutMs; auto txn1 = NewTxn(txn_opt); auto txn2 = NewTxn(txn_opt); // use a wait count to count the number of times the lock is waited inside // transaction lock std::atomic_int wait_count(0); SyncPoint::GetInstance()->DisableProcessing(); if (GetParam().use_per_key_point_lock_manager && GetParam().deadlock_timeout_us != 0) { // Use special sync point when deadlock timeout is enabled, so the test run // faster SyncPoint::GetInstance()->SetCallBack( "PerKeyPointLockManager::AcquireWithTimeout:" "WaitingTxnBeforeDeadLockDetection", [&wait_count](void* /*arg*/) { wait_count++; }); } else { // PointLockManager SyncPoint::GetInstance()->SetCallBack( wait_sync_point_name_, [&wait_count](void* /*arg*/) { wait_count++; }); } SyncPoint::GetInstance()->EnableProcessing(); // txn1 X lock ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); std::mutex coordinator_mutex; int iteration_count = 10000; // txn1 try to lock X lock in a loop auto t1 = port::Thread( [this, &txn1, &wait_count, &coordinator_mutex, &iteration_count]() { while (wait_count.load() < iteration_count) { // spin wait until the other thread enters the lock waiter queue. while (wait_count.load() % 2 == 0); // unlock the lock, so that the other thread can acquire the lock locker_->UnLock(txn1, 1, "k1", env_); { // Use the coordinator mutex to make sure the other thread has been // waked up and acquired the lock, before this thread try to acquire // the lock again. std::scoped_lock lock(coordinator_mutex); ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); } } locker_->UnLock(txn1, 1, "k1", env_); }); // txn2 try to lock X lock in a loop auto t2 = port::Thread( [this, &txn2, &wait_count, &coordinator_mutex, &iteration_count]() { while (wait_count.load() < iteration_count) { { // Use the coordinator mutex to make sure the other thread has been // waked up and acquired the lock, before this thread try to acquire // the lock again. std::scoped_lock lock(coordinator_mutex); ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, true)); } // spin wait until the other thread enters the lock waiter queue. while (wait_count.load() % 2 == 1); // unlock the lock, so that the other thread can acquire the lock locker_->UnLock(txn2, 1, "k1", env_); } }); // clean up t1.join(); t2.join(); delete txn2; delete txn1; } TEST_F(PerKeyPointLockManagerTest, LockUpgradeOrdering) { // When lock is upgraded, verify that it will only upgrade its lock after all // the shared lock that are before the first exclusive lock in the lock wait // queue. auto txn1 = NewTxn(txn_opt_); auto txn2 = NewTxn(txn_opt_); auto txn3 = NewTxn(txn_opt_); auto txn4 = NewTxn(txn_opt_); std::mutex txn4_mutex; std::unique_lock txn4_lock(txn4_mutex); std::atomic_bool txn4_waked_up(false); std::atomic_int wait_count(0); SyncPoint::GetInstance()->DisableProcessing(); SyncPoint::GetInstance()->SetCallBack( wait_sync_point_name_, [&wait_count](void* /*arg*/) { wait_count++; }); SyncPoint::GetInstance()->SetCallBack( "PerKeyPointLockManager::AcquireWithTimeout:AfterWokenUp", [&txn4, &txn4_mutex, &txn4_waked_up](void* arg) { auto transaction_id = *(static_cast(arg)); if (transaction_id == txn4->GetID()) { txn4_waked_up.store(true); { // wait for txn4 mutex to be released, so that this thread will be // blocked. std::scoped_lock lock(txn4_mutex); } } }); SyncPoint::GetInstance()->EnableProcessing(); // Txn1 X lock ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); // Txn2,3,4 try S lock port::Thread t1([this, &txn2]() { ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, false)); }); port::Thread t2([this, &txn3]() { ASSERT_OK(locker_->TryLock(txn3, 1, "k1", env_, false)); }); port::Thread t3([this, &txn4]() { ASSERT_OK(locker_->TryLock(txn4, 1, "k1", env_, false)); }); // wait for all 3 transactions to enter wait state while (wait_count.load() < 3) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } // Txn1 unlock locker_->UnLock(txn1, 1, "k1", env_); // Txn2,3 take S lock t1.join(); t2.join(); // wait for txn4 to be woken up, otherwise txn2 will get deadlock while (!txn4_waked_up.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } // Txn2 try X lock std::atomic_bool txn2_exclusive_lock_acquired(false); port::Thread t4([this, &txn2, &txn2_exclusive_lock_acquired]() { ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, true)); txn2_exclusive_lock_acquired.store(true); }); // wait for txn2 to enter wait state while (wait_count.load() < 4) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } // Txn3 release S lock locker_->UnLock(txn3, 1, "k1", env_); // Validate Txn2 has not acquired the lock yet ASSERT_FALSE(txn2_exclusive_lock_acquired.load()); // Txn4 take S lock txn4_lock.unlock(); t3.join(); // Txn4 release S lock Txn2 upgraded to X lock Txn2 locker_->UnLock(txn4, 1, "k1", env_); t4.join(); ASSERT_TRUE(txn2_exclusive_lock_acquired.load()); // release lock clean up locker_->UnLock(txn2, 1, "k1", env_); delete txn4; delete txn3; delete txn2; delete txn1; } TEST_F(PerKeyPointLockManagerTest, LockDownGradeRaceCondition) { // When a lock is downgraded, it should notify all the shared waiters in the // queue to take the lock. auto txn1 = NewTxn(txn_opt_); auto txn2 = NewTxn(txn_opt_); // Txn1 X lock ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, true)); // Txn2 try S lock port::Thread t1; BlockUntilWaitingTxn(wait_sync_point_name_, t1, [this, &txn2]() { ASSERT_OK(locker_->TryLock(txn2, 1, "k1", env_, false)); }); // Txn1 downgrade to S lock ASSERT_OK(locker_->TryLock(txn1, 1, "k1", env_, false)); // Txn2 take S lock t1.join(); // clean up locker_->UnLock(txn1, 1, "k1", env_); locker_->UnLock(txn2, 1, "k1", env_); delete txn2; delete txn1; } // Run AnyLockManagerTest with PointLockManager INSTANTIATE_TEST_CASE_P(PointLockManager, AnyLockManagerTest, ::testing::Values(nullptr)); // Run AnyLockManagerTest with PerKeyPointLockManager template void PerKeyPointLockManagerTestSetup(PointLockManagerTest* self) { self->init(); self->deadlock_timeout_us = N; self->UsePerKeyPointLockManager(); } INSTANTIATE_TEST_CASE_P( PerLockPointLockManager, AnyLockManagerTest, ::testing::Values(PerKeyPointLockManagerTestSetup<0>, PerKeyPointLockManagerTestSetup<100>, PerKeyPointLockManagerTestSetup<1000>)); // Run PointLockManagerTest with PerLockPointLockManager and PointLockManager INSTANTIATE_TEST_CASE_P( PointLockCorrectnessCheckTestSuite, SpotLockManagerTest, ::testing::ValuesIn(std::vector{ {true, 0}, {true, 100}, {true, 1000}, {false, 0}})); } // namespace ROCKSDB_NAMESPACE int main(int argc, char** argv) { ROCKSDB_NAMESPACE::port::InstallStackTraceHandler(); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }