Context
Follow-up items from the review of #48 (compact binary cache envelope, closing #37). Neither is a correctness bug — the round-trip and framing are verified correct — but both are judgment calls worth a deliberate decision now that the format is merged.
1. No-TTL entries that fail to deserialize are never evicted
GetAndRefreshAsync (src/NatsDistributedCache/NatsCache.cs) treats a null deserialize result as a cache miss:
var kvEntry = natsResult.Value;
if (kvEntry.Value == null)
{
return null;
}
When the stored bytes are a legacy JSON entry or genuine corruption, Deserialize returns null and we return a miss without deleting the key. Entries carrying a TTL self-clean when NATS expires them, but a key with no expiration that is only ever read (never re-Set) becomes a permanent silent miss and lingers in the bucket forever.
This is the documented migration behavior ("re-populated on next write"), so it self-heals under normal cache-aside usage. Two things to consider:
- Should we proactively
DeleteAsync the key (optimistic-concurrency, using kvEntry.Revision) when a present entry deserializes to null, so dead/legacy no-TTL entries get cleaned up instead of lingering?
- The old JSON path threw on unparseable bytes (loud, logged via the
catch); the new path swallows it silently. If we keep the silent-miss behavior, consider a debug/trace log when a present entry fails to deserialize, to aid diagnosis.
2. Sliding ticks are read without range/sign validation
In CacheEntryBinarySerializer.Deserialize (src/NatsDistributedCache/CacheEntryBinarySerializer.cs), absoluteTicks is bounds-checked before constructing the DateTimeOffset, but slidingTicks is accepted as any long:
if (!reader.TryReadLittleEndian(out long slidingTicks))
{
return null;
}
slidingExpirationTicks = slidingTicks;
A corrupt entry with HasSlidingExpiration set and slidingTicks = long.MaxValue deserializes "successfully" and yields TimeSpan.FromTicks(long.MaxValue) (~10,675 days) as a TTL. No crash today — UpdateEntryExpirationAsync's if (ttl > TimeSpan.Zero) guard prevents anything worse — but it's an asymmetry with the fail-closed handling of absolute ticks. Consider rejecting non-positive / absurd sliding ticks (fail closed to a miss) for consistency.
Acceptance criteria
- Decide + document the intended behavior for a present-but-undeserializable no-TTL entry (evict vs. leave; log vs. silent).
- Sliding-ticks validation is consistent with absolute-ticks handling, with a unit test covering the corrupt-value case.
Source: review of #48.
Context
Follow-up items from the review of #48 (compact binary cache envelope, closing #37). Neither is a correctness bug — the round-trip and framing are verified correct — but both are judgment calls worth a deliberate decision now that the format is merged.
1. No-TTL entries that fail to deserialize are never evicted
GetAndRefreshAsync(src/NatsDistributedCache/NatsCache.cs) treats anulldeserialize result as a cache miss:When the stored bytes are a legacy JSON entry or genuine corruption,
Deserializereturnsnulland we return a miss without deleting the key. Entries carrying a TTL self-clean when NATS expires them, but a key with no expiration that is only ever read (never re-Set) becomes a permanent silent miss and lingers in the bucket forever.This is the documented migration behavior ("re-populated on next write"), so it self-heals under normal cache-aside usage. Two things to consider:
DeleteAsyncthe key (optimistic-concurrency, usingkvEntry.Revision) when a present entry deserializes tonull, so dead/legacy no-TTL entries get cleaned up instead of lingering?catch); the new path swallows it silently. If we keep the silent-miss behavior, consider a debug/trace log when a present entry fails to deserialize, to aid diagnosis.2. Sliding ticks are read without range/sign validation
In
CacheEntryBinarySerializer.Deserialize(src/NatsDistributedCache/CacheEntryBinarySerializer.cs),absoluteTicksis bounds-checked before constructing theDateTimeOffset, butslidingTicksis accepted as anylong:A corrupt entry with
HasSlidingExpirationset andslidingTicks = long.MaxValuedeserializes "successfully" and yieldsTimeSpan.FromTicks(long.MaxValue)(~10,675 days) as a TTL. No crash today —UpdateEntryExpirationAsync'sif (ttl > TimeSpan.Zero)guard prevents anything worse — but it's an asymmetry with the fail-closed handling of absolute ticks. Consider rejecting non-positive / absurd sliding ticks (fail closed to a miss) for consistency.Acceptance criteria
Source: review of #48.