fix(drivers): InitializeAsync releases previous device/session state before overwrite (#630)
FOCAS, AbLegacy, AbCip, TwinCAT, S7 and OpcUaClient orphaned the previous probe/recycle/fixed-tree loops and clients when re-initialised on a live driver. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -178,8 +178,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// <param name="driverConfigJson">The driver configuration JSON (see remarks — not re-parsed).</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
// #630: release the previous device states' loops + clients BEFORE they are overwritten (the actor's
|
||||
// AdoptConfigDuringInit calls this without a teardown first). Not ShutdownAsync: that also drops _poll
|
||||
// and subscriptions, which nothing would re-drive.
|
||||
await ReleaseDeviceStatesAsync().ConfigureAwait(false);
|
||||
Volatile.Write(ref _health, new DriverHealth(DriverState.Initializing, null, null));
|
||||
try
|
||||
{
|
||||
@@ -316,7 +320,56 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
Volatile.Write(ref _health, new DriverHealth(DriverState.Faulted, null, ex.Message));
|
||||
throw;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels, bounded-joins and disposes every existing device state (probe / recycle / fixed-tree
|
||||
/// loops + client) and the alarm projection, then clears the table. Device part of
|
||||
/// <see cref="ShutdownAsync"/> only (no poll / subscription teardown).
|
||||
/// </summary>
|
||||
private async Task ReleaseDeviceStatesAsync()
|
||||
{
|
||||
if (_alarmProjection is { } proj)
|
||||
{
|
||||
await proj.DisposeAsync().ConfigureAwait(false);
|
||||
_alarmProjection = null;
|
||||
}
|
||||
if (_devices.Count == 0) return;
|
||||
var loops = new List<Task>();
|
||||
foreach (var state in _devices.Values)
|
||||
{
|
||||
try { state.ProbeCts?.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling probe CTS for {Host} failed", state.Options.HostAddress); }
|
||||
try { state.RecycleCts?.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling recycle CTS for {Host} failed", state.Options.HostAddress); }
|
||||
try { state.FixedTreeCts?.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling fixed-tree CTS for {Host} failed", state.Options.HostAddress); }
|
||||
if (state.ProbeTask is { } pt) loops.Add(pt);
|
||||
if (state.RecycleTask is { } rt) loops.Add(rt);
|
||||
if (state.FixedTreeTask is { } ft) loops.Add(ft);
|
||||
}
|
||||
if (loops.Count > 0)
|
||||
{
|
||||
var all = Task.WhenAll(loops);
|
||||
var finished = await Task.WhenAny(all, Task.Delay(LoopJoinTimeout, CancellationToken.None)).ConfigureAwait(false);
|
||||
if (finished != all)
|
||||
_logger.LogWarning("FOCAS re-initialise: {Count} background loop(s) did not stop within {Timeout}; proceeding",
|
||||
loops.Count, LoopJoinTimeout);
|
||||
}
|
||||
foreach (var state in _devices.Values)
|
||||
{
|
||||
state.ProbeCts?.Dispose();
|
||||
state.ProbeCts = null;
|
||||
state.ProbeTask = null;
|
||||
state.RecycleCts?.Dispose();
|
||||
state.RecycleCts = null;
|
||||
state.RecycleTask = null;
|
||||
state.FixedTreeCts?.Dispose();
|
||||
state.FixedTreeCts = null;
|
||||
state.FixedTreeTask = null;
|
||||
state.DisposeClient();
|
||||
}
|
||||
_devices.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user