fix(dashboard): alarm poll loop retries through faults and surfaces them; correct stale session-events empty-state copy
This commit is contained in:
@@ -309,27 +309,76 @@
|
||||
};
|
||||
}
|
||||
|
||||
// Fault handling sits inside the loop, matching ProviderStatusLoopAsync: a query or render
|
||||
// fault on one tick is transient (a provider blip, a momentarily unavailable session), so it
|
||||
// is surfaced on the page and retried on the next tick rather than ending polling for the
|
||||
// life of the page. Cancellation is the only exit. The loop method itself therefore cannot
|
||||
// fault, which is what DrainAsync in DisposeAsync relies on.
|
||||
private async Task PollLoopAsync()
|
||||
{
|
||||
if (!await PollOnceAsync().ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using PeriodicTimer timer = new(TimeSpan.FromSeconds(3));
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await timer.WaitForNextTickAsync(_cts.Token).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await PollOnceAsync().ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns false only when cancellation has ended the poll; a non-cancellation fault returns
|
||||
// true so the caller waits for the next tick and tries again.
|
||||
private async Task<bool> PollOnceAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await InvokeAsync(RefreshAlarmsAsync).ConfigureAwait(false);
|
||||
using PeriodicTimer timer = new(TimeSpan.FromSeconds(3));
|
||||
while (await timer.WaitForNextTickAsync(_cts.Token).ConfigureAwait(false))
|
||||
{
|
||||
await InvokeAsync(RefreshAlarmsAsync).ConfigureAwait(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ReportPollFaultAsync(ex).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReportPollFaultAsync(Exception fault)
|
||||
{
|
||||
try
|
||||
{
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
_queryError = fault.Message;
|
||||
StateHasChanged();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
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.
|
||||
// Reporting is best-effort: the fault being reported may itself be the teardown race
|
||||
// this catch-all exists for — an InvokeAsync against a disposed renderer — in which
|
||||
// case the dispatch fails the same way and there is no page left to show it on. The
|
||||
// poll loop keeps ticking either way and exits on the next cancellation check.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,8 +117,9 @@ else
|
||||
@if (_recentEvents.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
Waiting for events. The dashboard mirrors the session's gRPC event stream — events
|
||||
appear here only while a gRPC client is also consuming this session's events.
|
||||
Waiting for events. The dashboard subscribes to this session's events directly, so
|
||||
rows appear as the session's worker emits them while this page is open — no gRPC
|
||||
client has to be consuming the session.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user