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 ILogger<DashboardSnapshotService> _logger;
|
||||||
private readonly SemaphoreSlim _apiKeySummaryRefreshGate = new(1, 1);
|
private readonly SemaphoreSlim _apiKeySummaryRefreshGate = new(1, 1);
|
||||||
private IReadOnlyList<DashboardApiKeySummary> _apiKeySummaries = Array.Empty<DashboardApiKeySummary>();
|
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>
|
/// <summary>Initializes a new instance of the DashboardSnapshotService class.</summary>
|
||||||
/// <param name="sessionRegistry">Registry of active gateway sessions.</param>
|
/// <param name="sessionRegistry">Registry of active gateway sessions.</param>
|
||||||
@@ -97,9 +103,30 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
|||||||
Faults: CreateFaultSummaries(sessions, generatedAt),
|
Faults: CreateFaultSummaries(sessions, generatedAt),
|
||||||
ApiKeys: Volatile.Read(ref _apiKeySummaries),
|
ApiKeys: Volatile.Read(ref _apiKeySummaries),
|
||||||
Configuration: _configurationProvider.GetEffectiveConfiguration(),
|
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>
|
/// <summary>
|
||||||
/// Watches dashboard snapshots at regular intervals asynchronously.
|
/// Watches dashboard snapshots at regular intervals asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ public sealed class GatewayBrowseScopeProvider(IGatewayRequestIdentityAccessor i
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context)
|
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;
|
ApiKeyConstraints constraints = identityAccessor.Current?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
|
||||||
return constraints.BrowseSubtrees;
|
return constraints.BrowseSubtrees;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user