298 lines
7.5 KiB
Markdown
298 lines
7.5 KiB
Markdown
# 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):**
|
|
```cpp
|
|
// 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):**
|
|
```cpp
|
|
// 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**:
|
|
```cpp
|
|
Spell::~Spell()
|
|
{
|
|
ASSERT(m_caster->ToPlayer()->m_spellModTakingSpell != this);
|
|
// ↑ This fails because another thread is modifying the Player!
|
|
}
|
|
```
|
|
|
|
**What Happened:**
|
|
1. Map worker thread starts destroying LOGINEFFECT spell
|
|
2. Spell destructor checks `m_spellModTakingSpell`
|
|
3. **AT THE SAME TIME**: Bot session update thread modifies Player spell state
|
|
4. 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**:
|
|
|
|
1. **Map system** updates players on map worker threads
|
|
2. **Session system** was designed for **sequential** updates
|
|
3. **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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
void BotWorldSessionMgr::WaitForCompletion()
|
|
{
|
|
std::unique_lock lock(_completionMutex);
|
|
_completionCV.wait(lock, [this] {
|
|
return _pendingUpdates.load() == 0;
|
|
});
|
|
}
|
|
```
|
|
|
|
### Step 4: Integrate into World::Update
|
|
|
|
```cpp
|
|
// 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.
|