5.2 KiB
5.2 KiB
DEADLOCK FIX ATTEMPT #10 - ROOT CAUSE ANALYSIS
Executive Summary
After 9 failed attempts, I have identified the EXACT recursive mutex deadlock path that caused "resource deadlock would occur" exceptions across accounts 2-38.
The Root Cause - Writer-Preference Deadlock
The Fatal Sequence
- Thread A (BotAI::UpdateStrategies) acquires
shared_lock(_mutex)at BotAI.cpp:220 - Thread B (BotAI::ActivateStrategy) tries to acquire
unique_lock(_mutex)at BotAI.cpp:737- Thread B BLOCKS waiting for Thread A to release shared lock
- Thread A continues execution while holding shared_lock
- Thread A calls
strategy->UpdateBehavior()at BotAI.cpp:246 - Strategy callback calls back into
BotAI::GetStrategy()at BotAI.cpp:708 GetStrategy()tries to acquire ANOTHERshared_lock(_mutex)- DEADLOCK: Thread A cannot acquire second shared_lock because Thread B is waiting for unique_lock (writer-preference)
Why This Happens with std::shared_mutex
Windows std::shared_mutex uses writer-preference:
- If a writer (unique_lock) is waiting, new readers (shared_lock) are BLOCKED
- This prevents reader starvation but creates deadlock when:
- Reader A holds shared_lock
- Writer B waits for unique_lock
- Reader A tries to acquire ANOTHER shared_lock → DEADLOCK
Call Stack Evidence
BotAI::UpdateAI()
→ BotAI::UpdateStrategies() [Line 119]
→ std::shared_lock lock(_mutex) [Line 220] ← LOCK ACQUIRED
→ strategy->IsActive(this) [Line 228] ← Safe (atomic)
→ strategy->UpdateBehavior(this, diff) [Line 246] ← CALLBACK WHILE HOLDING LOCK
→ LeaderFollowBehavior::UpdateBehavior()
→ BotAI::GetStrategy("follow") [Indirect call]
→ std::shared_lock lock(_mutex) [Line 708] ← ATTEMPTS SECOND LOCK
→ **DEADLOCK** (writer waiting blocks this reader)
Secondary Deadlock Path
OnGroupJoined() also has mutex re-acquisition:
BotAI::OnGroupJoined()
→ GetStrategy("follow") [Line 610]
→ std::shared_lock lock(_mutex) [Line 708] ← FIRST LOCK
→ ActivateStrategy("follow") [Line 632]
→ std::unique_lock lock(_mutex) [Line 737] ← DEADLOCK if another thread holds shared_lock
The Fix - Complete Lock Release Before Callbacks
Changed: BotAI.cpp:210-266 UpdateStrategies()
Before (Deadlock-prone):
std::vector<Strategy*> strategiesToUpdate;
{
std::shared_lock lock(_mutex);
for (auto const& strategyName : _activeStrategies)
{
auto it = _strategies.find(strategyName);
if (it != _strategies.end())
{
Strategy* strategy = it->second.get();
if (strategy && strategy->IsActive(this)) // ← IsActive() while holding lock
{
strategiesToUpdate.push_back(strategy);
}
}
}
} // Lock released
// Call callbacks
for (Strategy* strategy : strategiesToUpdate)
{
followBehavior->UpdateFollowBehavior(this, diff); // ← May call GetStrategy()
}
After (Deadlock-free):
std::vector<std::pair<Strategy*, bool>> strategiesToCheck;
{
std::shared_lock lock(_mutex);
for (auto const& strategyName : _activeStrategies)
{
auto it = _strategies.find(strategyName);
if (it != _strategies.end())
{
// Just collect pointer - no method calls while locked
strategiesToCheck.push_back({it->second.get(), false});
}
}
} // ← LOCK RELEASED BEFORE ANY METHOD CALLS
// Check IsActive() WITHOUT holding lock (it's atomic - thread-safe)
std::vector<Strategy*> strategiesToUpdate;
for (auto& [strategy, dummy] : strategiesToCheck)
{
if (strategy && strategy->IsActive(this)) // ← Safe: no lock held, IsActive() is atomic
{
strategiesToUpdate.push_back(strategy);
}
}
// Call callbacks WITHOUT holding ANY lock
for (Strategy* strategy : strategiesToUpdate)
{
followBehavior->UpdateFollowBehavior(this, diff); // ← Safe: can call GetStrategy()
}
Why This Fix Works
- Minimal Lock Hold Time: Acquire lock only to copy strategy pointers
- No Method Calls Under Lock: Don't call
IsActive()or any other methods while locked - Atomic IsActive(): Strategy::IsActive() uses
std::atomic<bool>- safe to call without mutex - Free Callbacks: Strategy callbacks can now safely call
GetStrategy()without deadlock
Files Modified
src/modules/Playerbot/AI/BotAI.cpp:210-266- UpdateStrategies() complete rewrite
Testing Required
- Start server with 100+ bots (accounts 2-38 especially)
- Monitor for "resource deadlock would occur" exceptions
- Verify follow behavior still works correctly
- Check performance impact (should be minimal - fewer lock acquisitions)
Thread Safety Guarantee
Strategy::IsActive()returnsstd::atomic<bool> _active- safe to read without lockStrategy*pointers are stable (strategies not deleted while active)_activeStrategiesis only read with lock held- Callbacks execute with NO locks held
Expected Outcome
ZERO "resource deadlock would occur" exceptions across all bot accounts.