From e48609b0004b68a74a833ea7731d49c436567f15 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 11:41:03 -0400 Subject: [PATCH] feat(abcip): per-runtime operation lock on DeviceState (AbLegacy pattern) --- ...2-09-driver-fleet-batch-plan.md.tasks.json | 2 +- .../AbCipDriver.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/archreview/plans/R2-09-driver-fleet-batch-plan.md.tasks.json b/archreview/plans/R2-09-driver-fleet-batch-plan.md.tasks.json index fd71d35d..fb0fa1d6 100644 --- a/archreview/plans/R2-09-driver-fleet-batch-plan.md.tasks.json +++ b/archreview/plans/R2-09-driver-fleet-batch-plan.md.tasks.json @@ -34,7 +34,7 @@ { "id": "B2.2", "subject": "B2: port _runtimeLocks + GetRuntimeLock + dispose onto AbCip DeviceState (AbLegacy pattern)", - "status": "pending", + "status": "completed", "blockedBy": [ "B2.1" ] diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs index 9b6a72c8..3a9612a6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs @@ -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 _runtimeLocks = + new(StringComparer.OrdinalIgnoreCase); + + /// + /// Per-runtime operation lock (05/STAB-4). A libplctag Tag 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 + /// / against the + /// same cached runtime, and GetStatus returns the last 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 dictionary key. Mirrors the field-proven AbLegacy pattern. + /// + /// The tag name (the key). + /// The per-runtime operation semaphore. + public SemaphoreSlim GetRuntimeLock(string tagName) => + _runtimeLocks.GetOrAdd(tagName, _ => new SemaphoreSlim(1, 1)); + /// /// Compute the effective 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(); } } }