#49 Address unserializable entries - #54
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses follow-ups from issue #49 by defining and documenting the runtime behavior for present-but-undeserializable cache entries, and by hardening the binary envelope deserializer against corrupt sliding-expiration tick values.
Changes:
- Add bounds validation for sliding-expiration ticks during binary deserialization, with unit tests covering corrupt/truncated cases.
- Treat undeserializable entries as cache misses, emit a Debug log event, and document the intended upgrade/migration behavior.
- Add an integration test covering the “legacy JSON entry reads as miss + debug log” behavior and the “self-heals on next write” path.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/UnitTests/Serialization/CacheEntryBinarySerializerTests.cs | Adds unit coverage for invalid/truncated sliding ticks and validates the accepted upper bound. |
| test/IntegrationTests/Cache/UndeserializableEntryTests.cs | Adds integration coverage for legacy/undeserializable entries being treated as misses and logged. |
| src/NatsDistributedCache/NatsCache.Log.cs | Introduces a new Debug log event for undeserializable entries. |
| src/NatsDistributedCache/NatsCache.cs | Logs and returns a miss when a present entry cannot be deserialized. |
| src/NatsDistributedCache/CacheEntryBinarySerializer.cs | Adds sliding-ticks validation to fail closed on corrupt values. |
| README.md | Documents the cache entry format and upgrade/migration behavior for legacy/corrupt entries. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mtmk
left a comment
There was a problem hiding this comment.
LGTM with optional comments:
Should we accept DateTimeOffset.MaxValue or Timeout.Infinite to mean no TTL. as it is it would throw.
|
Re: accepting I skipped |
Signed-off-by: Matthew DeVenny <matt@codecargo.com>
Signed-off-by: Matthew DeVenny <matt@codecargo.com>
…imit NATS message TTLs are encoded as (int)ttl.TotalSeconds in ToTtlString, which overflows above int.MaxValue seconds (~68 years) and emits an invalid header. Cap both expiration kinds at that limit: - Rename the shared ceiling to MaxTtlTicks (= int.MaxValue seconds) and correct it from DateTimeOffset.MaxValue.Ticks (~10k years), which was well above what the encoding can represent. - GetTtl now rejects an absolute-only expiration whose window exceeds the ceiling (the absolute+sliding minimum is already bounded by sliding). - The deserializer still fails closed on stored sliding ticks above it. Also strengthen the no-eviction integration test with a direct KV-store assertion, and fix an inaccurate magnitude in a test comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matthew DeVenny <matt@codecargo.com>
…d logs
Address review feedback from mtmk:
- Treat DateTimeOffset.MaxValue / TimeSpan.MaxValue expirations as "cache
forever" (no TTL) instead of rejecting them as out-of-range — a common
"never expire" idiom, normalized to no expiration in GetTtl/CreateCacheEntry.
- Include the maximum window in the range-check exception messages.
- Use PascalCase structured log property names ({Key}).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matthew DeVenny <matt@codecargo.com>
3300660 to
efea397
Compare
|
sounds good thanks. I think we can leave |
Closes #49.
Follow-ups from the review of #48 (compact binary cache envelope): define the runtime behavior for present-but-undeserializable entries, harden the deserializer against corrupt sliding ticks, and — surfaced during review of this PR — bound expirations to what the NATS TTL wire format can actually encode.
1. Undeserializable entries → logged cache miss
When a present entry can't be deserialized (a legacy JSON envelope from a pre-binary release, or genuine corruption),
GetAndRefreshAsyncnow logs at Debug (EventId 102 UndeserializableEntry) and returns a cache miss. It deliberately does not evict or throw:IDistributedCacheconsumers.The entry self-heals when the key is next written (
Setoverwrites unconditionally); TTL'd entries are reaped by NATS. Documented in code and in a new README "Cache Entry Format and Upgrades" section.2. Sliding-ticks validation
CacheEntryBinarySerializer.Deserializenow fails closed on non-positive or out-of-range sliding ticks, consistent with the existing absolute-ticks bounds check.3. Expiration ceiling = NATS TTL encoding limit (review-driven)
NATS message TTLs are encoded as
(int)ttl.TotalSeconds(NatsExtensions.ToTtlString), which overflows aboveint.MaxValueseconds (~68 years) and emits an invalid header. A sharedMaxTtlTicksceiling is now enforced end-to-end so any accepted value round-trips through both the serializer and the TTL header:GetTtlthrows for sliding and absolute expirations beyond the ceiling (write path).Deserializefails closed on stored sliding ticks beyond it (read path).This keeps write and read symmetric: nothing that can be
Set()reads back as an undeserializable miss, and no accepted expiration overflows the TTL header.Range-check exceptions state the limit (e.g. "The maximum is 24855.03:14:07."), and the
MaxValuesentinels —DateTimeOffset.MaxValue(absolute) andTimeSpan.MaxValue(sliding/relative) — are treated as "never expire" (no TTL) rather than rejected, matching the common "cache forever" idiom.Tests
MaxValuesentinels normalize to no expiration inGetTtl/CreateCacheEntry.All green after rebasing on
main: 95 unit + 65 integration, 0 build warnings.