fix(plugins): close LA5 ownership races
This commit is contained in:
parent
fbe9c8a288
commit
f820eb258d
14 changed files with 467 additions and 107 deletions
|
|
@ -16,6 +16,7 @@ public sealed class RuntimeEntityDirectory
|
|||
public const uint LastLocalEntityId = 0x3FFF_FFFFu;
|
||||
|
||||
private readonly InboundPhysicsStateController _inbound = new();
|
||||
private readonly object _activeGate = new();
|
||||
private readonly Dictionary<uint, RuntimeEntityRecord> _activeByGuid = new();
|
||||
private readonly Dictionary<(uint Guid, ushort Incarnation), RuntimeEntityRecord>
|
||||
_teardownByIncarnation = new();
|
||||
|
|
@ -35,10 +36,27 @@ public sealed class RuntimeEntityDirectory
|
|||
_nextLocalEntityId = firstLocalEntityId;
|
||||
}
|
||||
|
||||
public int Count => _activeByGuid.Count;
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_activeGate)
|
||||
return _activeByGuid.Count;
|
||||
}
|
||||
}
|
||||
public int PendingTeardownCount => _teardownByIncarnation.Count;
|
||||
public int ClaimedLocalIdCount => _byLocalId.Count;
|
||||
public int ClaimedLocalIdCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_activeGate)
|
||||
return _byLocalId.Count;
|
||||
}
|
||||
}
|
||||
public ulong SessionLifetimeVersion { get; private set; }
|
||||
/// <summary>Update-thread-only borrowed collection. Cross-thread hosts use
|
||||
/// <see cref="AcquireActiveRead"/> through <c>IRuntimeEntityView.Visit</c>
|
||||
/// so membership cannot change during enumeration.</summary>
|
||||
public IReadOnlyCollection<RuntimeEntityRecord> ActiveRecords => _activeByGuid.Values;
|
||||
public IReadOnlyCollection<RuntimeEntityRecord> TeardownRecords =>
|
||||
_teardownByIncarnation.Values;
|
||||
|
|
@ -87,34 +105,48 @@ public sealed class RuntimeEntityDirectory
|
|||
|
||||
public RuntimeEntityRecord AddActive(WorldSession.EntitySpawn snapshot)
|
||||
{
|
||||
if (_activeByGuid.ContainsKey(snapshot.Guid))
|
||||
lock (_activeGate)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{snapshot.Guid:X8} already has an active incarnation.");
|
||||
}
|
||||
if (_activeByGuid.ContainsKey(snapshot.Guid))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{snapshot.Guid:X8} already has an active incarnation.");
|
||||
}
|
||||
|
||||
var record = new RuntimeEntityRecord(snapshot);
|
||||
_activeByGuid.Add(snapshot.Guid, record);
|
||||
try
|
||||
{
|
||||
ClaimLocalId(record);
|
||||
return record;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_activeByGuid.Remove(snapshot.Guid);
|
||||
throw;
|
||||
var record = new RuntimeEntityRecord(snapshot);
|
||||
_activeByGuid.Add(snapshot.Guid, record);
|
||||
try
|
||||
{
|
||||
ClaimLocalId(record);
|
||||
return record;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_activeByGuid.Remove(snapshot.Guid);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveActive(uint guid, out RuntimeEntityRecord? record) =>
|
||||
_activeByGuid.Remove(guid, out record);
|
||||
public bool RemoveActive(uint guid, out RuntimeEntityRecord? record)
|
||||
{
|
||||
lock (_activeGate)
|
||||
return _activeByGuid.Remove(guid, out record);
|
||||
}
|
||||
|
||||
public bool RemoveActive(RuntimeEntityRecord expected)
|
||||
{
|
||||
if (!IsCurrent(expected))
|
||||
return false;
|
||||
return _activeByGuid.Remove(expected.ServerGuid);
|
||||
lock (_activeGate)
|
||||
{
|
||||
if (!_activeByGuid.TryGetValue(
|
||||
expected.ServerGuid,
|
||||
out RuntimeEntityRecord? current)
|
||||
|| !ReferenceEquals(current, expected))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return _activeByGuid.Remove(expected.ServerGuid);
|
||||
}
|
||||
}
|
||||
|
||||
public void RetainTeardown(RuntimeEntityRecord record)
|
||||
|
|
@ -157,46 +189,52 @@ public sealed class RuntimeEntityDirectory
|
|||
|
||||
public uint ClaimLocalId(RuntimeEntityRecord record)
|
||||
{
|
||||
if (!IsKnown(record))
|
||||
lock (_activeGate)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A local id can only be claimed for an active or retained incarnation.");
|
||||
if (!IsKnown(record))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A local id can only be claimed for an active or retained incarnation.");
|
||||
}
|
||||
|
||||
if (record.LocalEntityId is { } existing)
|
||||
return existing;
|
||||
|
||||
uint start = _nextLocalEntityId;
|
||||
do
|
||||
{
|
||||
uint candidate = _nextLocalEntityId;
|
||||
_nextLocalEntityId = candidate == LastLocalEntityId
|
||||
? FirstLocalEntityId
|
||||
: candidate + 1u;
|
||||
if (_byLocalId.ContainsKey(candidate))
|
||||
continue;
|
||||
|
||||
_byLocalId.Add(candidate, record);
|
||||
record.LocalEntityId = candidate;
|
||||
return candidate;
|
||||
}
|
||||
while (_nextLocalEntityId != start);
|
||||
|
||||
throw new InvalidOperationException("The live entity id namespace is exhausted.");
|
||||
}
|
||||
|
||||
if (record.LocalEntityId is { } existing)
|
||||
return existing;
|
||||
|
||||
uint start = _nextLocalEntityId;
|
||||
do
|
||||
{
|
||||
uint candidate = _nextLocalEntityId;
|
||||
_nextLocalEntityId = candidate == LastLocalEntityId
|
||||
? FirstLocalEntityId
|
||||
: candidate + 1u;
|
||||
if (_byLocalId.ContainsKey(candidate))
|
||||
continue;
|
||||
|
||||
_byLocalId.Add(candidate, record);
|
||||
record.LocalEntityId = candidate;
|
||||
return candidate;
|
||||
}
|
||||
while (_nextLocalEntityId != start);
|
||||
|
||||
throw new InvalidOperationException("The live entity id namespace is exhausted.");
|
||||
}
|
||||
|
||||
public bool ReleaseLocalId(RuntimeEntityRecord record)
|
||||
{
|
||||
if (record.LocalEntityId is not { } localId)
|
||||
return false;
|
||||
if (_byLocalId.TryGetValue(localId, out RuntimeEntityRecord? retained)
|
||||
&& ReferenceEquals(retained, record))
|
||||
lock (_activeGate)
|
||||
{
|
||||
_byLocalId.Remove(localId);
|
||||
}
|
||||
if (record.LocalEntityId is not { } localId)
|
||||
return false;
|
||||
if (_byLocalId.TryGetValue(localId, out RuntimeEntityRecord? retained)
|
||||
&& ReferenceEquals(retained, record))
|
||||
{
|
||||
_byLocalId.Remove(localId);
|
||||
}
|
||||
|
||||
record.LocalEntityId = null;
|
||||
return true;
|
||||
record.LocalEntityId = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public ulong AdvanceLifetimeMutation(uint serverGuid)
|
||||
|
|
@ -219,15 +257,39 @@ public sealed class RuntimeEntityDirectory
|
|||
|
||||
public bool CompleteSessionClearIfConverged()
|
||||
{
|
||||
if (_activeByGuid.Count != 0 || _teardownByIncarnation.Count != 0)
|
||||
return false;
|
||||
lock (_activeGate)
|
||||
{
|
||||
if (_activeByGuid.Count != 0 || _teardownByIncarnation.Count != 0)
|
||||
return false;
|
||||
|
||||
_byLocalId.Clear();
|
||||
_byLocalId.Clear();
|
||||
}
|
||||
ParentAttachments.Clear();
|
||||
_inbound.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enters the exact active-membership read boundary. Add/remove and local-id
|
||||
/// membership commits serialize behind this allocation-free lease; record
|
||||
/// ownership remains canonical here and no copied gameplay collection is
|
||||
/// introduced.
|
||||
/// </summary>
|
||||
internal ActiveReadLease AcquireActiveRead() => new(_activeGate);
|
||||
|
||||
internal readonly struct ActiveReadLease : IDisposable
|
||||
{
|
||||
private readonly object _gate;
|
||||
|
||||
internal ActiveReadLease(object gate)
|
||||
{
|
||||
_gate = gate;
|
||||
Monitor.Enter(gate);
|
||||
}
|
||||
|
||||
public void Dispose() => Monitor.Exit(_gate);
|
||||
}
|
||||
|
||||
public void RefreshSnapshot(
|
||||
RuntimeEntityRecord record,
|
||||
WorldSession.EntitySpawn accepted,
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ internal sealed class RuntimeEntityObjectViews
|
|||
uint serverGuid,
|
||||
out RuntimeEntitySnapshot entity)
|
||||
{
|
||||
using RuntimeEntityDirectory.ActiveReadLease lease =
|
||||
owner.AcquireActiveRead();
|
||||
if (owner.TryGetActive(
|
||||
serverGuid,
|
||||
out RuntimeEntityRecord record))
|
||||
|
|
@ -106,6 +108,8 @@ internal sealed class RuntimeEntityObjectViews
|
|||
public void Visit(IRuntimeEntityVisitor visitor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(visitor);
|
||||
using RuntimeEntityDirectory.ActiveReadLease lease =
|
||||
owner.AcquireActiveRead();
|
||||
foreach (RuntimeEntityRecord record in owner.ActiveRecords)
|
||||
{
|
||||
RuntimeEntitySnapshot entity = Snapshot(record);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue