fix(ablegacy): prune per-key gates when an indirect index moves
Review 07 L7: each index value an indirect tag (N7:[N7:0]) swept through left a per-runtime and a creation SemaphoreSlim in the device's gate tables until shutdown. A moved index now drops the stale resolution's gates along with its handle, and sweeps any other stale resolution of the same tag a concurrent reader cached, so the tables stay bounded by the authored set. Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -965,8 +965,8 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
var concreteText = concrete.ToLibplctagName();
|
||||
var resolvedName = $"{def.Name}=>{concreteText}";
|
||||
|
||||
// The index moved: drop the previous resolution's handle so a sweeping index does not accumulate one
|
||||
// libplctag handle per value it ever took.
|
||||
// The index moved: drop the previous resolution's handle AND its per-key gates (review 07 L7), so a sweeping
|
||||
// index does not accumulate one libplctag handle plus two SemaphoreSlims per value it ever took.
|
||||
var previous = device.IndirectLastName.TryGetValue(def.Name, out var prior) ? prior : null;
|
||||
device.IndirectLastName[def.Name] = resolvedName;
|
||||
if (previous is not null && !string.Equals(previous, resolvedName, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -974,6 +974,17 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
var oldLock = device.GetRuntimeLock(previous);
|
||||
await oldLock.WaitAsync(ct).ConfigureAwait(false);
|
||||
try { EvictRuntime(device, previous); } finally { oldLock.Release(); }
|
||||
device.ForgetGates(previous);
|
||||
// A concurrent reader of the same indirect tag can have cached a resolution between our TryGetValue and
|
||||
// the write above (a stale one nothing would ever evict). Sweep every OTHER resolution of this tag.
|
||||
var prefix = def.Name + "=>";
|
||||
foreach (var key in device.Runtimes.Keys)
|
||||
{
|
||||
if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(key, resolvedName, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
EvictRuntime(device, key);
|
||||
device.ForgetGates(key);
|
||||
}
|
||||
}
|
||||
return new IndirectResolution(def with { Name = resolvedName, Address = concreteText }, 0, false);
|
||||
}
|
||||
@@ -1379,6 +1390,21 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
/// <summary>Tags already warned about for a malformed address (once-per-tag log).</summary>
|
||||
public System.Collections.Concurrent.ConcurrentDictionary<string, byte> MalformedAddressLogged { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Drops the per-runtime and creation gates of a runtime key that is no longer in use (a moved indirect
|
||||
/// resolution). The gates are removed, not disposed: a caller that already holds a reference finishes on
|
||||
/// it, and the next user of the key simply gets a fresh gate.
|
||||
/// </summary>
|
||||
/// <param name="runtimeKey">The runtime key whose gates to drop.</param>
|
||||
public void ForgetGates(string runtimeKey)
|
||||
{
|
||||
_runtimeLocks.TryRemove(runtimeKey, out _);
|
||||
_creationLocks.TryRemove($"tag:{runtimeKey}", out _);
|
||||
}
|
||||
|
||||
/// <summary>The number of per-key gates currently held (runtime + creation + RMW). Tests only.</summary>
|
||||
internal int GateCount => _runtimeLocks.Count + _creationLocks.Count + _rmwLocks.Count;
|
||||
|
||||
private volatile bool _retired;
|
||||
|
||||
/// <summary>True once this state has been unpublished and released; nothing may be cached on it any more.</summary>
|
||||
|
||||
Reference in New Issue
Block a user