Fix/magic effects read race - #940
eduardosmaniotto wants to merge 4 commits into
Conversation
MagicEffectsList exposed its mutable SortedList directly while effect-expiry timers mutated it under lock, so lock-free readers (BuffHandler.IsEffectActive, BotBuffHandler.HasEffect) could hit ArgumentException from Values.ToArray() mid-copy and abort the helper tick. The branch hotfix widened the catch to also swallow that variant, but left every other reader racy. Centralize synchronization in MagicEffectsList behind a Lock with thread-safe queries (ContainsEffect, ContainsAnyEffect, HasEffect, TryGetEffect, GetActiveEffectsSnapshot) and migrate all GameLogic and GameServer readers onto them. The live ActiveEffects reference is removed so the invariant (all access under lock) is enforced, not documented. Tests now seed state via AddEffectAsync and assert via snapshots.
ReviewOverall: this is the right fix. Moving synchronization into A few things worth looking at: 1.
|
Subscribe EffectTimeOut inside the lock and skip already-disposed effects in AddEffectAsync, closing the race where a timer firing between Add and subscribe left a stuck entry that poisoned _contains and could spin ClearAllEffectsAsync forever; ClearAll now also force-removes a head that survives its own disposal. Add a TryGetEffect(MagicEffectDefinition) overload and move the invisibility lookup back to definition matching, narrow EffectNumbers to const short and drop the truncating int overload (explicit byte casts at the packet boundary), and mark the async twins as source-compat shims. Tests seed via AddEffectAsync with teardown cleanup, assert via snapshots, and a new concurrency test hammers add/expiry against all four read APIs to pin the fix.
2c231ce to
bbce102
Compare
Fixes #939 — Offline helper tick crashes with
ArgumentException: Destination array was not long enough.Problem
Bots fail their offline helper tick with:
System.ArgumentException: Destination array was not long enough.
at MUnique.OpenMU.GameLogic.Offline.BuffHandler.IsEffectActive(...)
at MUnique.OpenMU.GameLogic.Offline.BuffHandler.NeedsPartyBuff(...)
at MUnique.OpenMU.GameLogic.Offline.BuffHandler.TryApplyPartyBuffAsync(...)
Each occurrence aborts that bot's tick, so the buff, loot, and attack
steps behind it don't run that round.
Root cause
MagicEffectsList.ActiveEffectsis a plainSortedListmutated oneffect-expiry timer threads, but readers enumerated it lock-free via
Values.ToArray()(BuffHandler.IsEffectActive,BotBuffHandler.HasEffect, and ~10 other call sites).ToArray()sizes its destination from
Count, then copies — if an effect isadded or refreshed in between, the source outgrows the destination
and the copy throws. Likelihood scales with party-buff scan
frequency: every ~500 ms tick, per in-range party member, against a
list changing on independent timers.
Fix
MagicEffectsListnow owns all synchronization (aLock; everypreviously locked region was await-free) and exposes thread-safe
queries:
ContainsEffect,ContainsAnyEffect,HasEffect,TryGetEffect,GetActiveEffectsSnapshot(+Async).directly exposed mutable collection is removed so the invariant is
enforced rather than advisory.
AddEffectAsyncpath and assertvia snapshots.