feat(dashboard): alarms page consumes snapshot_status feed frame for the truncation banner
The truncated-snapshot caveat moved from poll-only to push-driven. The page already held an in-process alarm-feed subscription for the provider badge; it now also handles the feed's snapshot_status frame, so a capped provider fetch is caveated when the monitor decides it rather than up to three seconds later. The poll's assignment stays as the reconcile baseline — both sources read the same monitor verdict, and the frame is consumed, never synthesized page-side. StreamAsync primes every subscriber with a snapshot_status frame at open, so a page attaching mid-truncation needs no priming logic of its own; the loop is renamed StatusFeedLoopAsync because it now feeds two indicators, not one.
This commit is contained in:
@@ -191,13 +191,13 @@
|
||||
private Task? _pollTask;
|
||||
|
||||
private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy;
|
||||
private Task? _providerStatusTask;
|
||||
private Task? _statusFeedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_pollTask = PollLoopAsync();
|
||||
_providerStatusTask = ProviderStatusLoopAsync();
|
||||
_statusFeedTask = StatusFeedLoopAsync();
|
||||
}
|
||||
|
||||
private string? ProviderStatusTitle()
|
||||
@@ -210,8 +210,13 @@
|
||||
// The badge tracks the central monitor directly rather than looping back through
|
||||
// /hubs/alarms: the alarm service is an in-process multi-subscriber fan-out, so a
|
||||
// server-rendered page needs no SignalR client, no loopback socket and no auth token.
|
||||
// Alarm rows still come from the 3-second poll below — this loop only feeds the badge.
|
||||
private async Task ProviderStatusLoopAsync()
|
||||
// This loop feeds the two gateway-status indicators — the provider badge and the
|
||||
// truncation banner — from the feed's own status frames, so both move as soon as the
|
||||
// monitor's verdict changes instead of on the next 3-second tick. Alarm rows still come
|
||||
// from the poll below, which also re-asserts the truncation verdict as its reconcile
|
||||
// baseline: both sources read the same monitor verdict, so they cannot disagree for
|
||||
// longer than one tick, and neither one is synthesized here.
|
||||
private async Task StatusFeedLoopAsync()
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
@@ -221,16 +226,30 @@
|
||||
.StreamAsync(alarmFilterPrefix: null, _cts.Token)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
if (message.PayloadCase != AlarmFeedMessage.PayloadOneofCase.ProviderStatus)
|
||||
switch (message.PayloadCase)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
case AlarmFeedMessage.PayloadOneofCase.ProviderStatus:
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
_providerStatus = DashboardAlarmProviderStatus.FromFeed(message);
|
||||
StateHasChanged();
|
||||
}).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
_providerStatus = DashboardAlarmProviderStatus.FromFeed(message);
|
||||
StateHasChanged();
|
||||
}).ConfigureAwait(false);
|
||||
// Every subscriber is primed with this frame at open, so a page that
|
||||
// attaches mid-truncation gets the caveat without waiting for an edge —
|
||||
// no page-side priming needed.
|
||||
case AlarmFeedMessage.PayloadOneofCase.SnapshotStatus:
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
_snapshotTruncated = message.SnapshotStatus.Truncated;
|
||||
StateHasChanged();
|
||||
}).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -241,7 +260,8 @@
|
||||
{
|
||||
// The monitor completes a subscriber's stream when it falls behind, and
|
||||
// again when the monitor restarts. Both are recoverable by resubscribing;
|
||||
// the badge holds its last value in the meantime.
|
||||
// the badge and banner hold their last values in the meantime, and the
|
||||
// resubscribe is primed with the current ones.
|
||||
}
|
||||
|
||||
try
|
||||
@@ -321,7 +341,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
// Fault handling sits inside the loop, matching ProviderStatusLoopAsync: a query or render
|
||||
// Fault handling sits inside the loop, matching StatusFeedLoopAsync: 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
|
||||
@@ -398,6 +418,10 @@
|
||||
{
|
||||
DashboardAlarmQueryResult result = await LiveData.QueryAlarmsAsync(_cts.Token);
|
||||
_queryError = result.Error;
|
||||
// Kept alongside the feed's snapshot_status frame rather than replaced by it: this is
|
||||
// the reconcile baseline. Both read the same monitor verdict, so the poll can only
|
||||
// confirm what the frame already showed — but it also re-establishes the banner for a
|
||||
// page whose feed subscription is mid-resubscribe after the monitor dropped it.
|
||||
_snapshotTruncated = result.SnapshotTruncated;
|
||||
_workerPid = result.WorkerProcessId;
|
||||
_lastRefresh = DateTimeOffset.UtcNow;
|
||||
@@ -416,7 +440,7 @@
|
||||
// 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))
|
||||
await Task.WhenAll(DrainAsync(_pollTask), DrainAsync(_statusFeedTask))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_cts.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user