Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGalaxySummary.cs
T
Joseph Doherty 9ae6bce0c8 fix(dashboard): copy Galaxy summary volatile fields fresh; memoize only O(N) breakdown
Resolves Server-059, Tests-041, Server-060 (2026-06-25 re-review).

DashboardSnapshotService memoized the whole Galaxy summary keyed on the cache
Sequence, but the shared library bumps Sequence only on a heavy refresh: the
steady-state tick, the refresh-failure path, and the age-based status getter all
replace the entry via 'previous with { ... }' at the SAME Sequence. The dashboard
therefore froze LastQueriedAt and, during a Galaxy SQL outage, kept showing
Healthy with no error for the whole outage.

Split DashboardGalaxySummaryProjector into ComputeBreakdown (the O(N) template/
category work, the only sequence-bound part) and BuildSummary (cheap volatile
fields copied fresh). ResolveGalaxySummary now memoizes only the breakdown by
Sequence and rebuilds the summary from the current entry each tick. Removed the
redundant DashboardGalaxyProjector wrapper.

Tests: same-sequence status/error/timestamp now reflected (the regression);
memoization-hit and sequence-invalidation guards; GatewayApplicationTests asserts
the DI container resolves IGalaxyBrowseScopeProvider to GatewayBrowseScopeProvider
(pins the registration-order invariant over the library's no-op default).
2026-06-25 13:42:39 -04:00

66 lines
2.5 KiB
C#

namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
/// <summary>
/// Snapshot of the Galaxy Repository (ZB) browse state surfaced on the dashboard.
/// Built by <see cref="DashboardGalaxySummaryProjector"/> from the shared-library Galaxy
/// hierarchy cache on each dashboard snapshot; the projector memoizes only the O(N)
/// <see cref="GalaxyObjectBreakdown"/> on the cache sequence while copying the volatile
/// status/timestamp fields fresh, so the dashboard never blocks on SQL.
/// </summary>
public sealed record DashboardGalaxySummary(
DashboardGalaxyStatus Status,
DateTimeOffset? LastQueriedAt,
DateTimeOffset? LastSuccessAt,
DateTimeOffset? LastDeployTime,
string? LastError,
int ObjectCount,
int AreaCount,
int AttributeCount,
int HistorizedAttributeCount,
int AlarmAttributeCount,
IReadOnlyList<DashboardGalaxyTemplateUsage> TopTemplates,
IReadOnlyList<DashboardGalaxyCategoryCount> ObjectCategories)
{
/// <summary>Gets the unknown Galaxy status placeholder.</summary>
public static DashboardGalaxySummary Unknown { get; } = new(
DashboardGalaxyStatus.Unknown,
LastQueriedAt: null,
LastSuccessAt: null,
LastDeployTime: null,
LastError: null,
ObjectCount: 0,
AreaCount: 0,
AttributeCount: 0,
HistorizedAttributeCount: 0,
AlarmAttributeCount: 0,
TopTemplates: Array.Empty<DashboardGalaxyTemplateUsage>(),
ObjectCategories: Array.Empty<DashboardGalaxyCategoryCount>());
}
public enum DashboardGalaxyStatus
{
Unknown = 0,
Healthy = 1,
Stale = 2,
Unavailable = 3,
}
public sealed record DashboardGalaxyTemplateUsage(string TemplateName, int InstanceCount);
public sealed record DashboardGalaxyCategoryCount(int CategoryId, string CategoryName, int ObjectCount);
/// <summary>
/// The O(N)-computed template-usage and category breakdown of a Galaxy hierarchy cache entry —
/// the only part of <see cref="DashboardGalaxySummary"/> that changes with the cache sequence
/// and is therefore safe to memoize on it.
/// </summary>
public sealed record GalaxyObjectBreakdown(
IReadOnlyList<DashboardGalaxyTemplateUsage> TopTemplates,
IReadOnlyList<DashboardGalaxyCategoryCount> ObjectCategories)
{
/// <summary>Gets the empty breakdown (no templates, no categories).</summary>
public static GalaxyObjectBreakdown Empty { get; } = new(
Array.Empty<DashboardGalaxyTemplateUsage>(),
Array.Empty<DashboardGalaxyCategoryCount>());
}