fix: release the alarm drain latch on escape; cancel before disposing the page poll timers
This commit is contained in:
@@ -114,6 +114,10 @@ else
|
||||
// goes away aborts its query instead of running it to completion against a component that's gone.
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
// Set first thing in DisposeAsync. A tick that resumes after teardown must not call
|
||||
// StateHasChanged — the renderer is gone and it throws, inside a task nobody awaits.
|
||||
private volatile bool _disposed;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadAsync();
|
||||
@@ -170,7 +174,10 @@ else
|
||||
// Release first: a StateHasChanged on a torn-down circuit must never leave the guard set.
|
||||
Volatile.Write(ref _tickRunning, 0);
|
||||
_refreshing = false;
|
||||
StateHasChanged();
|
||||
// This one sits OUTSIDE the try, so it has no catch to fall into — and a render
|
||||
// request against a disposed renderer throws. Skip it once the page is gone; there
|
||||
// is nothing left to render to.
|
||||
if (!_disposed) StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,13 +193,20 @@ else
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_disposed = true;
|
||||
// Cancel before draining so a tick already in flight aborts its queries rather than running
|
||||
// them to completion against a component that's gone.
|
||||
_cts.Cancel();
|
||||
// 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();
|
||||
_cts.Dispose();
|
||||
// ...but the callback body is `_ = InvokeAsync(LoadAsync)`, so that await covered only the
|
||||
// SCHEDULING of the tick, never the tick itself: a query can still be running against
|
||||
// _cts.Token right now. Disposing the source out from under it turns the clean
|
||||
// cancellation the line above arranged into an ObjectDisposedException mid-query, so skip
|
||||
// the dispose while a tick holds it. An undisposed CancellationTokenSource with no timer
|
||||
// and no materialised WaitHandle costs nothing and is collected with the component.
|
||||
if (Volatile.Read(ref _tickRunning) == 0) _cts.Dispose();
|
||||
}
|
||||
|
||||
private sealed record NodeRow(
|
||||
|
||||
@@ -337,6 +337,10 @@ else
|
||||
// goes away aborts its queries instead of running them against a component that's gone.
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
// Set first thing in DisposeAsync. A tick that resumes after teardown must not call
|
||||
// StateHasChanged — the renderer is gone and it throws, inside a task nobody awaits.
|
||||
private volatile bool _disposed;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Refresh();
|
||||
@@ -390,7 +394,10 @@ else
|
||||
// Release first: a StateHasChanged on a torn-down circuit must never leave the guard set.
|
||||
Volatile.Write(ref _tickRunning, 0);
|
||||
_refreshing = false;
|
||||
StateHasChanged();
|
||||
// This one sits OUTSIDE the try, so it has no catch to fall into — and a render
|
||||
// request against a disposed renderer throws. Skip it once the page is gone; there
|
||||
// is nothing left to render to.
|
||||
if (!_disposed) StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,10 +416,15 @@ else
|
||||
.Select(d => new HostsDriverInstanceInfo(d.DriverInstanceId, d.ClusterId, d.Name, d.DriverType, d.Enabled))
|
||||
.ToListAsync(_cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException)
|
||||
{
|
||||
// The circuit went away mid-load. Rethrow so the caller treats it as a clean exit — it
|
||||
// must NOT fall into the swallow-and-clear below, which would blank the cached config.
|
||||
// ObjectDisposedException belongs here for the same reason: teardown disposes _cts (and
|
||||
// with it the DbContext's token registration) while a query may still be running, and
|
||||
// that shape reaches this method as ODE rather than OCE. Sending it to the generic catch
|
||||
// blanked _nodes/_instances on the way out — the exact swallow-and-clear this rethrow
|
||||
// exists to prevent, and permanent, because a torn-down page never ticks again.
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -513,6 +525,7 @@ else
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_disposed = true;
|
||||
// Unsubscribe first so the singleton store can't invoke a handler on a disposed component.
|
||||
DriverStore.SnapshotChanged -= OnSnapshotChanged;
|
||||
// Cancel before draining so a tick already in flight aborts its queries rather than running
|
||||
@@ -521,7 +534,13 @@ else
|
||||
// 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();
|
||||
_cts.Dispose();
|
||||
// ...but the callback body is `_ = InvokeAsync(TickAsync)`, so that await covered only the
|
||||
// SCHEDULING of the tick, never the tick itself: LoadConfigAsync can still be running
|
||||
// against _cts.Token right now. Disposing the source out from under it turns the clean
|
||||
// cancellation the line above arranged into an ObjectDisposedException mid-query, so skip
|
||||
// the dispose while a tick holds it. An undisposed CancellationTokenSource with no timer
|
||||
// and no materialised WaitHandle costs nothing and is collected with the component.
|
||||
if (Volatile.Read(ref _tickRunning) == 0) _cts.Dispose();
|
||||
}
|
||||
|
||||
private sealed record MemberRow(
|
||||
|
||||
Reference in New Issue
Block a user