perf(grpc): O(1) unconstrained bulk fast path, direct filtered-command build, cache eviction, capacity hints

This commit is contained in:
Joseph Doherty
2026-08-15 12:24:17 -04:00
parent 88d38bb900
commit 7171892984
6 changed files with 261 additions and 33 deletions
@@ -8,10 +8,12 @@ using LibApiKeyIdentity = ZB.MOM.WW.Auth.Abstractions.ApiKeys.ApiKeyIdentity;
namespace ZB.MOM.WW.MxGateway.Tests.Security.Authentication;
/// <summary>
/// Hot-path decorators. Covers both mechanisms: <see cref="CachingApiKeyVerifier"/>
/// (read/verification coalescing plus revoke/rotate invalidation) and
/// Hot-path decorators. Covers all three mechanisms: <see cref="CachingApiKeyVerifier"/>
/// (read/verification coalescing plus revoke/rotate invalidation),
/// <see cref="CoalescingMarkApiKeyStore"/> (the <c>last_used</c> write coalescing that keeps the
/// per-RPC database write off the throughput ceiling).
/// per-RPC database write off the throughput ceiling), and the constraint-blob cache inside
/// <see cref="GatewayApiKeyIdentityMapper"/> (which keeps the per-RPC constraints JSON parse off
/// the authenticated path).
/// </summary>
public sealed class CachingApiKeyVerifierTests
{
@@ -245,6 +247,50 @@ public sealed class CachingApiKeyVerifierTests
Assert.Equal(keyId, CachingApiKeyVerifier.TryParseKeyId($"Bearer mxgw_{keyId}_secret"));
}
/// <summary>
/// The mapper's constraint-blob cache is bounded by eviction, not by a hard stop at the cap:
/// once it is full the oldest entry is dropped so a newly-seen blob is still cached. A cache
/// that merely stopped accepting entries would re-parse every blob beyond the cap on every
/// single RPC, forever. Asserted behaviourally through instance identity — a cached blob maps
/// to the same <see cref="ApiKeyConstraints"/> instance, a re-parsed one does not.
/// </summary>
[Fact]
public void ToGatewayIdentity_ConstraintCacheOverCapacity_EvictsOldestAndKeepsCaching()
{
string firstJson = ConstraintsJson("Area_FifoProbe");
ApiKeyConstraints first = MapConstraints(firstJson);
Assert.Same(first, MapConstraints(firstJson));
// Push strictly more than the cap through the cache after the probe blob, so FIFO eviction
// is guaranteed to have reached it however full the (process-wide) cache already was.
for (int i = 0; i < GatewayApiKeyIdentityMapper.MaxCachedConstraintBlobs + 8; i++)
{
MapConstraints(ConstraintsJson($"Area_FifoFlood_{i}"));
}
Assert.True(
GatewayApiKeyIdentityMapper.CurrentCacheSize <= GatewayApiKeyIdentityMapper.MaxCachedConstraintBlobs,
$"cache grew to {GatewayApiKeyIdentityMapper.CurrentCacheSize} entries, past the {GatewayApiKeyIdentityMapper.MaxCachedConstraintBlobs} cap");
// Evicted, so the probe blob is parsed afresh...
ApiKeyConstraints reparsed = MapConstraints(firstJson);
Assert.NotSame(first, reparsed);
Assert.Equal(first.ReadSubtrees, reparsed.ReadSubtrees);
// ...and re-cached, rather than re-parsed on every later call.
Assert.Same(reparsed, MapConstraints(firstJson));
}
private static ApiKeyConstraints MapConstraints(string constraintsJson) =>
GatewayApiKeyIdentityMapper.ToGatewayIdentity(new LibApiKeyIdentity(
KeyId: "operator01",
DisplayName: "Operator Key",
Scopes: new HashSet<string>(StringComparer.Ordinal),
Constraints: constraintsJson)).EffectiveConstraints;
private static string ConstraintsJson(string readSubtree) =>
ApiKeyConstraintSerializer.Serialize(ApiKeyConstraints.Empty with { ReadSubtrees = [readSubtree] })!;
private static MemoryCache NewCache() => new(new MemoryCacheOptions());
private static ApiKeyVerification Success(string keyId) => new(