From 098adf43d0a0d1f31a8a3c9cb59742e10d6611a0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 12:10:42 -0400 Subject: [PATCH] fix(ablegacy): dispose per-parent RMW locks on teardown (review symmetry) DisposeRuntimes() now disposes and clears _rmwLocks, _creationLocks, and _runtimeLocks so ReinitializeAsync/ShutdownAsync cycles don't orphan their SemaphoreSlim instances. Mirrors the TwinCAT _bitRmwLocks fix already shipped. --- .../AbLegacyDriver.cs | 9 ++++ .../AbLegacyBitRmwTests.cs | 43 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs index 430fbedb..0c3e3c3a 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs @@ -928,6 +928,15 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover Runtimes.Clear(); foreach (var r in ParentRuntimes.Values) r.Dispose(); ParentRuntimes.Clear(); + // Dispose + clear the per-parent RMW gates and the per-runtime/creation locks so + // ReinitializeAsync cycles don't orphan their SemaphoreSlim instances (each leaks a + // wait handle once contended). + foreach (var sem in _rmwLocks.Values) sem.Dispose(); + _rmwLocks.Clear(); + foreach (var sem in _creationLocks.Values) sem.Dispose(); + _creationLocks.Clear(); + foreach (var sem in _runtimeLocks.Values) sem.Dispose(); + _runtimeLocks.Clear(); } } } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs index f5a196c3..f9941642 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs @@ -178,6 +178,49 @@ public sealed class AbLegacyBitRmwTests Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(0xFF); } + /// + /// Verifies that disposing (teardown) and re-initializing the driver does not orphan the + /// per-parent RMW-lock semaphores from the previous session. A bit write after the + /// reinit cycle must succeed, proving that clears + /// the lock map so fresh semaphores are created for the new session rather than re-using + /// (already-disposed) handles from the old one. + /// + [Fact] + public async Task RMW_locks_are_disposed_and_fresh_after_driver_reinitialise() + { + var factory = new FakeAbLegacyTagFactory + { + Customise = p => new FakeAbLegacyTag(p) { Value = (short)0 }, + }; + var opts = new AbLegacyDriverOptions + { + Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], + Tags = [new AbLegacyTagDefinition("Bit2", "ab://10.0.0.5/1,0", "N7:0/2", AbLegacyDataType.Bit)], + Probe = new AbLegacyProbeOptions { Enabled = false }, + }; + + // First session: init + bit write — seeds an RMW lock entry for "N7:0". + var drv = new AbLegacyDriver(opts, "drv-1", factory); + await drv.InitializeAsync("{}", CancellationToken.None); + var firstResult = await drv.WriteAsync([new WriteRequest("Bit2", true)], CancellationToken.None); + firstResult.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); + + // Teardown — DisposeRuntimes must dispose + clear the RMW lock map. + await drv.DisposeAsync(); + + // Second session: re-init on a fresh driver instance (mirrors ReinitializeAsync behavior). + factory.Tags.Clear(); + var drv2 = new AbLegacyDriver(opts, "drv-1", factory); + await drv2.InitializeAsync("{}", CancellationToken.None); + var secondResult = await drv2.WriteAsync([new WriteRequest("Bit2", false)], CancellationToken.None); + + // Must succeed — a disposed/orphaned semaphore would throw ObjectDisposedException. + secondResult.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good); + Convert.ToInt32(factory.Tags["N7:0"].Value).ShouldBe(0); + + await drv2.DisposeAsync(); + } + /// /// A non-zero libplctag status returned by the parent-word write is surfaced as a non-Good /// OPC UA StatusCode — the PCCC device rejection propagates to the caller.