Files
ThordekkCore/PANDORAS_BOX_ANALYSIS.md
T
2026-01-20 21:33:16 -03:00

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:

  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)


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.