fix(adminui): /hosts timer dispose-race hardening + IAsyncDisposable parity with DriverStatusPanel (review)
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
@inject IDriverStatusSnapshotStore DriverStore
|
@inject IDriverStatusSnapshotStore DriverStore
|
||||||
@inject IDbContextFactory<OtOpcUaConfigDbContext> DbFactory
|
@inject IDbContextFactory<OtOpcUaConfigDbContext> DbFactory
|
||||||
@inject Microsoft.Extensions.Logging.ILogger<Hosts> Logger
|
@inject Microsoft.Extensions.Logging.ILogger<Hosts> Logger
|
||||||
@implements IDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h4 class="mb-0">Cluster hosts</h4>
|
<h4 class="mb-0">Cluster hosts</h4>
|
||||||
@@ -226,12 +226,20 @@ else
|
|||||||
await LoadConfigAsync();
|
await LoadConfigAsync();
|
||||||
RebuildDriverGroups();
|
RebuildDriverGroups();
|
||||||
DriverStore.SnapshotChanged += OnSnapshotChanged;
|
DriverStore.SnapshotChanged += OnSnapshotChanged;
|
||||||
_timer = new Timer(_ => InvokeAsync(async () =>
|
_timer = new Timer(_ => _ = InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
Refresh();
|
try
|
||||||
await LoadConfigAsync();
|
{
|
||||||
RebuildDriverGroups();
|
Refresh();
|
||||||
StateHasChanged();
|
await LoadConfigAsync();
|
||||||
|
RebuildDriverGroups();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Circuit disposed while a tick was in flight — ignore (the discarded task would
|
||||||
|
// otherwise swallow this silently). Mirrors DriverStatusPanel's drain-on-dispose.
|
||||||
|
}
|
||||||
}), null,
|
}), null,
|
||||||
TimeSpan.FromSeconds(RefreshIntervalSeconds),
|
TimeSpan.FromSeconds(RefreshIntervalSeconds),
|
||||||
TimeSpan.FromSeconds(RefreshIntervalSeconds));
|
TimeSpan.FromSeconds(RefreshIntervalSeconds));
|
||||||
@@ -337,10 +345,13 @@ else
|
|||||||
_ => "chip-idle",
|
_ => "chip-idle",
|
||||||
};
|
};
|
||||||
|
|
||||||
public void Dispose()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
_timer?.Dispose();
|
// Unsubscribe first so the singleton store can't invoke a handler on a disposed component.
|
||||||
DriverStore.SnapshotChanged -= OnSnapshotChanged;
|
DriverStore.SnapshotChanged -= OnSnapshotChanged;
|
||||||
|
// Drain the timer so an in-flight callback can't touch a component that's already gone
|
||||||
|
// (System.Threading.Timer's async dispose awaits any in-flight callback — .NET 6+).
|
||||||
|
if (_timer is not null) await _timer.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record MemberRow(
|
private sealed record MemberRow(
|
||||||
|
|||||||
Reference in New Issue
Block a user