perf(dashboard): memoize Galaxy summary by cache sequence; document scope-provider identity invariant
This commit is contained in:
@@ -30,6 +30,12 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
||||
private readonly ILogger<DashboardSnapshotService> _logger;
|
||||
private readonly SemaphoreSlim _apiKeySummaryRefreshGate = new(1, 1);
|
||||
private IReadOnlyList<DashboardApiKeySummary> _apiKeySummaries = Array.Empty<DashboardApiKeySummary>();
|
||||
// Memoizes the projected Galaxy summary against the immutable cache sequence. The shared
|
||||
// library bumps Sequence only on a heavy refresh, so an unchanged sequence means the entry
|
||||
// (and therefore its summary) is unchanged and the O(N) projection can be reused. This keeps
|
||||
// the ~1s snapshot tick O(1) for Galaxy, restoring the pre-adoption behavior where the
|
||||
// summary was computed once per refresh rather than once per tick.
|
||||
private GalaxySummaryCache? _galaxySummaryCache;
|
||||
|
||||
/// <summary>Initializes a new instance of the DashboardSnapshotService class.</summary>
|
||||
/// <param name="sessionRegistry">Registry of active gateway sessions.</param>
|
||||
@@ -97,9 +103,30 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
||||
Faults: CreateFaultSummaries(sessions, generatedAt),
|
||||
ApiKeys: Volatile.Read(ref _apiKeySummaries),
|
||||
Configuration: _configurationProvider.GetEffectiveConfiguration(),
|
||||
Galaxy: DashboardGalaxyProjector.Project(_galaxyHierarchyCache.Current));
|
||||
Galaxy: ResolveGalaxySummary());
|
||||
}
|
||||
|
||||
private DashboardGalaxySummary ResolveGalaxySummary()
|
||||
{
|
||||
GalaxyHierarchyCacheEntry entry = _galaxyHierarchyCache.Current;
|
||||
long sequence = entry.Sequence;
|
||||
|
||||
// Lock-free reuse: a matching sequence means the entry is unchanged, so the previously
|
||||
// projected summary is still correct. A racing recompute for a new sequence is harmless —
|
||||
// the projection is pure, so any winner stores identical content for that sequence.
|
||||
GalaxySummaryCache? cached = Volatile.Read(ref _galaxySummaryCache);
|
||||
if (cached is not null && cached.Sequence == sequence)
|
||||
{
|
||||
return cached.Summary;
|
||||
}
|
||||
|
||||
DashboardGalaxySummary summary = DashboardGalaxyProjector.Project(entry);
|
||||
Volatile.Write(ref _galaxySummaryCache, new GalaxySummaryCache(sequence, summary));
|
||||
return summary;
|
||||
}
|
||||
|
||||
private sealed record GalaxySummaryCache(long Sequence, DashboardGalaxySummary Summary);
|
||||
|
||||
/// <summary>
|
||||
/// Watches dashboard snapshots at regular intervals asynchronously.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,6 +11,13 @@ public sealed class GatewayBrowseScopeProvider(IGatewayRequestIdentityAccessor i
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context)
|
||||
{
|
||||
// Invariant: the caller identity is the ambient one pushed by
|
||||
// GatewayGrpcAuthorizationInterceptor earlier in this same call. That interceptor is
|
||||
// registered globally and therefore runs before the library's galaxy service, so
|
||||
// Current is populated for any authenticated galaxy RPC. If it is somehow absent we
|
||||
// return empty BrowseSubtrees, which the library treats as "no scoping" (full
|
||||
// hierarchy) — safe only because the global interceptor has already authenticated and
|
||||
// authorized the request first. Do not reorder the interceptor without revisiting this.
|
||||
ApiKeyConstraints constraints = identityAccessor.Current?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
|
||||
return constraints.BrowseSubtrees;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user