perf(ui): shared KPI cache, live-cache-backed alarm summary, coalesced debug renders
This commit is contained in:
@@ -15,8 +15,7 @@
|
||||
@implements IDisposable
|
||||
@inject ICentralHealthAggregator HealthAggregator
|
||||
@inject ISiteRepository SiteRepository
|
||||
@inject CommunicationService CommunicationService
|
||||
@inject IAuditLogQueryService AuditLogQueryService
|
||||
@inject IKpiSnapshotCache KpiSnapshots
|
||||
@inject IKpiHistoryQueryService KpiHistory
|
||||
@inject Microsoft.Extensions.Options.IOptions<HealthMonitoringOptions> HealthOptions
|
||||
|
||||
@@ -640,7 +639,9 @@
|
||||
// Non-fatal — fall back to showing siteId only
|
||||
}
|
||||
|
||||
await RefreshNow();
|
||||
// First load rides whatever the shared KPI cache already has fresh — a second
|
||||
// operator opening the dashboard inside the window costs no extra query.
|
||||
await RefreshAllAsync(forceRefresh: false);
|
||||
|
||||
// Site Health Trends (M6) load on their own path — never from the
|
||||
// timer tick below — so a trend-query fault can't disturb the live tile
|
||||
@@ -655,7 +656,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
await RefreshNow();
|
||||
await RefreshAllAsync(forceRefresh: false);
|
||||
StateHasChanged();
|
||||
}
|
||||
finally
|
||||
@@ -769,20 +770,38 @@
|
||||
? "Central Cluster"
|
||||
: $"{GetSiteName(siteKey)} ({siteKey})";
|
||||
|
||||
private async Task RefreshNow()
|
||||
// Per-tile budget for one refresh tick. The underlying KPI Asks carry the
|
||||
// 30s CommunicationOptions.QueryTimeout, so without this a single hung
|
||||
// singleton stalled the WHOLE tick (and, with a 10s timer, stacked ticks)
|
||||
// rather than degrading its own tile. Cancelling here abandons only this
|
||||
// circuit's wait — the shared KPI flight continues and its result lands in
|
||||
// the process-level cache for the next tick.
|
||||
private static readonly TimeSpan TileTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
// The operator's explicit "Refresh Now" bypasses the shared cache's freshness
|
||||
// window; the timer tick and the first load ride whatever is already fresh
|
||||
// (which is the whole point — N dashboards share one query per window).
|
||||
private Task RefreshNow() => RefreshAllAsync(forceRefresh: true);
|
||||
|
||||
// Every tile loads concurrently, each with its own short budget, so one slow
|
||||
// KPI degrades one tile instead of the tick. Every loader swallows its own
|
||||
// faults, so Task.WhenAll here can never observe an exception.
|
||||
private async Task RefreshAllAsync(bool forceRefresh)
|
||||
{
|
||||
_siteStates = HealthAggregator.GetAllSiteStates();
|
||||
await LoadOutboxKpis();
|
||||
await Task.WhenAll(LoadSiteCallKpis(), LoadSiteCallNodeKpis());
|
||||
await LoadAuditKpis();
|
||||
await Task.WhenAll(
|
||||
LoadOutboxKpis(forceRefresh),
|
||||
LoadSiteCallKpis(forceRefresh),
|
||||
LoadSiteCallNodeKpis(forceRefresh),
|
||||
LoadAuditKpis(forceRefresh));
|
||||
}
|
||||
|
||||
private async Task LoadOutboxKpis()
|
||||
private async Task LoadOutboxKpis(bool forceRefresh)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await CommunicationService.GetNotificationKpisAsync(
|
||||
new NotificationKpiRequest(Guid.NewGuid().ToString("N")));
|
||||
using var timeout = new CancellationTokenSource(TileTimeout);
|
||||
var response = await KpiSnapshots.GetNotificationKpisAsync(forceRefresh, timeout.Token);
|
||||
if (response.Success)
|
||||
{
|
||||
_outboxKpi = response;
|
||||
@@ -795,6 +814,11 @@
|
||||
_outboxKpiError = response.ErrorMessage ?? "KPI query failed.";
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_outboxKpiAvailable = false;
|
||||
_outboxKpiError = "KPI query timed out.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_outboxKpiAvailable = false;
|
||||
@@ -807,12 +831,12 @@
|
||||
// killing the dashboard. Mirrors LoadOutboxKpis's error handling shape — a
|
||||
// response with Success == false (repository fault) and an Ask that threw
|
||||
// (transport fault) both collapse to "unavailable".
|
||||
private async Task LoadSiteCallKpis()
|
||||
private async Task LoadSiteCallKpis(bool forceRefresh)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await CommunicationService.GetSiteCallKpisAsync(
|
||||
new SiteCallKpiRequest(Guid.NewGuid().ToString("N")));
|
||||
using var timeout = new CancellationTokenSource(TileTimeout);
|
||||
var response = await KpiSnapshots.GetSiteCallKpisAsync(forceRefresh, timeout.Token);
|
||||
if (response.Success)
|
||||
{
|
||||
_siteCallKpi = response;
|
||||
@@ -825,6 +849,11 @@
|
||||
_siteCallKpiError = response.ErrorMessage ?? "KPI query failed.";
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_siteCallKpiAvailable = false;
|
||||
_siteCallKpiError = "KPI query timed out.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_siteCallKpiAvailable = false;
|
||||
@@ -834,12 +863,12 @@
|
||||
|
||||
// Per-node site-call KPI loader (M5.2). Best-effort; a fault silently
|
||||
// suppresses the per-node sub-table rather than degrading the dashboard.
|
||||
private async Task LoadSiteCallNodeKpis()
|
||||
private async Task LoadSiteCallNodeKpis(bool forceRefresh)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await CommunicationService.GetPerNodeSiteCallKpisAsync(
|
||||
new PerNodeSiteCallKpiRequest(Guid.NewGuid().ToString("N")));
|
||||
using var timeout = new CancellationTokenSource(TileTimeout);
|
||||
var response = await KpiSnapshots.GetPerNodeSiteCallKpisAsync(forceRefresh, timeout.Token);
|
||||
if (response.Success)
|
||||
{
|
||||
_siteCallNodeKpis = response.Nodes;
|
||||
@@ -864,14 +893,20 @@
|
||||
// Audit KPI loader: wraps the service call so a transient DB outage degrades
|
||||
// the three tiles to em dashes with an inline error rather than killing the
|
||||
// dashboard. Mirrors LoadOutboxKpis's error handling shape.
|
||||
private async Task LoadAuditKpis()
|
||||
private async Task LoadAuditKpis(bool forceRefresh)
|
||||
{
|
||||
try
|
||||
{
|
||||
_auditKpi = await AuditLogQueryService.GetKpiSnapshotAsync();
|
||||
using var timeout = new CancellationTokenSource(TileTimeout);
|
||||
_auditKpi = await KpiSnapshots.GetAuditKpiSnapshotAsync(forceRefresh, timeout.Token);
|
||||
_auditKpiAvailable = true;
|
||||
_auditKpiError = null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_auditKpiAvailable = false;
|
||||
_auditKpiError = "KPI query timed out.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_auditKpiAvailable = false;
|
||||
|
||||
Reference in New Issue
Block a user