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.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,49 @@ public sealed class AbLegacyBitRmwTests
|
||||
Convert.ToInt32(factory.Tags["B3:0"].Value).ShouldBe(0xFF);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="DeviceState.DisposeRuntimes"/> clears
|
||||
/// the lock map so fresh semaphores are created for the new session rather than re-using
|
||||
/// (already-disposed) handles from the old one.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
||||
Reference in New Issue
Block a user