perf(grpc): O(1) unconstrained bulk fast path, direct filtered-command build, cache eviction, capacity hints
This commit is contained in:
+49
-3
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user