fix(modbus): dispose the previous transport and probe loops on re-initialize
This commit is contained in:
@@ -218,6 +218,93 @@ public sealed class ModbusLifecycleHygieneTests
|
||||
"DisposeAsync must dispose the PollGroupEngine so its background Task stops, not just the transport");
|
||||
}
|
||||
|
||||
// -------------------- Re-initialize transport hygiene --------------------
|
||||
|
||||
/// <summary>
|
||||
/// A second <c>InitializeAsync</c> on a live driver — the shape
|
||||
/// <c>DriverInstanceActor.AdoptConfigDuringInit</c> produces when an ApplyDelta lands while the
|
||||
/// driver is Connecting/Reconnecting — must dispose the transport the previous initialize
|
||||
/// published before publishing a new one. Pre-fix <c>_transport</c> was simply overwritten,
|
||||
/// stranding a connected transport no later dispose could reach.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_second_InitializeAsync_disposes_the_first_transport()
|
||||
{
|
||||
var first = new FakeTransport();
|
||||
var second = new FakeTransport();
|
||||
var handed = new List<FakeTransport>();
|
||||
var opts = new ModbusDriverOptions
|
||||
{
|
||||
Host = "fake",
|
||||
Probe = new ModbusProbeOptions { Enabled = false },
|
||||
RawTags = ModbusRawTags.Entries([new ModbusTagDefinition("A", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16)]),
|
||||
};
|
||||
var drv = new ModbusDriver(opts, "modbus-1", _ =>
|
||||
{
|
||||
var next = handed.Count == 0 ? first : second;
|
||||
handed.Add(next);
|
||||
return next;
|
||||
});
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
// No ShutdownAsync in between — exactly what AdoptConfigDuringInit does.
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
handed.Count.ShouldBe(2, "each initialize must build its own transport");
|
||||
first.DisposeCount.ShouldBe(1, "the first initialize's transport must be disposed by the second initialize");
|
||||
second.DisposeCount.ShouldBe(0, "the live transport must not be disposed");
|
||||
|
||||
// …and the driver is still functional, serving reads through the SECOND transport.
|
||||
drv.GetHealth().State.ShouldNotBe(DriverState.Faulted);
|
||||
second.HoldingRegisters[0] = 7;
|
||||
var r = await drv.ReadAsync(["A"], CancellationToken.None);
|
||||
r[0].StatusCode.ShouldBe(0u);
|
||||
Convert.ToInt32(r[0].Value).ShouldBe(7);
|
||||
second.SendCount.ShouldBeGreaterThan(0, "reads must route through the transport the second initialize published");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The probe / re-probe CancellationTokenSources are re-assigned by every initialize, so a
|
||||
/// second initialize must cancel the previous pair or their background loops run forever
|
||||
/// against a transport that is already gone.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_second_InitializeAsync_stops_the_first_probe_and_reprobe_loops()
|
||||
{
|
||||
var first = new FakeTransport();
|
||||
var second = new FakeTransport();
|
||||
var handed = 0;
|
||||
var opts = new ModbusDriverOptions
|
||||
{
|
||||
Host = "fake",
|
||||
Probe = new ModbusProbeOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Interval = TimeSpan.FromMilliseconds(50),
|
||||
Timeout = TimeSpan.FromSeconds(1),
|
||||
},
|
||||
AutoProhibitReprobeInterval = TimeSpan.FromMilliseconds(50),
|
||||
RawTags = ModbusRawTags.Entries([new ModbusTagDefinition("A", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16)]),
|
||||
};
|
||||
var drv = new ModbusDriver(opts, "modbus-1", _ => Interlocked.Increment(ref handed) == 1 ? first : second);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await Task.Delay(200);
|
||||
Interlocked.CompareExchange(ref first.SendCount, 0, 0).ShouldBeGreaterThan(0,
|
||||
"the first initialize's probe loop should have issued at least one send");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
// The first transport's loops must stop; tolerate one in-flight send straddling the cancel.
|
||||
var atReinit = Interlocked.CompareExchange(ref first.SendCount, 0, 0);
|
||||
await Task.Delay(400);
|
||||
var atRest = Interlocked.CompareExchange(ref first.SendCount, 0, 0);
|
||||
(atRest - atReinit).ShouldBeLessThanOrEqualTo(1,
|
||||
"the previous initialize's probe/re-probe loops must be cancelled by the next initialize");
|
||||
|
||||
await drv.DisposeAsync();
|
||||
}
|
||||
|
||||
// -------------------- Finding -005 / -012 (3) --------------------
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user