diff --git a/src/iocore/eventsystem/CMakeLists.txt b/src/iocore/eventsystem/CMakeLists.txt index a6f4d6fc436..e5c5bde6624 100644 --- a/src/iocore/eventsystem/CMakeLists.txt +++ b/src/iocore/eventsystem/CMakeLists.txt @@ -72,6 +72,9 @@ if(BUILD_TESTING) target_link_libraries(test_Action ts::inkevent configmanager Catch2::Catch2WithMain) add_catch2_test(NAME test_Action COMMAND test_Action) + add_executable(test_Lock unit_tests/test_Lock.cc) + target_link_libraries(test_Lock ts::inkevent configmanager Catch2::Catch2WithMain) + add_catch2_test(NAME test_Lock COMMAND test_Lock) endif() clang_tidy_check(inkevent) diff --git a/src/iocore/eventsystem/unit_tests/test_Lock.cc b/src/iocore/eventsystem/unit_tests/test_Lock.cc new file mode 100644 index 00000000000..2194d059382 --- /dev/null +++ b/src/iocore/eventsystem/unit_tests/test_Lock.cc @@ -0,0 +1,393 @@ +/** @file + + Catch2 unit tests for the Lock.h mutex acquisition macros. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. + */ + +#define CATCH_CONFIG_THREAD_SAFE_ASSERTIONS +#include "inkevent_test_fixtures.h" + +#include + +#include + +using inkevent_test::AtomicFlag; +using inkevent_test::EventProcessorListener; + +CATCH_REGISTER_LISTENER(EventProcessorListener) + +namespace +{ + +class HoldOnEThread : public Continuation +{ +public: + HoldOnEThread(ProxyMutex *self_mutex, Ptr &target) : Continuation(self_mutex), target_mutex(target) + { + SET_HANDLER(&HoldOnEThread::on_event); + this->callback_action = eventProcessor.schedule_imm(this, ET_CALL); + ink_assert(this->callback_action != nullptr); + } + + // In case of an exception in a thread that would have set release, we set + // it here in order to unfreeze any threads that may be waiting on done. + ~HoldOnEThread() + { + this->cancel_callback(); + this->wait_for_callback_finish(); + } + + bool + wait_for_callback_start() + { + return this->held.wait_until_set(); + } + + bool + wait_for_callback_finish() + { + this->release.set(); + // The callback can finish without setting done due to wait timeouts. We + // return false in that case. + return this->done.wait_until_set(); + } + +private: + Action *callback_action{}; + Ptr target_mutex; + AtomicFlag held; + AtomicFlag release; + AtomicFlag done; + + int + on_event(int /* event ATS_UNUSED */, void * /* data ATS_UNUSED */) + { + SCOPED_MUTEX_LOCK(guard, this->target_mutex, this_ethread()); + this->held.set(); + if (this->release.wait_until_set()) { + MUTEX_RELEASE(guard); + this->done.set(); + } + return 0; + } + + void + cancel_callback() + { + SCOPED_MUTEX_LOCK(guard, this->mutex, this_ethread()); + if (this->is_expecting_callback()) { + this->callback_action->cancel(this); + ink_assert(this->callback_action->cancelled); + this->done.set(); + } + } + + bool + is_expecting_callback() + { + if (reinterpret_cast(this->callback_action) & 1) { + return false; + } + + ink_assert(this->mutex->thread_holding == this_ethread()); + return !this->held.is_set() && !this->callback_action->cancelled; + } +}; + +} // namespace + +TEST_CASE("MUTEX_TRY_LOCK on an unheld ProxyMutex constructs a guard whose is_locked() reports the successful acquisition", + "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + MUTEX_TRY_LOCK(guard, m, t); + + REQUIRE(guard.is_locked()); + REQUIRE(m->thread_holding == t); + REQUIRE(m->nthread_holding == 1); +} + +TEST_CASE("MUTEX_TRY_LOCK against a contended ProxyMutex constructs a guard whose is_locked() reports the failed acquisition", + "[inkevent][lock][multithread]") +{ + Ptr contended{new_ProxyMutex()}; + Ptr cont_self{new_ProxyMutex()}; + HoldOnEThread holder{cont_self.get(), contended}; + + REQUIRE(holder.wait_for_callback_start()); + + EThread *t = this_ethread(); + MUTEX_TRY_LOCK(guard, contended, t); + + REQUIRE_FALSE(guard.is_locked()); +} + +TEST_CASE("MUTEX_TRY_LOCK by the holding thread is reentrant and returns a guard reporting is_locked() == true", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + MUTEX_TRY_LOCK(outer, m, t); + REQUIRE(outer.is_locked()); + REQUIRE(m->nthread_holding == 1); + + { + MUTEX_TRY_LOCK(inner, m, t); + REQUIRE(inner.is_locked()); + REQUIRE(m->nthread_holding == 2); + } + + REQUIRE(m->nthread_holding == 1); + REQUIRE(m->thread_holding == t); +} + +TEST_CASE("A MUTEX_TRY_LOCK guard releases the lock at scope exit, leaving the ProxyMutex unheld", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + { + MUTEX_TRY_LOCK(guard, m, t); + REQUIRE(guard.is_locked()); + } + + REQUIRE(m->nthread_holding == 0); + REQUIRE(m->thread_holding == nullptr); +} + +TEST_CASE("MUTEX_RELEASE on a MUTEX_TRY_LOCK guard releases the lock early and flips is_locked() to false", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + MUTEX_TRY_LOCK(guard, m, t); + CHECK(guard.is_locked()); + + MUTEX_RELEASE(guard); + + REQUIRE_FALSE(guard.is_locked()); + REQUIRE(m->nthread_holding == 0); + REQUIRE(m->thread_holding == nullptr); +} + +TEST_CASE("MUTEX_RELEASE invoked twice on the same MUTEX_TRY_LOCK guard is a no-op on the second call", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + MUTEX_TRY_LOCK(guard, m, t); + MUTEX_RELEASE(guard); + CHECK_FALSE(guard.is_locked()); + + MUTEX_RELEASE(guard); + + REQUIRE_FALSE(guard.is_locked()); + REQUIRE(m->nthread_holding == 0); +} + +TEST_CASE("SCOPED_MUTEX_LOCK on an unheld ProxyMutex acquires the lock during construction", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + SCOPED_MUTEX_LOCK(guard, m, t); + + REQUIRE(m->thread_holding == t); + REQUIRE(m->nthread_holding == 1); +} + +TEST_CASE("A SCOPED_MUTEX_LOCK guard releases the lock when its enclosing scope ends", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + { + SCOPED_MUTEX_LOCK(guard, m, t); + REQUIRE(m->nthread_holding == 1); + } + + REQUIRE(m->nthread_holding == 0); + REQUIRE(m->thread_holding == nullptr); +} + +TEST_CASE("SCOPED_MUTEX_LOCK is reentrant when the calling EThread already holds the lock", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + SCOPED_MUTEX_LOCK(outer, m, t); + REQUIRE(m->nthread_holding == 1); + + { + SCOPED_MUTEX_LOCK(inner, m, t); + REQUIRE(m->nthread_holding == 2); + REQUIRE(m->thread_holding == t); + } + + REQUIRE(m->nthread_holding == 1); + REQUIRE(m->thread_holding == t); +} + +TEST_CASE("MUTEX_RELEASE on a SCOPED_MUTEX_LOCK guard releases the lock early and renders the destructor a no-op", + "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + { + SCOPED_MUTEX_LOCK(guard, m, t); + MUTEX_RELEASE(guard); + + REQUIRE(m->nthread_holding == 0); + REQUIRE(m->thread_holding == nullptr); + } + + REQUIRE(m->nthread_holding == 0); + REQUIRE(m->thread_holding == nullptr); +} + +TEST_CASE("MUTEX_TAKE_LOCK acquires an unheld ProxyMutex and records the calling thread as its holder", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + MUTEX_TAKE_LOCK(m, t); + + REQUIRE(m->thread_holding == t); + REQUIRE(m->nthread_holding == 1); + + MUTEX_UNTAKE_LOCK(m, t); +} + +TEST_CASE("MUTEX_TAKE_LOCK by the holding thread is reentrant and increments the reentry count without blocking", + "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + MUTEX_TAKE_LOCK(m, t); + MUTEX_TAKE_LOCK(m, t); + + REQUIRE(m->nthread_holding == 2); + REQUIRE(m->thread_holding == t); + + MUTEX_UNTAKE_LOCK(m, t); + MUTEX_UNTAKE_LOCK(m, t); + + REQUIRE(m->nthread_holding == 0); + REQUIRE(m->thread_holding == nullptr); +} + +TEST_CASE("After the holding EThread fully releases a contended ProxyMutex, MUTEX_TRY_LOCK on the main thread succeeds", + "[inkevent][lock][multithread]") +{ + Ptr contended{new_ProxyMutex()}; + Ptr cont_self{new_ProxyMutex()}; + HoldOnEThread holder{cont_self.get(), contended}; + + REQUIRE(holder.wait_for_callback_start()); + REQUIRE(holder.wait_for_callback_finish()); + + EThread *t = this_ethread(); + MUTEX_TRY_LOCK(guard, contended, t); + + REQUIRE(guard.is_locked()); + REQUIRE(contended->thread_holding == t); +} + +TEST_CASE("WEAK_SCOPED_MUTEX_LOCK on a non-null ProxyMutex acquires the lock during construction", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + WEAK_SCOPED_MUTEX_LOCK(guard, m, t); + + REQUIRE(m->thread_holding == t); + REQUIRE(m->nthread_holding == 1); +} + +TEST_CASE("A WEAK_SCOPED_MUTEX_LOCK guard releases its lock when its enclosing scope ends", "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + { + WEAK_SCOPED_MUTEX_LOCK(guard, m, t); + REQUIRE(m->nthread_holding == 1); + } + + REQUIRE(m->nthread_holding == 0); + REQUIRE(m->thread_holding == nullptr); +} + +TEST_CASE("WEAK_MUTEX_TRY_LOCK on an unheld ProxyMutex constructs a guard whose is_locked() reports the successful acquisition", + "[inkevent][lock]") +{ + Ptr m{new_ProxyMutex()}; + EThread *t = this_ethread(); + + WEAK_MUTEX_TRY_LOCK(guard, m, t); + + REQUIRE(guard.is_locked()); + REQUIRE(m->thread_holding == t); + REQUIRE(m->nthread_holding == 1); +} + +TEST_CASE("WEAK_MUTEX_TRY_LOCK against a contended ProxyMutex constructs a guard whose is_locked() reports the failed acquisition", + "[inkevent][lock][multithread]") +{ + Ptr contended{new_ProxyMutex()}; + Ptr cont_self{new_ProxyMutex()}; + HoldOnEThread holder{cont_self.get(), contended}; + + REQUIRE(holder.wait_for_callback_start()); + + EThread *t = this_ethread(); + WEAK_MUTEX_TRY_LOCK(guard, contended, t); + + REQUIRE_FALSE(guard.is_locked()); +} + +TEST_CASE("WEAK_MUTEX_TRY_LOCK on a null Ptr reports is_locked() == true", "[inkevent][lock]") +{ + Ptr empty; + EThread *t = this_ethread(); + + REQUIRE(empty.get() == nullptr); + + WEAK_MUTEX_TRY_LOCK(guard, empty, t); + + REQUIRE(guard.is_locked()); +} + +TEST_CASE("MUTEX_RELEASE on a null WEAK_MUTEX_TRY_LOCK guard clears is_locked() without unlocking a mutex", "[inkevent][lock]") +{ + Ptr empty; + EThread *t = this_ethread(); + + WEAK_MUTEX_TRY_LOCK(guard, empty, t); + CHECK(guard.is_locked()); + + MUTEX_RELEASE(guard); + + REQUIRE_FALSE(guard.is_locked()); +}