fix(dashboard): parallel bounded alarm drains, best-effort disposal catch-alls, ConfigureAwait alignment

This commit is contained in:
Joseph Doherty
2026-08-16 04:20:59 -04:00
parent 3faa272db9
commit 7cfb2e727d
2 changed files with 23 additions and 6 deletions
+4 -2
View File
@@ -246,9 +246,11 @@ the pill therefore reports is the case it exists for — the channel completing
a page that is still watching.
`AlarmsPage` owns two loops of its own (the 3 s alarm poll and the provider-status
badge) and bounds each drain at 5 seconds on dispose, for the same reason
badge) and bounds their drain at 5 seconds on dispose, for the same reason
`DashboardPageBase` bounds its watch drain: both loops render through the renderer's
dispatcher, and disposal can run on it.
dispatcher, and disposal can run on it. The two are drained concurrently, so the
bound on disposal is 5 seconds in total rather than per loop — a wedged dispatcher
blocks both loops at once, and draining them in sequence would time out twice.
### SignalR hubs (remote clients)
@@ -323,6 +323,14 @@
catch (OperationCanceledException)
{
}
catch
{
// Catch-all for the same reason ProviderStatusLoopAsync has one: a teardown race
// can fault InvokeAsync (a disposed renderer) after cancellation has already been
// requested. Letting that fault the task would surface it out of the drain in
// DisposeAsync, skipping _cts.Dispose(). The loop ends here and the page holds its
// last rendered rows — it is being disposed or has nothing left to poll with.
}
}
private async Task RefreshAlarmsAsync()
@@ -341,10 +349,13 @@
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
await _cts.CancelAsync().ConfigureAwait(false);
await DrainAsync(_pollTask);
await DrainAsync(_providerStatusTask);
// Drained together, not one after the other: the wedged dispatcher this bound exists
// for blocks both loops at once, so sequential drains would time out twice and make
// the real bound 10 seconds. DrainAsync tolerates a null task.
await Task.WhenAll(DrainAsync(_pollTask), DrainAsync(_providerStatusTask))
.ConfigureAwait(false);
_cts.Dispose();
GC.SuppressFinalize(this);
@@ -362,7 +373,7 @@
try
{
await loop.WaitAsync(LoopDrainTimeout);
await loop.WaitAsync(LoopDrainTimeout).ConfigureAwait(false);
}
catch (TimeoutException)
{
@@ -370,5 +381,9 @@
catch (OperationCanceledException)
{
}
catch
{
// Other disposal-time errors are best-effort.
}
}
}