fix(host): publish the known-id set and its stamp atomically; never write the shared guard options
BoundedApiKeyStore kept the set (volatile) and its DateTimeOffset stamp (non-volatile, 16 bytes) in separate fields, so a reader could pair a new set with an old stamp or read a torn stamp; they are now one immutable snapshot behind a volatile reference. An inner store that cannot list ids now switches the set off with a per-instance flag instead of writing KnownIdSet = false into the shared ApiKeyStoreGuardOptions. Refs #733 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -120,6 +120,36 @@ public sealed class BoundedApiKeyStoreKnownIdTests
|
||||
inner.ListCalls.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix round 1 minor — an inner store that cannot list ids switches the known-id set off for THIS
|
||||
/// decorator only. It used to do so by writing <c>KnownIdSet = false</c> into the guard options it was
|
||||
/// handed, a shared options object, so one store's capability leaked into every other consumer.
|
||||
/// Positive control: a second decorator over a listing store on the same options instance still uses
|
||||
/// the set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task An_unlisting_store_does_not_mutate_the_shared_guard_options()
|
||||
{
|
||||
var guard = new ApiKeyStoreGuardOptions { MissBurst = 100, MissRefillPerSecond = 20 };
|
||||
var options = Options.Create(new ApiKeyOptions
|
||||
{
|
||||
Username = "apikey", Pepper = "unit-test-pepper-xxxx", StoreTimeoutMs = 0, StoreCacheSeconds = 5,
|
||||
});
|
||||
var clock = new FakeClock(T0);
|
||||
var unlisting = new ListingStore("k-real") { SupportsListing = false };
|
||||
var first = new BoundedApiKeyStore(unlisting, options, NullLogger<BoundedApiKeyStore>.Instance, clock, guard);
|
||||
|
||||
(await first.FindAsync("k-guess", T0.UtcDateTime, default)).ShouldBeNull();
|
||||
unlisting.FindCalls.ShouldBe(1, "no set: the per-id path ran");
|
||||
guard.KnownIdSet.ShouldBeTrue("the shared options object is never written");
|
||||
|
||||
var listing = new ListingStore("k-real");
|
||||
var second = new BoundedApiKeyStore(listing, options, NullLogger<BoundedApiKeyStore>.Instance, clock, guard);
|
||||
(await second.FindAsync("k-guess", T0.UtcDateTime, default)).ShouldBeNull();
|
||||
listing.FindCalls.ShouldBe(0, "the second decorator still refuses unknown ids in memory");
|
||||
listing.ListCalls.ShouldBe(1);
|
||||
}
|
||||
|
||||
private static BoundedApiKeyStore Bounded(
|
||||
IApiKeyStore inner, TimeProvider clock, int cacheSeconds, int missBurst = 100, double refillPerSecond = 20) => new(
|
||||
inner,
|
||||
|
||||
Reference in New Issue
Block a user