From ccf0ab77bfcbe2126b0254a016cd33e07afbb03c Mon Sep 17 00:00:00 2001 From: Hayodea Hekol Date: Tue, 30 Sep 2025 13:57:23 -0400 Subject: [PATCH] Move SerializedAsyncContin template method impls into header --- include/serializedAsynchronousContinuation.h | 220 +++++++++++++++- smocore/CMakeLists.txt | 1 - .../serializedAsynchronousContinuation.cpp | 238 ------------------ 3 files changed, 218 insertions(+), 241 deletions(-) delete mode 100644 smocore/serializedAsynchronousContinuation.cpp diff --git a/include/serializedAsynchronousContinuation.h b/include/serializedAsynchronousContinuation.h index 3a30a77..0b2c066 100644 --- a/include/serializedAsynchronousContinuation.h +++ b/include/serializedAsynchronousContinuation.h @@ -341,8 +341,27 @@ public: * @brief Handle a likely deadlock situation by logging debug information * @param firstFailedQutex The first qutex that failed acquisition */ - void handleDeadlock(Qutex &firstFailedQutex); - void handleGridlock(Qutex &firstFailedQutex); + void handleDeadlock(const Qutex &firstFailedQutex) + { + std::cerr << __func__ << ": Deadlock: " + << "Lockvoker has been waiting for " + << std::chrono::duration_cast( + std::chrono::steady_clock::now() - this->creationTimestamp) + .count() + << "ms, failed on qutex @" << &firstFailedQutex + << " (" << firstFailedQutex.name << ")" << std::endl; + } + + void handleGridlock(const Qutex &firstFailedQutex) + { + std::cerr << __func__ << ": Gridlock: " + << "Lockvoker has been waiting for " + << std::chrono::duration_cast( + std::chrono::steady_clock::now() - this->creationTimestamp) + .count() + << "ms, failed on qutex @" << &firstFailedQutex + << " (" << firstFailedQutex.name << ")" << std::endl; + } #endif private: @@ -356,6 +375,203 @@ public: }; }; +/******************************************************************************/ + +#ifdef CONFIG_ENABLE_DEBUG_LOCKS + +template +template +bool +SerializedAsynchronousContinuation +::LockerAndInvoker +::traceContinuationHistoryForDeadlockOn(Qutex& firstFailedQutex) +{ + /** EXPLANATION: + * In this function we will trace through the chain of continuations that + * led up to this Lockvoker's continuation. For each continuation which is + * a SerializedAsynchronousContinuation, we check through its LockSet to see + * if it contains the lock that failed acquisition. If it does, we have a + * deadlock. + */ + + /* We can't start with the continuation directly referenced by this starting + * Lockvoker as it would contain the all locks we're currently trying to + * acquire...and rightly so because it's the continuation for this current + * lockvoker. + */ + for (std::shared_ptr currContin = + this->serializedContinuation.getCallersContinuationShPtr(); + currContin != nullptr; + currContin = currContin->getCallersContinuationShPtr()) + { + auto serializedCont = std::dynamic_pointer_cast< + SerializedAsynchronousContinuation>(currContin); + + if (serializedCont == nullptr) { continue; } + + // Check if the firstFailedQutex is in this continuation's LockSet + try { + serializedCont->requiredLocks.getLockUsageDesc(firstFailedQutex); + } catch (const std::runtime_error& e) { + std::cerr << __func__ << ": " << e.what() << std::endl; + continue; + } + + std::cout << __func__ << ":Deadlock detected: Found " + << "firstFailedQutex @" << &firstFailedQutex + << " (" << firstFailedQutex.name << ") in LockSet of " + << "SerializedAsynchronousContinuation @" + << serializedCont.get() << std::endl; + + return true; + } + + return false; +} + +template +std::unique_ptr>> +SerializedAsynchronousContinuation::getAcquiredQutexHistory() +const +{ + auto heldLocks = std::make_unique< + std::forward_list>>(); + + /** EXPLANATION: + * Walk through the continuation chain to collect all acquired locks + * + * We don't add the current continuation's locks because it's the one + * failing to acquire locks and backing off. So we start from the previous + * continuation. + */ + for (std::shared_ptr currContin = + this->getCallersContinuationShPtr(); + currContin != nullptr; + currContin = currContin->getCallersContinuationShPtr()) + { + auto serializedCont = std::dynamic_pointer_cast< + SerializedAsynchronousContinuation>(currContin); + + if (serializedCont == nullptr) { continue; } + + // Add this continuation's locks to the held locks list + for (size_t i = 0; i < serializedCont->requiredLocks.locks.size(); ++i) + { + heldLocks->push_front(serializedCont->requiredLocks.locks[i].first); + } + } + + return heldLocks; +} + +template +template +bool +SerializedAsynchronousContinuation +::LockerAndInvoker +::obsolete::traceContinuationHistoryForGridlockOn(Qutex &firstFailedQutex) +{ + /** EXPLANATION: + * In this function we check for gridlocks which are slightly different + * from deadlocks. In a gridlock, two requests are waiting for locks that + * are held by the other. I.e: + * + * R1 holds LockA and is waiting for LockB. + * R2 holds LockB and is waiting for LockA. + * + * This differs from deadlocks because it's not a single request which is + * attempting to re-acquire a lock that it already holds. + * + * To detect this condition, we wait until the acquisition timeout has + * expired. Then: we extract the current owner of the first lock we're + * failing to acquire. + * + * From there, we go through each of the locks in the foreign owner's + * current (i.e: immediate, most recent continuation's) required LockSet. + * For each of the locks in the foreign owner's most immediate required + * LockSet, we trace backward in our *OWN* history to see if any of *OUR* + * continuations (excluding our most immediate continuation) contains that + * lock. + * + * If we find a match, that means that we're holding a lock that the foreign + * owner is waiting for. And we already know that the foreign owner is + * holding a lock that we're waiting for (when we extracted the current + * owner of the first failed lock in our most immediate Lockset). + * + * Hence, we have a gridlock. + */ + + std::shared_ptr foreignOwnerShPtr = + firstFailedQutex.getCurrOwner(); + // If no current owner, can't be a gridlock + if (foreignOwnerShPtr == nullptr) + { return false; } + + // Use reference for the rest of the function for safety. + LockerAndInvokerBase &foreignOwner = *foreignOwnerShPtr; + + /* For each lock in the foreign owner's LockSet, check if we hold it + * in any of our previous continuations (excluding our most immediate one) + */ + for (size_t i = 0; i < foreignOwner.getLockSetSize(); ++i) + { + Qutex& foreignLock = foreignOwner.getLockAt(i); + + /* Skip the firstFailedQutex since we already know the foreign owner + * holds it -- hence it's impossible for any of our previous + * continuations to hold it. + */ + if (&foreignLock == &firstFailedQutex) + { continue; } + + /** EXPLANATION: + * Trace backward through our continuation history (excluding our most + * immediate continuation). + * + * The reason we exclude our most immediate continuation is because the + * LockSet acquisition algorithm backs off if it fails to acquire ALL + * locks in the set. So if the lock that the foreign owner is waiting + * for is in our most immediate continuation, and NOT in one of our + * previous continuations, then we will back off and the foreign owner + * should eventually be able to acquire that lock. + */ + for (std::shared_ptr currContin = + this->serializedContinuation.getCallersContinuationShPtr(); + currContin != nullptr; + currContin = currContin->getCallersContinuationShPtr()) + { + auto serializedCont = std::dynamic_pointer_cast< + SerializedAsynchronousContinuation>(currContin); + + if (serializedCont == nullptr) { continue; } + + // Check if this continuation holds the foreign lock + try { + const auto& lockUsageDesc = serializedCont->requiredLocks + .getLockUsageDesc(foreignLock); + + // Matched! We hold a lock that the foreign owner is waiting for + std::cout << __func__ << ": Gridlock detected: We hold lock @" + << &foreignLock << " (" << foreignLock.name << ") in " + "continuation @" << serializedCont.get() + << ", while foreign owner @" << &foreignOwner + << " holds lock @" << &firstFailedQutex << " (" + << firstFailedQutex.name << ") that we're waiting for" + << std::endl; + + return true; + } catch (const std::runtime_error& e) { + // This continuation doesn't hold the foreign lock. Continue. + continue; + } + } + } + + return false; +} + +#endif // CONFIG_ENABLE_DEBUG_LOCKS + } // namespace smo #endif // SERIALIZED_ASYNCHRONOUS_CONTINUATION_H diff --git a/smocore/CMakeLists.txt b/smocore/CMakeLists.txt index 28397d5..5d395dc 100644 --- a/smocore/CMakeLists.txt +++ b/smocore/CMakeLists.txt @@ -11,7 +11,6 @@ add_library(smocore STATIC painfulQuale.cpp qutex.cpp lockerAndInvokerBase.cpp - serializedAsynchronousContinuation.cpp # Body body/body.cpp diff --git a/smocore/serializedAsynchronousContinuation.cpp b/smocore/serializedAsynchronousContinuation.cpp deleted file mode 100644 index 864a28a..0000000 --- a/smocore/serializedAsynchronousContinuation.cpp +++ /dev/null @@ -1,238 +0,0 @@ -#include -#include -#include - -namespace smo { - -#ifdef CONFIG_ENABLE_DEBUG_LOCKS - -template -template -bool -SerializedAsynchronousContinuation -::LockerAndInvoker -::traceContinuationHistoryForDeadlockOn(Qutex& firstFailedQutex) -{ - /** EXPLANATION: - * In this function we will trace through the chain of continuations that - * led up to this Lockvoker's continuation. For each continuation which is - * a SerializedAsynchronousContinuation, we check through its LockSet to see - * if it contains the lock that failed acquisition. If it does, we have a - * deadlock. - */ - - /* We can't start with the continuation directly referenced by this starting - * Lockvoker as it would contain the all locks we're currently trying to - * acquire...and rightly so because it's the continuation for this current - * lockvoker. - */ - for (std::shared_ptr currContin = - this->serializedContinuation.getCallersContinuationShPtr(); - currContin != nullptr; - currContin = currContin->getCallersContinuationShPtr()) - { - auto serializedCont = std::dynamic_pointer_cast< - SerializedAsynchronousContinuation>(currContin); - - if (serializedCont == nullptr) { continue; } - - // Check if the firstFailedQutex is in this continuation's LockSet - try { - const auto& lockUsageDesc = serializedCont->requiredLocks - .getLockUsageDesc(firstFailedQutex); - } catch (const std::runtime_error& e) { - std::cerr << __func__ << ": " << e.what() << std::endl; - continue; - } - - std::cout << __func__ << ":Deadlock detected: Found " - << "firstFailedQutex @" << &firstFailedQutex - << " (" << firstFailedQutex.name << ") in LockSet of " - << "SerializedAsynchronousContinuation @" - << serializedCont.get() << std::endl; - - return true; - } - - return false; -} - -template -template -bool -SerializedAsynchronousContinuation -::LockerAndInvoker -::obsolete::traceContinuationHistoryForGridlockOn(Qutex &firstFailedQutex) -{ - /** EXPLANATION: - * In this function we check for gridlocks which are slightly different - * from deadlocks. In a gridlock, two requests are waiting for locks that - * are held by the other. I.e: - * - * R1 holds LockA and is waiting for LockB. - * R2 holds LockB and is waiting for LockA. - * - * This differs from deadlocks because it's not a single request which is - * attempting to re-acquire a lock that it already holds. - * - * To detect this condition, we wait until the acquisition timeout has - * expired. Then: we extract the current owner of the first lock we're - * failing to acquire. - * - * From there, we go through each of the locks in the foreign owner's - * current (i.e: immediate, most recent continuation's) required LockSet. - * For each of the locks in the foreign owner's most immediate required - * LockSet, we trace backward in our *OWN* history to see if any of *OUR* - * continuations (excluding our most immediate continuation) contains that - * lock. - * - * If we find a match, that means that we're holding a lock that the foreign - * owner is waiting for. And we already know that the foreign owner is - * holding a lock that we're waiting for (when we extracted the current - * owner of the first failed lock in our most immediate Lockset). - * - * Hence, we have a gridlock. - */ - - std::shared_ptr foreignOwnerShPtr = - firstFailedQutex.getCurrOwner(); - // If no current owner, can't be a gridlock - if (foreignOwnerShPtr == nullptr) - { return false; } - - // Use reference for the rest of the function for safety. - LockerAndInvokerBase &foreignOwner = *foreignOwnerShPtr; - - /* For each lock in the foreign owner's LockSet, check if we hold it - * in any of our previous continuations (excluding our most immediate one) - */ - for (size_t i = 0; i < foreignOwner.getLockSetSize(); ++i) - { - Qutex& foreignLock = foreignOwner.getLockAt(i); - - /* Skip the firstFailedQutex since we already know the foreign owner - * holds it -- hence it's impossible for any of our previous - * continuations to hold it. - */ - if (&foreignLock == &firstFailedQutex) - { continue; } - - /** EXPLANATION: - * Trace backward through our continuation history (excluding our most - * immediate continuation). - * - * The reason we exclude our most immediate continuation is because the - * LockSet acquisition algorithm backs off if it fails to acquire ALL - * locks in the set. So if the lock that the foreign owner is waiting - * for is in our most immediate continuation, and NOT in one of our - * previous continuations, then we will back off and the foreign owner - * should eventually be able to acquire that lock. - */ - for (std::shared_ptr currContin = - this->serializedContinuation.getCallersContinuationShPtr(); - currContin != nullptr; - currContin = currContin->getCallersContinuationShPtr()) - { - auto serializedCont = std::dynamic_pointer_cast< - SerializedAsynchronousContinuation>(currContin); - - if (serializedCont == nullptr) { continue; } - - // Check if this continuation holds the foreign lock - try { - const auto& lockUsageDesc = serializedCont->requiredLocks - .getLockUsageDesc(foreignLock); - - // Matched! We hold a lock that the foreign owner is waiting for - std::cout << __func__ << ": Gridlock detected: We hold lock @" - << &foreignLock << " (" << foreignLock.name << ") in " - "continuation @" << serializedCont.get() - << ", while foreign owner @" << &foreignOwner - << " holds lock @" << &firstFailedQutex << " (" - << firstFailedQutex.name << ") that we're waiting for" - << std::endl; - - return true; - } catch (const std::runtime_error& e) { - // This continuation doesn't hold the foreign lock. Continue. - continue; - } - } - } - - return false; -} - -template -template -void -SerializedAsynchronousContinuation -::LockerAndInvoker -::handleDeadlock(Qutex &firstFailedQutex) -{ - std::cerr << __func__ << ": Deadlock: " - << "Lockvoker has been waiting for " - << std::chrono::duration_cast( - std::chrono::steady_clock::now() - this->creationTimestamp) - .count() - << "ms, failed on qutex @" << &firstFailedQutex - << " (" << firstFailedQutex.name << ")" << std::endl; -} - -template -template -void -SerializedAsynchronousContinuation -::LockerAndInvoker -::handleGridlock(Qutex &firstFailedQutex) -{ - std::cerr << __func__ << ": Gridlock: " - << "Lockvoker has been waiting for " - << std::chrono::duration_cast( - std::chrono::steady_clock::now() - this->creationTimestamp) - .count() - << "ms, failed on qutex @" << &firstFailedQutex - << " (" << firstFailedQutex.name << ")" << std::endl; -} - -#endif - -template -std::unique_ptr>> -SerializedAsynchronousContinuation::getAcquiredQutexHistory() -const -{ - auto heldLocks = std::make_unique< - std::forward_list>>(); - - /** EXPLANATION: - * Walk through the continuation chain to collect all acquired locks - * - * We don't add the current continuation's locks because it's the one - * failing to acquire locks and backing off. So we start from the previous - * continuation. - */ - for (std::shared_ptr currContin = - this->getCallersContinuationShPtr(); - currContin != nullptr; - currContin = currContin->getCallersContinuationShPtr()) - { - auto serializedCont = std::dynamic_pointer_cast< - SerializedAsynchronousContinuation>(currContin); - - if (serializedCont == nullptr) { continue; } - - // Add this continuation's locks to the held locks list - for (size_t i = 0; i < serializedCont->requiredLocks.locks.size(); ++i) - { - heldLocks->push_front(serializedCont->requiredLocks.locks[i].first); - } - } - - return heldLocks; -} - -// Explicit template instantiations for the types we need -// Add more as needed for your specific use cases - -} // namespace smo