feat(abcip): per-runtime operation lock on DeviceState (AbLegacy pattern)

This commit is contained in:
Joseph Doherty
2026-07-13 11:41:03 -04:00
parent 32f248c93d
commit e48609b000
2 changed files with 25 additions and 1 deletions
@@ -1243,6 +1243,24 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
public SemaphoreSlim GetRmwLock(string parentTagName) =>
_rmwLocks.GetOrAdd(parentTagName, _ => new SemaphoreSlim(1, 1));
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, SemaphoreSlim> _runtimeLocks =
new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Per-runtime operation lock (05/STAB-4). A libplctag <c>Tag</c> handle is not safe for
/// concurrent Read/GetStatus/DecodeValue (or Encode/Write/GetStatus) — the server read
/// path, every poll-group loop, and the alarm-projection loop all call
/// <see cref="AbCipDriver.ReadAsync"/> / <see cref="AbCipDriver.WriteAsync"/> against the
/// same cached runtime, and <c>GetStatus</c> returns the <em>last</em> operation's status
/// — so an unserialised overlap decodes a torn / cross-attributed value with Good status.
/// Callers hold this lock around the whole op sequence. Keyed by tag name, which is also
/// the <see cref="Runtimes"/> dictionary key. Mirrors the field-proven AbLegacy pattern.
/// </summary>
/// <param name="tagName">The tag name (the <see cref="Runtimes"/> key).</param>
/// <returns>The per-runtime operation semaphore.</returns>
public SemaphoreSlim GetRuntimeLock(string tagName) =>
_runtimeLocks.GetOrAdd(tagName, _ => new SemaphoreSlim(1, 1));
/// <summary>
/// Compute the effective <see cref="AbCipTagCreateParams"/> for a
/// tag on this device. Combines the per-device options
@@ -1277,6 +1295,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
Runtimes.Clear();
foreach (var r in ParentRuntimes.Values) r.Dispose();
ParentRuntimes.Clear();
// Dispose + clear the per-runtime/RMW gates so ReinitializeAsync cycles don't orphan
// their SemaphoreSlim instances (each leaks a wait handle once contended).
foreach (var sem in _runtimeLocks.Values) sem.Dispose();
_runtimeLocks.Clear();
foreach (var sem in _rmwLocks.Values) sem.Dispose();
_rmwLocks.Clear();
}
}
}