Merge b626-630 (batch #626+): release prior device/session state on re-Initialize in six drivers

Conflict in S7Driver.ReleaseConnectionAsync resolved: kept the gated Plc swap from #630 and
carried the negotiated-PDU reset from #632 #294 into it.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 14:43:44 -04:00
14 changed files with 563 additions and 50 deletions
@@ -225,6 +225,11 @@ public sealed class S7Driver
/// <inheritdoc />
public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
// #630: InitializeAsync is reachable on a live driver (DriverInstanceActor.AdoptConfigDuringInit
// runs it with no teardown first). Stop the previous probe loop and drop the previous connection
// before they are overwritten below. Deliberately NOT ShutdownAsync: that also drops _poll and the
// subscriptions, which are owned by the shutdown path (the actor re-subscribes after every successful init).
await ReleaseConnectionAsync().ConfigureAwait(false);
Health = new DriverHealth(DriverState.Initializing, null, null);
try
{
@@ -363,7 +368,18 @@ public sealed class S7Driver
// disposing its CTS (drain-before-dispose, STAB-6-S7), so no poll iteration touches _gate
// after this returns.
await _poll.DisposeAsync().ConfigureAwait(false);
await ReleaseConnectionAsync().ConfigureAwait(false);
_writeFault = null;
_connectBackoff.RecordSuccess();
Health = new DriverHealth(DriverState.Unknown, Health.LastSuccessfulRead, null);
}
/// <summary>
/// Cancels + bounded-joins the probe loop, then disposes its CTS and the PLC connection. The
/// connection part of <see cref="ShutdownAsync"/> (everything but the poll engine and health).
/// </summary>
private async Task ReleaseConnectionAsync()
{
var probeCts = _probeCts;
var probeTask = _probeTask;
try { probeCts?.Cancel(); } catch { }
@@ -382,14 +398,27 @@ public sealed class S7Driver
_probeCts = null;
_probeTask = null;
_initialized = false;
_plcDead = false;
_writeFault = null;
_connectBackoff.RecordSuccess();
try { Plc?.Dispose(); } catch { /* best-effort — tearing down anyway */ }
Plc = null;
Volatile.Write(ref _negotiatedPduSize, 0); // a stale post-teardown reading would read as "online"
Health = new DriverHealth(DriverState.Unknown, Health.LastSuccessfulRead, null);
// Swap the connection out under _gate so a Read/Write/poll pass that already holds the gate (they
// re-read Plc inside it) can never see a disposed Plc mid-swap. Bounded: a wedged holder must not hang
// teardown, in which case we proceed exactly as the un-gated path always did.
var gated = false;
try { gated = await _gate.WaitAsync(DrainTimeout).ConfigureAwait(false); } catch { }
if (!gated)
_logger.LogWarning(
"S7 driver {DriverInstanceId}: disposing the S7 connection while an operation still holds the gate (waited {Timeout})",
_driverInstanceId, DrainTimeout);
try
{
_initialized = false;
_plcDead = false;
try { Plc?.Dispose(); } catch { /* best-effort — tearing down anyway */ }
Plc = null;
Volatile.Write(ref _negotiatedPduSize, 0); // a stale post-teardown reading would read as "online"
}
finally
{
if (gated) { try { _gate.Release(); } catch (ObjectDisposedException) { } }
}
}
/// <inheritdoc />