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.