fix(drivers): join FOCAS and AbLegacy background loops on shutdown (#602 STAB-23/6)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -277,7 +277,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
{
|
||||
state.ProbeCts = new CancellationTokenSource();
|
||||
var ct = state.ProbeCts.Token;
|
||||
_ = Task.Run(() => ProbeLoopAsync(state, ct), ct);
|
||||
state.ProbeTask = Task.Run(() => ProbeLoopAsync(state, ct), ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
{
|
||||
state.RecycleCts = new CancellationTokenSource();
|
||||
var ct = state.RecycleCts.Token;
|
||||
_ = Task.Run(() => RecycleLoopAsync(state, ct), ct);
|
||||
state.RecycleTask = Task.Run(() => RecycleLoopAsync(state, ct), ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
{
|
||||
state.FixedTreeCts = new CancellationTokenSource();
|
||||
var ct = state.FixedTreeCts.Token;
|
||||
_ = Task.Run(() => FixedTreeLoopAsync(state, ct), ct);
|
||||
state.FixedTreeTask = Task.Run(() => FixedTreeLoopAsync(state, ct), ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +326,9 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Upper bound on how long shutdown waits for the background loops to observe cancellation.</summary>
|
||||
internal static readonly TimeSpan LoopJoinTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -335,22 +338,43 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
await proj.DisposeAsync().ConfigureAwait(false);
|
||||
_alarmProjection = null;
|
||||
}
|
||||
// STAB-23: cancel every loop first, JOIN them (bounded), and only then dispose the CTSs and the
|
||||
// client. Fire-and-forget loops used to outlive shutdown: one parked in EnsureConnectedAsync could
|
||||
// finish its connect AFTER DisposeClient and leave a live socket on a shut-down driver.
|
||||
var loops = new List<Task>();
|
||||
foreach (var state in _devices.Values)
|
||||
{
|
||||
// Cancel-then-dispose can race in tight shutdown loops; swallowing is intentional
|
||||
// but we now log the cause so a noisy shutdown leaves a Debug trace.
|
||||
try { state.ProbeCts?.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling probe CTS for {Host} failed", state.Options.HostAddress); }
|
||||
state.ProbeCts?.Dispose();
|
||||
state.ProbeCts = null;
|
||||
try { state.RecycleCts?.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling recycle CTS for {Host} failed", state.Options.HostAddress); }
|
||||
state.RecycleCts?.Dispose();
|
||||
state.RecycleCts = null;
|
||||
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 shutdown: {Count} background loop(s) did not stop within {Timeout}; proceeding with teardown",
|
||||
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();
|
||||
@@ -1575,6 +1599,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
public CancellationTokenSource? RecycleCts { get; set; }
|
||||
/// <summary>Gets or sets the cancellation token source for the fixed-tree loop.</summary>
|
||||
public CancellationTokenSource? FixedTreeCts { get; set; }
|
||||
/// <summary>The probe loop task, retained so shutdown can join it (STAB-23).</summary>
|
||||
public Task? ProbeTask { get; set; }
|
||||
/// <summary>The recycle loop task, retained so shutdown can join it (STAB-23).</summary>
|
||||
public Task? RecycleTask { get; set; }
|
||||
/// <summary>The fixed-tree loop task, retained so shutdown can join it (STAB-23).</summary>
|
||||
public Task? FixedTreeTask { get; set; }
|
||||
/// <summary>Gets or sets the fixed-tree cache for this device.</summary>
|
||||
public FocasFixedTreeCache? FixedTreeCache { get; set; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user