7.5 KiB
PANDORA'S BOX - OPTION 5 THREAD SAFETY ANALYSIS
Generated: October 27, 2025 23:45
Crash: Spell.cpp:603 - m_spellModTakingSpell assertion
Root Cause: Fire-and-forget removed synchronization
THE PROBLEM YOU IDENTIFIED
You're absolutely correct - Option 5 opened Pandora's Box!
What Option 5 Changed
BEFORE (OLD Synchronization Barrier):
// Submit bot updates to thread pool
for (bot in bots) {
futures.push_back(ThreadPool::Submit(updateLogic));
}
// WAIT FOR ALL BOT UPDATES TO COMPLETE
for (future in futures) {
future.wait_for(timeout); // BLOCKS main thread
}
// ONLY AFTER ALL BOTS FINISH:
// Main thread continues to Map::SendObjectUpdates()
AFTER (Option 5 Fire-and-Forget):
// Submit bot updates to thread pool
for (bot in bots) {
ThreadPool::Submit(updateLogic); // NO WAIT
}
// IMMEDIATELY CONTINUE (no waiting!)
// Main thread proceeds to Map::SendObjectUpdates()
// WHILE bot updates still running in background!
The Race Condition
Timeline with OLD code:
0ms: Main thread submits 100 bot updates
100ms: Main thread WAITS at synchronization barrier
200ms: Worker threads update bots (Player::Update)
300ms: All bot updates complete
400ms: Main thread wakes up
500ms: Main thread runs Map::SendObjectUpdates
Timeline with NEW code (Option 5):
0ms: Main thread submits 100 bot updates
1ms: Main thread IMMEDIATELY continues (no wait!)
2ms: Main thread runs Map::SendObjectUpdates
... Worker threads STILL updating bots in parallel!
RACE CONDITION:
- Map worker thread: Player::Update → Spell::~Spell
- Bot update thread: session->Update() → Player modification
- CRASH: Both accessing Player state simultaneously!
THE ASSERTION FAILURE
Spell.cpp:603:
Spell::~Spell()
{
ASSERT(m_caster->ToPlayer()->m_spellModTakingSpell != this);
// ↑ This fails because another thread is modifying the Player!
}
What Happened:
- Map worker thread starts destroying LOGINEFFECT spell
- Spell destructor checks
m_spellModTakingSpell - AT THE SAME TIME: Bot session update thread modifies Player spell state
- Assertion fails - data race!
WHY THIS DIDN'T HAPPEN BEFORE
With Synchronization Barrier (OLD)
The main thread BLOCKED until all bot sessions finished updating:
- Bot sessions run in thread pool
- BUT: Main thread waits for them to complete
- Map updates happen AFTER bot updates finish
- No overlap = No race condition
With Fire-and-Forget (Option 5)
The main thread does NOT wait:
- Bot sessions run in thread pool
- Main thread continues immediately
- Map updates happen WHILE bot updates still running
- Overlap = Race condition!
THE REAL PROBLEM
TrinityCore's architecture assumes single-threaded access to Player objects:
- Map system updates players on map worker threads
- Session system was designed for sequential updates
- Player objects are NOT thread-safe for concurrent access
Our bot sessions are now updating Player objects in parallel with map updates!
WHY THE SYNCHRONIZATION BARRIER EXISTED
The OLD code's synchronization barrier was there for a CRITICAL REASON:
Ensure bot session updates complete BEFORE main thread continues
Without this barrier:
- Bot updates run in background
- Main thread proceeds immediately
- Map updates start while bots still updating
- Multiple threads access same Player → CRASH
OPTIONS TO FIX
Option A: Restore Synchronization (Regression)
Go back to waiting for futures - but this brings back the deadlock!
Option B: Single-Threaded Bot Updates
Don't use thread pool at all - update bots sequentially on main thread
Option C: Per-Player Locking
Add mutex to each Player object - but massive performance hit
Option D: Message Passing Architecture
Bot updates send "messages" to apply changes, main thread applies them
Option E: Separate Update Phase
Update bots in DIFFERENT phase than map updates (before or after)
RECOMMENDED FIX: Option E (Separate Phase)
Concept: Update bot sessions in a phase where map updates are NOT running
void World::Update(uint32 diff)
{
// Phase 1: Update bot sessions (thread pool, parallel)
sBotWorldSessionMgr->UpdateSessions(diff);
// Phase 2: WAIT for bot sessions to complete
sBotWorldSessionMgr->WaitForCompletion();
// Phase 3: Update maps (now safe - bots done)
sMapMgr->Update(diff);
}
Benefits:
- Bot sessions still run in parallel (performance)
- But isolated from map updates (thread safety)
- No ObjectAccessor deadlock (different phase)
- Fire-and-forget within bot update phase only
IMPLEMENTATION PLAN
Step 1: Add Completion Tracking
// BotWorldSessionMgr.h
class BotWorldSessionMgr {
private:
std::atomic<uint32> _pendingUpdates{0};
std::condition_variable _completionCV;
std::mutex _completionMutex;
public:
void WaitForCompletion();
};
Step 2: Track Submitted Tasks
// BotWorldSessionMgr.cpp UpdateSessions()
_pendingUpdates.store(sessionsToUpdate.size());
for (auto session : sessionsToUpdate) {
auto updateLogic = [this, ...]() {
// ... bot update ...
// Decrement pending counter
uint32 remaining = _pendingUpdates.fetch_sub(1) - 1;
if (remaining == 0) {
// Last bot finished - notify waiting thread
std::lock_guard lock(_completionMutex);
_completionCV.notify_one();
}
};
ThreadPool::Submit(updateLogic); // Fire-and-forget
}
Step 3: Wait for Completion
void BotWorldSessionMgr::WaitForCompletion()
{
std::unique_lock lock(_completionMutex);
_completionCV.wait(lock, [this] {
return _pendingUpdates.load() == 0;
});
}
Step 4: Integrate into World::Update
// World.cpp
void World::Update(uint32 diff)
{
// ... other systems ...
// PHASE 1: Start bot updates (parallel, non-blocking)
sBotWorldSessionMgr->UpdateSessions(diff);
// PHASE 2: Wait for bot updates to complete
sBotWorldSessionMgr->WaitForCompletion();
// PHASE 3: Now safe to update maps
sMapMgr->Update(diff);
// ... rest of update ...
}
COMPARISON
| Approach | Parallel | Safe | Deadlock Risk |
|---|---|---|---|
| OLD (Sync Barrier) | ✅ Yes | ❌ No | ✅ YES (deadlock) |
| Option 5 (Fire-forget) | ✅ Yes | ❌ No | ✅ No (but crashes!) |
| Option E (Phased) | ✅ Yes | ✅ Yes | ✅ No |
WHY OPTION E FIXES BOTH ISSUES
Problem 1: Main Thread Deadlock (OLD)
Cause: Main thread waits for bots → bots wait for ObjectAccessor → ObjectAccessor waits for main thread
Fix: Bots update in SEPARATE PHASE before maps
- Bots don't compete with Map::SendObjectUpdates for locks
- No circular dependency
Problem 2: Race Condition (Option 5)
Cause: Bot updates and map updates run simultaneously
Fix: Bots complete BEFORE maps start
- No simultaneous access to Player objects
- Thread-safe by design
CONCLUSION
You were absolutely right - Option 5 opened Pandora's Box!
The synchronization barrier was protecting us from race conditions, even though it also caused deadlocks.
The fix: Keep the fire-and-forget pattern BUT wait for completion before map updates start, not during session updates.
This gives us:
- ✅ Parallel bot updates (performance)
- ✅ No deadlock (separate phase)
- ✅ Thread safety (sequential phases)
NEXT ACTION: Implement Option E (Phased Updates) to close Pandora's Box properly.