diff --git a/libwvdrmengine/Android.mk b/libwvdrmengine/Android.mk index 0dd7d639..e953180e 100644 --- a/libwvdrmengine/Android.mk +++ b/libwvdrmengine/Android.mk @@ -110,6 +110,7 @@ CORE_SRC_DIR := cdm/core/src LOCAL_SRC_FILES := \ $(CORE_SRC_DIR)/properties.cpp \ $(UTIL_SRC_DIR)/platform.cpp \ + $(UTIL_SRC_DIR)/rw_lock.cpp \ $(UTIL_SRC_DIR)/string_conversions.cpp \ $(UTIL_SRC_DIR)/clock.cpp \ $(UTIL_SRC_DIR)/file_store.cpp \ diff --git a/libwvdrmengine/build_and_run_all_unit_tests.sh b/libwvdrmengine/build_and_run_all_unit_tests.sh index ba19e930..0ee1ca6a 100755 --- a/libwvdrmengine/build_and_run_all_unit_tests.sh +++ b/libwvdrmengine/build_and_run_all_unit_tests.sh @@ -109,6 +109,7 @@ try_adb_push oemcrypto_test try_adb_push policy_engine_constraints_unittest try_adb_push policy_engine_unittest try_adb_push request_license_test +try_adb_push rw_lock_test try_adb_push service_certificate_unittest try_adb_push timer_unittest try_adb_push usage_table_header_unittest diff --git a/libwvdrmengine/cdm/core/test/rw_lock_test.cpp b/libwvdrmengine/cdm/core/test/rw_lock_test.cpp new file mode 100644 index 00000000..68d6e56a --- /dev/null +++ b/libwvdrmengine/cdm/core/test/rw_lock_test.cpp @@ -0,0 +1,293 @@ +// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "rw_lock.h" + +using std::this_thread::sleep_for; + +namespace wvcdm { + +namespace { + +const size_t kFleetSize = 20; + +const auto kShortSnooze = std::chrono::milliseconds(10); +// By convention, we assume any other threads put into flight will have +// finished their minimal actions after 1 second. +const auto kDefaultTimeout = std::chrono::seconds(1); + +struct ConditionVariablePair { + std::mutex mutex; + std::condition_variable condition_variable; +}; + +template +bool WaitForTrue(std::atomic* value, Duration timeout) { + std::chrono::steady_clock clock; + auto start = clock.now(); + while (!*value) { + if (clock.now() - start >= timeout) return false; + sleep_for(kShortSnooze); + } + return true; +} + +} // namespace + +TEST(SharedMutexUnitTests, ReadersDoNotBlockReaders) { + shared_mutex mutex_under_test; + + // Used to signal the primary reader thread to stop holding the lock. + ConditionVariablePair delay_primary; + // Used to indicate if the primary thread is waiting. (and thus holding the + // mutex) + std::atomic primary_is_waiting(false); + + // Start a thread that takes the shared_mutex as a reader and holds it until + // signalled to stop. + std::future primary_reader = std::async(std::launch::async, [&] { + shared_lock auto_lock(mutex_under_test); + + ASSERT_FALSE(primary_is_waiting); + primary_is_waiting = true; + + std::unique_lock lock(delay_primary.mutex); + delay_primary.condition_variable.wait(lock); + + primary_is_waiting = false; + }); + + // Wait for the primary thread to be waiting. + ASSERT_TRUE(WaitForTrue(&primary_is_waiting, kDefaultTimeout)); + + // Start a bunch of threads that take the shared_mutex as a reader and + // immediately release it. These should not be blocked. + std::vector> reader_fleet; + + for (size_t i = 0; i < kFleetSize; ++i) { + reader_fleet.push_back(std::async(std::launch::async, [&] { + shared_lock auto_lock(mutex_under_test); + EXPECT_TRUE(primary_is_waiting); + sleep_for(kShortSnooze); + })); + } + + // The readers fleet should all complete successfully. + for (std::future& reader : reader_fleet) { + EXPECT_EQ(std::future_status::ready, reader.wait_for(kDefaultTimeout)); + } + + // Verify the primary reader's state and signal it to finish. + EXPECT_TRUE(primary_is_waiting); + delay_primary.condition_variable.notify_one(); + EXPECT_EQ(std::future_status::ready, + primary_reader.wait_for(kDefaultTimeout)); +} + +TEST(SharedMutexUnitTests, ReadersBlockWriters) { + shared_mutex mutex_under_test; + + // Used to signal the reader thread to stop holding the lock. + ConditionVariablePair delay_reader; + // Used to indicate if the reader thread is waiting. (and thus holding the + // mutex) + std::atomic reader_is_waiting(false); + + // Start a thread that takes the shared_mutex as a reader and holds it until + // signalled to stop. + std::future reader = std::async(std::launch::async, [&] { + shared_lock auto_lock(mutex_under_test); + + ASSERT_FALSE(reader_is_waiting); + reader_is_waiting = true; + + std::unique_lock lock(delay_reader.mutex); + delay_reader.condition_variable.wait(lock); + + reader_is_waiting = false; + }); + + // Wait for the reader thread to be waiting. + ASSERT_TRUE(WaitForTrue(&reader_is_waiting, kDefaultTimeout)); + + // Start a thread that takes the shared_mutex as a writer and immediately + // releases it. This should be blocked by the reader above. + std::future writer = std::async(std::launch::async, [&] { + std::unique_lock auto_lock(mutex_under_test); + EXPECT_FALSE(reader_is_waiting); + sleep_for(kShortSnooze); + }); + + // The writer should not be complete still, because it is waiting on the + // shared_mutex. + EXPECT_NE(std::future_status::ready, writer.wait_for(kDefaultTimeout)); + + // Verify the reader is still waiting and signal it to finish. + EXPECT_TRUE(reader_is_waiting); + delay_reader.condition_variable.notify_one(); + + // Make sure the reader actually finishes. + EXPECT_EQ(std::future_status::ready, reader.wait_for(kDefaultTimeout)); + + // The writer should now be able to complete as well. + EXPECT_EQ(std::future_status::ready, writer.wait_for(kDefaultTimeout)); +} + +TEST(SharedMutexUnitTests, WritersBlockReaders) { + shared_mutex mutex_under_test; + + // Used to signal the writer thread to stop holding the lock. + ConditionVariablePair delay_writer; + // Used to indicate if the writer thread is waiting. (and thus holding the + // mutex) + std::atomic writer_is_waiting(false); + + // Start a thread that takes the shared_mutex as a writer and holds it until + // signalled to stop. + std::future writer = std::async(std::launch::async, [&] { + std::unique_lock auto_lock(mutex_under_test); + + ASSERT_FALSE(writer_is_waiting); + writer_is_waiting = true; + + std::unique_lock lock(delay_writer.mutex); + delay_writer.condition_variable.wait(lock); + + writer_is_waiting = false; + }); + + // Wait for the writer thread to be waiting. + ASSERT_TRUE(WaitForTrue(&writer_is_waiting, kDefaultTimeout)); + + // Start a thread that takes the shared_mutex as a reader and immediately + // releases it. This should be blocked by the writer above. + std::future reader = std::async(std::launch::async, [&] { + shared_lock auto_lock(mutex_under_test); + EXPECT_FALSE(writer_is_waiting); + sleep_for(kShortSnooze); + }); + + // The reader should not be complete still, because it is waiting on the + // shared_mutex. + EXPECT_NE(std::future_status::ready, reader.wait_for(kDefaultTimeout)); + + // Verify the writer is still waiting and signal it to finish. + EXPECT_TRUE(writer_is_waiting); + delay_writer.condition_variable.notify_one(); + + // Make sure the writer actually finishes. + EXPECT_EQ(std::future_status::ready, writer.wait_for(kDefaultTimeout)); + + // The reader should now be able to complete as well. + EXPECT_EQ(std::future_status::ready, reader.wait_for(kDefaultTimeout)); +} + +TEST(SharedMutexUnitTests, WritersBlockWriters) { + shared_mutex mutex_under_test; + + // Used to signal the primary writer thread to stop holding the lock. + ConditionVariablePair delay_primary; + // Used to indicate if the primary thread is waiting. (and thus holding the + // mutex) + std::atomic primary_is_waiting(false); + + // Start a thread that takes the shared_mutex as a writer and holds it until + // signalled to stop. + std::future primary = std::async(std::launch::async, [&] { + std::unique_lock auto_lock(mutex_under_test); + + ASSERT_FALSE(primary_is_waiting); + primary_is_waiting = true; + + std::unique_lock lock(delay_primary.mutex); + delay_primary.condition_variable.wait(lock); + + primary_is_waiting = false; + }); + + // Wait for the primary thread to be waiting. + ASSERT_TRUE(WaitForTrue(&primary_is_waiting, kDefaultTimeout)); + + // Start a thread that takes the shared_mutex as a writer and immediately + // releases it. This should be blocked by the primary writer above. + std::future secondary = std::async(std::launch::async, [&] { + std::unique_lock auto_lock(mutex_under_test); + EXPECT_FALSE(primary_is_waiting); + sleep_for(kShortSnooze); + }); + + // The secondary writer should not be complete still, because it is waiting on + // the shared_mutex. + EXPECT_NE(std::future_status::ready, secondary.wait_for(kDefaultTimeout)); + + // Verify the primary is still waiting and signal it to finish. + EXPECT_TRUE(primary_is_waiting); + delay_primary.condition_variable.notify_one(); + + // Make sure the primary writer actually finished. + EXPECT_EQ(std::future_status::ready, primary.wait_for(kDefaultTimeout)); + + // The secondary writer should now be complete as well. + EXPECT_EQ(std::future_status::ready, secondary.wait_for(kDefaultTimeout)); +} + +TEST(SharedMutexUnitTests, NonConcurrentLocksDoNotBlock) { + shared_mutex mutex_under_test; + + for (size_t i = 0; i < kFleetSize / 2; ++i) { + // Start a thread that takes the shared_mutex as a reader and immediately + // releases it. + std::future reader = std::async(std::launch::async, [&] { + shared_lock auto_lock(mutex_under_test); + sleep_for(kShortSnooze); + }); + + ASSERT_EQ(std::future_status::ready, reader.wait_for(kDefaultTimeout)); + + // Start a thread that takes the shared_mutex as a writer and immediately + // releases it. + std::future writer = std::async(std::launch::async, [&] { + std::unique_lock auto_lock(mutex_under_test); + sleep_for(kShortSnooze); + }); + + ASSERT_EQ(std::future_status::ready, writer.wait_for(kDefaultTimeout)); + } +} + +TEST(SharedMutexUnitTests, LargeVolumeOfThreadsSortsItselfOutEventually) { + shared_mutex mutex_under_test; + + // Start a lot of threads that try to hold the lock at once. + std::vector> fleet; + + for (size_t i = 0; i < kFleetSize / 2; ++i) { + fleet.push_back(std::async(std::launch::async, [&] { + std::unique_lock auto_lock(mutex_under_test); + sleep_for(kShortSnooze); + })); + + fleet.push_back(std::async(std::launch::async, [&] { + shared_lock auto_lock(mutex_under_test); + sleep_for(kShortSnooze); + })); + } + + // The fleet should all complete eventually. + for (std::future& future : fleet) { + EXPECT_EQ(std::future_status::ready, future.wait_for(kDefaultTimeout)); + } +} + +} // namespace wvcdm diff --git a/libwvdrmengine/cdm/test/Android.mk b/libwvdrmengine/cdm/test/Android.mk index 069d8ec2..8c4d519f 100644 --- a/libwvdrmengine/cdm/test/Android.mk +++ b/libwvdrmengine/cdm/test/Android.mk @@ -107,6 +107,10 @@ test_src_dir := . test_main := ../core/test/test_main.cpp include $(LOCAL_PATH)/integration-test.mk +test_name := rw_lock_test +test_src_dir := ../core/test +include $(LOCAL_PATH)/integration-test.mk + test_name := service_certificate_unittest test_src_dir := ../core/test include $(LOCAL_PATH)/unit-test.mk diff --git a/libwvdrmengine/cdm/util/include/rw_lock.h b/libwvdrmengine/cdm/util/include/rw_lock.h new file mode 100644 index 00000000..82724057 --- /dev/null +++ b/libwvdrmengine/cdm/util/include/rw_lock.h @@ -0,0 +1,65 @@ +// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#ifndef WVCDM_UTIL_RW_LOCK_H_ +#define WVCDM_UTIL_RW_LOCK_H_ + +#include + +#include +#include + +#include "disallow_copy_and_assign.h" +#include "util_common.h" + +namespace wvcdm { + +// A simple reader-writer mutex implementation that mimics the one from C++17 +class CORE_UTIL_EXPORT shared_mutex { + public: + shared_mutex() : reader_count_(0), has_writer_(false) {} + ~shared_mutex(); + + // These methods take the mutex as a reader. They do not fulfill the + // SharedMutex requirement from the C++14 STL, but they fulfill enough of it + // to be used with |shared_lock| below. + void lock_shared(); + void unlock_shared(); + + // These methods take the mutex as a writer. They fulfill the Mutex + // requirement from the C++11 STL so that this mutex can be used with + // |std::unique_lock|. + void lock() { lock_implementation(false); } + bool try_lock() { return lock_implementation(true); } + void unlock(); + + private: + bool lock_implementation(bool abort_if_unavailable); + + uint32_t reader_count_; + bool has_writer_; + + std::mutex mutex_; + std::condition_variable condition_variable_; + + CORE_DISALLOW_COPY_AND_ASSIGN(shared_mutex); +}; + +// A simple reader lock implementation that mimics the one from C++14 +template +class shared_lock { + public: + explicit shared_lock(Mutex& lock) : lock_(&lock) { lock_->lock_shared(); } + explicit shared_lock(Mutex* lock) : lock_(lock) { lock_->lock_shared(); } + ~shared_lock() { lock_->unlock_shared(); } + + private: + Mutex* lock_; + + CORE_DISALLOW_COPY_AND_ASSIGN(shared_lock); +}; + +} // namespace wvcdm + +#endif // WVCDM_UTIL_RW_LOCK_H_ diff --git a/libwvdrmengine/cdm/util/src/rw_lock.cpp b/libwvdrmengine/cdm/util/src/rw_lock.cpp new file mode 100644 index 00000000..43dff64c --- /dev/null +++ b/libwvdrmengine/cdm/util/src/rw_lock.cpp @@ -0,0 +1,60 @@ +// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "rw_lock.h" + +#include "log.h" + +namespace wvcdm { + +shared_mutex::~shared_mutex() { + if (reader_count_ > 0) { + LOGE("shared_mutex destroyed with active readers!"); + } + if (has_writer_) { + LOGE("shared_mutex destroyed with an active writer!"); + } +} + +void shared_mutex::lock_shared() { + std::unique_lock lock(mutex_); + + while (has_writer_) { + condition_variable_.wait(lock); + } + + ++reader_count_; +} + +void shared_mutex::unlock_shared() { + std::unique_lock lock(mutex_); + + --reader_count_; + + if (reader_count_ == 0) { + condition_variable_.notify_all(); + } +} + +bool shared_mutex::lock_implementation(bool abort_if_unavailable) { + std::unique_lock lock(mutex_); + + while (reader_count_ > 0 || has_writer_) { + if (abort_if_unavailable) return false; + condition_variable_.wait(lock); + } + + has_writer_ = true; + return true; +} + +void shared_mutex::unlock() { + std::unique_lock lock(mutex_); + + has_writer_ = false; + + condition_variable_.notify_all(); +} + +} // namespace wvcdm diff --git a/libwvdrmengine/run_all_unit_tests.sh b/libwvdrmengine/run_all_unit_tests.sh index 4269415e..713c4ab2 100755 --- a/libwvdrmengine/run_all_unit_tests.sh +++ b/libwvdrmengine/run_all_unit_tests.sh @@ -113,6 +113,7 @@ adb_shell_run license_keys_unittest adb_shell_run license_unittest adb_shell_run policy_engine_constraints_unittest adb_shell_run policy_engine_unittest +adb_shell_run rw_lock_test adb_shell_run service_certificate_unittest adb_shell_run timer_unittest adb_shell_run usage_table_header_unittest