Skip to content

Binary envelope follow-ups: evict undeserializable entries + validate sliding ticks #49

Description

@matthewdevenny

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions