/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Test bed for folly/Synchronized.h #include #include #include #include #include #include #include #include #include #include FOLLY_GNU_DISABLE_WARNING("-Wdeprecated-declarations") using namespace folly::sync_tests; namespace folly { template class SynchronizedTest : public testing::Test {}; using SynchronizedTestTypes = testing::Types< folly::DistributedMutex, folly::SharedMutexReadPriority, folly::SharedMutexWritePriority, std::mutex, std::recursive_mutex, #if FOLLY_LOCK_TRAITS_HAVE_TIMED_MUTEXES std::timed_mutex, std::recursive_timed_mutex, #endif #ifdef RW_SPINLOCK_USE_X86_INTRINSIC_ folly::RWTicketSpinLock32, folly::RWTicketSpinLock64, #endif folly::SpinLock>; TYPED_TEST_SUITE(SynchronizedTest, SynchronizedTestTypes); TYPED_TEST(SynchronizedTest, Basic) { testBasic(); } TYPED_TEST(SynchronizedTest, WithLock) { testWithLock(); } TYPED_TEST(SynchronizedTest, Unlock) { testUnlock(); } TYPED_TEST(SynchronizedTest, Deprecated) { testDeprecated(); } TYPED_TEST(SynchronizedTest, Concurrency) { testConcurrency(); } TYPED_TEST(SynchronizedTest, AcquireLocked) { testAcquireLocked(); } TYPED_TEST(SynchronizedTest, AcquireLockedWithConst) { testAcquireLockedWithConst(); } TYPED_TEST(SynchronizedTest, DualLocking) { testDualLocking(); } TYPED_TEST(SynchronizedTest, DualLockingWithConst) { testDualLockingWithConst(); } TYPED_TEST(SynchronizedTest, ConstCopy) { testConstCopy(); } TYPED_TEST(SynchronizedTest, InPlaceConstruction) { testInPlaceConstruction(); } TYPED_TEST(SynchronizedTest, Exchange) { testExchange(); } template class SynchronizedTimedTest : public testing::Test {}; using SynchronizedTimedTestTypes = testing::Types< #if FOLLY_LOCK_TRAITS_HAVE_TIMED_MUTEXES std::timed_mutex, std::recursive_timed_mutex, #endif #ifdef RW_SPINLOCK_USE_X86_INTRINSIC_ folly::RWTicketSpinLock32, folly::RWTicketSpinLock64, #endif folly::SharedMutexReadPriority, folly::SharedMutexWritePriority>; TYPED_TEST_SUITE(SynchronizedTimedTest, SynchronizedTimedTestTypes); TYPED_TEST(SynchronizedTimedTest, Timed) { testTimed(); } template class SynchronizedTimedWithConstTest : public testing::Test {}; using SynchronizedTimedWithConstTestTypes = testing::Types< #ifdef RW_SPINLOCK_USE_X86_INTRINSIC_ folly::RWTicketSpinLock32, folly::RWTicketSpinLock64, #endif folly::SharedMutexReadPriority, folly::SharedMutexWritePriority>; TYPED_TEST_SUITE( SynchronizedTimedWithConstTest, SynchronizedTimedWithConstTestTypes); TYPED_TEST(SynchronizedTimedWithConstTest, TimedShared) { testTimedShared(); } using CountPair = std::pair; // This class is specialized only to be uesed in SynchronizedLockTest class FakeMutex { public: void lock() { ++lockCount_; } void unlock() { ++unlockCount_; } static CountPair getLockUnlockCount() { return CountPair{lockCount_, unlockCount_}; } static void resetLockUnlockCount() { lockCount_ = 0; unlockCount_ = 0; } private: // Keep these two static for test access // Keep them thread_local in case of tests are run in parallel within one // process static thread_local int lockCount_; static thread_local int unlockCount_; }; thread_local int FakeMutex::lockCount_{0}; thread_local int FakeMutex::unlockCount_{0}; // SynchronizedLockTest is used to verify the correct lock unlock behavior // happens per design class SynchronizedLockTest : public testing::Test { public: void SetUp() override { FakeMutex::resetLockUnlockCount(); } }; /** * Test mutex to help to automate assertions, taken from LockTraitsTest.cpp */ class FakeAllPowerfulAssertingMutexInternal { public: enum class CurrentLockState { UNLOCKED, SHARED, UPGRADE, UNIQUE }; void lock() { EXPECT_EQ(this->lock_state, CurrentLockState::UNLOCKED); this->lock_state = CurrentLockState::UNIQUE; } void unlock() { EXPECT_EQ(this->lock_state, CurrentLockState::UNIQUE); this->lock_state = CurrentLockState::UNLOCKED; } void lock_shared() { EXPECT_EQ(this->lock_state, CurrentLockState::UNLOCKED); this->lock_state = CurrentLockState::SHARED; } void unlock_shared() { EXPECT_EQ(this->lock_state, CurrentLockState::SHARED); this->lock_state = CurrentLockState::UNLOCKED; } void lock_upgrade() { EXPECT_EQ(this->lock_state, CurrentLockState::UNLOCKED); this->lock_state = CurrentLockState::UPGRADE; } void unlock_upgrade() { EXPECT_EQ(this->lock_state, CurrentLockState::UPGRADE); this->lock_state = CurrentLockState::UNLOCKED; } void unlock_upgrade_and_lock() { EXPECT_EQ(this->lock_state, CurrentLockState::UPGRADE); this->lock_state = CurrentLockState::UNIQUE; } void unlock_and_lock_upgrade() { EXPECT_EQ(this->lock_state, CurrentLockState::UNIQUE); this->lock_state = CurrentLockState::UPGRADE; } void unlock_and_lock_shared() { EXPECT_EQ(this->lock_state, CurrentLockState::UNIQUE); this->lock_state = CurrentLockState::SHARED; } void unlock_upgrade_and_lock_shared() { EXPECT_EQ(this->lock_state, CurrentLockState::UPGRADE); this->lock_state = CurrentLockState::SHARED; } template bool try_lock_for(const std::chrono::duration&) { EXPECT_EQ(this->lock_state, CurrentLockState::UNLOCKED); this->lock_state = CurrentLockState::UNIQUE; return true; } template bool try_lock_upgrade_for(const std::chrono::duration&) { EXPECT_EQ(this->lock_state, CurrentLockState::UNLOCKED); this->lock_state = CurrentLockState::UPGRADE; return true; } template bool try_unlock_upgrade_and_lock_for( const std::chrono::duration&) { EXPECT_EQ(this->lock_state, CurrentLockState::UPGRADE); this->lock_state = CurrentLockState::UNIQUE; return true; } /* * Initialize the FakeMutex with an unlocked state */ CurrentLockState lock_state{CurrentLockState::UNLOCKED}; }; /** * The following works around the internal mutex for synchronized being * private * * This is horridly thread unsafe. */ static FakeAllPowerfulAssertingMutexInternal globalAllPowerfulAssertingMutex; class FakeAllPowerfulAssertingMutex { public: void lock() { globalAllPowerfulAssertingMutex.lock(); } void unlock() { globalAllPowerfulAssertingMutex.unlock(); } void lock_shared() { globalAllPowerfulAssertingMutex.lock_shared(); } void unlock_shared() { globalAllPowerfulAssertingMutex.unlock_shared(); } void lock_upgrade() { globalAllPowerfulAssertingMutex.lock_upgrade(); } void unlock_upgrade() { globalAllPowerfulAssertingMutex.unlock_upgrade(); } void unlock_upgrade_and_lock() { globalAllPowerfulAssertingMutex.unlock_upgrade_and_lock(); } void unlock_and_lock_upgrade() { globalAllPowerfulAssertingMutex.unlock_and_lock_upgrade(); } void unlock_and_lock_shared() { globalAllPowerfulAssertingMutex.unlock_and_lock_shared(); } void unlock_upgrade_and_lock_shared() { globalAllPowerfulAssertingMutex.unlock_upgrade_and_lock_shared(); } template bool try_lock_for(const std::chrono::duration& arg) { return globalAllPowerfulAssertingMutex.try_lock_for(arg); } template bool try_lock_upgrade_for(const std::chrono::duration& arg) { return globalAllPowerfulAssertingMutex.try_lock_upgrade_for(arg); } template bool try_unlock_upgrade_and_lock_for( const std::chrono::duration& arg) { return globalAllPowerfulAssertingMutex.try_unlock_upgrade_and_lock_for(arg); } // reset state on destruction ~FakeAllPowerfulAssertingMutex() { globalAllPowerfulAssertingMutex = FakeAllPowerfulAssertingMutexInternal{}; } }; class NonDefaultConstructibleMutex { public: explicit NonDefaultConstructibleMutex(int valueIn) { value = valueIn; } NonDefaultConstructibleMutex() = delete; NonDefaultConstructibleMutex(const NonDefaultConstructibleMutex&) = delete; NonDefaultConstructibleMutex(NonDefaultConstructibleMutex&&) = delete; NonDefaultConstructibleMutex& operator=(const NonDefaultConstructibleMutex&) = delete; NonDefaultConstructibleMutex& operator=(NonDefaultConstructibleMutex&&) = delete; static int value; void lock() {} void unlock() {} }; int NonDefaultConstructibleMutex::value{0}; TEST_F(SynchronizedLockTest, TestCopyConstructibleValues) { struct NonCopyConstructible { NonCopyConstructible(const NonCopyConstructible&) = delete; NonCopyConstructible& operator=(const NonCopyConstructible&) = delete; }; struct CopyConstructible {}; EXPECT_FALSE(std::is_copy_constructible< folly::Synchronized>::value); EXPECT_FALSE(std::is_copy_assignable< folly::Synchronized>::value); EXPECT_TRUE(std::is_copy_constructible< folly::Synchronized>::value); EXPECT_TRUE( std::is_copy_assignable>::value); } namespace { class Dummy { public: void foo() {} }; } // namespace TEST_F(SynchronizedLockTest, ReadLockAsNonConstUnsafe) { { folly::Synchronized sync; auto rlock = sync.rlock(); rlock.asNonConstUnsafe().foo(); } { folly::Synchronized sync; auto rlock = sync.rlock(std::chrono::seconds{1}); rlock.asNonConstUnsafe().foo(); } } TEST_F(SynchronizedLockTest, UpgradeLocking) { folly::Synchronized sync; // sanity assert static_assert( std::is_same::type, int>::value, "The ulock function was not well configured, blame aary@instagram.com"); { auto ulock = sync.ulock(); EXPECT_TRUE((std::is_same::value)); EXPECT_TRUE( (std::is_same::value)); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UPGRADE); } // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); // test going from upgrade to exclusive { auto ulock = sync.ulock(); auto wlock = ulock.moveFromUpgradeToWrite(); EXPECT_TRUE((std::is_same::value)); EXPECT_EQ(static_cast(ulock), false); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNIQUE); } // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); // test going from upgrade to shared { auto ulock = sync.ulock(); auto slock = ulock.moveFromUpgradeToRead(); EXPECT_EQ(static_cast(ulock), false); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::SHARED); } // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); // test going from exclusive to upgrade { auto wlock = sync.wlock(); auto ulock = wlock.moveFromWriteToUpgrade(); EXPECT_EQ(static_cast(wlock), false); EXPECT_TRUE((std::is_same::value)); EXPECT_TRUE( (std::is_same::value)); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UPGRADE); } // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); // test going from exclusive to shared { auto wlock = sync.wlock(); auto slock = wlock.moveFromWriteToRead(); EXPECT_EQ(static_cast(wlock), false); EXPECT_TRUE((std::is_same::value)); EXPECT_TRUE( (std::is_same::value)); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::SHARED); } // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); } TEST_F(SynchronizedLockTest, UpgradeLockingWithULock) { folly::Synchronized sync; // sanity assert static_assert( std::is_same::type, int>::value, "The ulock function was not well configured, blame aary@instagram.com"); // test from upgrade to write sync.withULockPtr([](auto ulock) { EXPECT_EQ(static_cast(ulock), true); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UPGRADE); auto wlock = ulock.moveFromUpgradeToWrite(); EXPECT_EQ(static_cast(ulock), false); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNIQUE); }); // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); // test from write to upgrade sync.withWLockPtr([](auto wlock) { EXPECT_EQ(static_cast(wlock), true); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNIQUE); auto ulock = wlock.moveFromWriteToUpgrade(); EXPECT_EQ(static_cast(wlock), false); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UPGRADE); }); // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); // test from upgrade to shared sync.withULockPtr([](auto ulock) { EXPECT_EQ(static_cast(ulock), true); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UPGRADE); auto slock = ulock.moveFromUpgradeToRead(); EXPECT_EQ(static_cast(ulock), false); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::SHARED); }); // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); // test from write to shared sync.withWLockPtr([](auto wlock) { EXPECT_EQ(static_cast(wlock), true); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNIQUE); auto slock = wlock.moveFromWriteToRead(); EXPECT_EQ(static_cast(wlock), false); EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::SHARED); }); // should be unlocked here EXPECT_EQ( globalAllPowerfulAssertingMutex.lock_state, FakeAllPowerfulAssertingMutexInternal::CurrentLockState::UNLOCKED); } TEST_F(SynchronizedLockTest, TestPieceWiseConstruct) { auto&& synchronized = folly::Synchronized{ std::piecewise_construct, std::forward_as_tuple(3), std::forward_as_tuple(1)}; EXPECT_EQ(*synchronized.lock(), 3); EXPECT_EQ(NonDefaultConstructibleMutex::value, 1); } TEST_F(SynchronizedLockTest, TestConstConversion) { folly::Synchronized sync{}; EXPECT_EQ(0, sync.copy()); { using ct = decltype(std::as_const(sync).rlock()); ct l = sync.rlock(); // const-converting constructor EXPECT_EQ(0, *l); l.unlock(); l = sync.rlock(); // const-converting assignment operator EXPECT_EQ(0, *l); } } namespace { constexpr auto kLockable = 1; constexpr auto kWLockable = 2; constexpr auto kRLockable = 4; constexpr auto kULockable = 8; template class TryLockable { public: explicit TryLockable( bool shouldSucceed, folly::Function onLockIn, folly::Function onUnlockIn) : kShouldSucceed{shouldSucceed}, onLock{std::move(onLockIn)}, onUnlock{std::move(onUnlockIn)} {} void lock() { EXPECT_TRUE(false); } template < int LockableType = kLockableType, std::enable_if_t* = nullptr> void lock_shared() { EXPECT_TRUE(false); } template < int LockableType = kLockableType, std::enable_if_t* = nullptr> void lock_upgrade() { EXPECT_TRUE(false); } bool tryLockImpl(int lockableMask) { // if the lockable type of this instance is one of the possible options as // expressed in the mask go through the usual test code if (kLockableType | lockableMask) { if (kShouldSucceed) { onLock(); return true; } else { return false; } } // else fail the test EXPECT_TRUE(false); return false; } void unlockImpl(int lockableMask) { if (kLockableType | lockableMask) { onUnlock(); return; } EXPECT_TRUE(false); } bool try_lock() { return tryLockImpl(kLockable | kWLockable); } bool try_lock_shared() { return tryLockImpl(kRLockable); } bool try_lock_upgrade() { return tryLockImpl(kULockable); } void unlock() { unlockImpl(kLockable | kWLockable); } void unlock_shared() { unlockImpl(kLockable | kRLockable); } void unlock_upgrade() { unlockImpl(kLockable | kULockable); } const bool kShouldSucceed; folly::Function onLock; folly::Function onUnlock; }; struct TestSharedMutex { public: void lock() { onLock_(); } void unlock() { onUnlock_(); } void lock_shared() { onLockShared_(); } void unlock_shared() { onUnlockShared_(); } bool try_lock() { onLock_(); return true; } bool try_lock_shared() { onLockShared_(); return true; } std::function onLock_; std::function onUnlock_; std::function onLockShared_; std::function onUnlockShared_; }; struct TestMutex { public: void lock() { onLock(); ++numTimesLocked; } bool try_lock() { if (shouldTryLockSucceed) { lock(); return true; } return false; } void unlock() { onUnlock(); ++numTimesUnlocked; } int numTimesLocked{0}; int numTimesUnlocked{0}; bool shouldTryLockSucceed{true}; std::function onLock{[] {}}; std::function onUnlock{[] {}}; }; template void testTryLock(Func func) { { auto locked = 0; auto unlocked = 0; folly::Synchronized> synchronized{ std::piecewise_construct, std::make_tuple(), std::make_tuple(true, [&] { ++locked; }, [&] { ++unlocked; })}; { auto lock = func(synchronized); EXPECT_TRUE(lock); EXPECT_EQ(locked, 1); } EXPECT_EQ(locked, 1); EXPECT_EQ(unlocked, 1); } { auto locked = 0; auto unlocked = 0; folly::Synchronized> synchronized{ std::piecewise_construct, std::make_tuple(), std::make_tuple(false, [&] { ++locked; }, [&] { ++unlocked; })}; { auto lock = func(synchronized); EXPECT_FALSE(lock); EXPECT_EQ(locked, 0); } EXPECT_EQ(locked, 0); EXPECT_EQ(unlocked, 0); } } class MutexTrack { public: static int gOrder; void lock_shared() {} void unlock_shared() {} void lock() { order = MutexTrack::gOrder++; } void unlock() { order = -1; --gOrder; } int order{-1}; }; int MutexTrack::gOrder{0}; } // namespace TEST_F(SynchronizedLockTest, TestTryLock) { testTryLock( [](auto& synchronized) { return synchronized.tryLock(); }); } TEST_F(SynchronizedLockTest, TestTryWLock) { testTryLock( [](auto& synchronized) { return synchronized.tryWLock(); }); } TEST_F(SynchronizedLockTest, TestTryRLock) { testTryLock( [](auto& synchronized) { return synchronized.tryRLock(); }); } TEST_F(SynchronizedLockTest, TestTryULock) { testTryLock( [](auto& synchronized) { return synchronized.tryULock(); }); } template using LPtr = LockedPtr, LockPolicy>; namespace { template