You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every hit deserializes; stock's L1 hit returns the cached object instance for immutable types. The biggest remaining per-hit cost. Needs entry identity on cached reads — v4 candidate API #1: an opaque version token stable for the lifetime of the stored entry, to key a deserialized-object memo. Prototype on the v4 branch; blocked on v4.
Assessed StackExchange/StackExchange.Redis#3349 against this issue: it unblocks it, and in a better shape than the "opaque version token" this issue asked for — instead of exposing entry identity for us to key an external memo (side table, lifetime management, leak risk), the memo lives on the cache entry and dies with it (invalidation, expiry, eviction, replacement — push invalidation included), with MaxBytes accounting handled by the core. No external map to keep coherent.
How it fits:
Serve-eligibility is decided before the memo is consulted (TryServeFromCache: cache.TryGet(..., maxAgeTicks, ...) first, memo only replaces the Parse step) — so our WithMaxCacheAge read contexts and entry TTL semantics are untouched.
Reuse is keyed by handler instance (ReferenceEquals), so we need one handler per (T, serializer) — natural, since we already resolve one serializer per T.
The opt-in is IRespResultReuse<TResult> (MayReuse(in T) per-value veto + SizeOf(in T) budget estimate); its docs literally name "the HybridCache immutability conventions" as the intended caller.
What changes on our side (the real work): today's GET handler is PooledBlobHandler : IRespHandler<BufferChunk> — a pooled, ownership-transferring buffer that can never be shared, so as the code stands the PR does nothing for the entry path. We move envelope parse + deserialization into a per-T handler whose TResult is an immutable parsed atom (creation timestamp, expiry, tag region, value). Then:
the atom itself is always shareable (MayReuse true, SizeOf ≈ payload length) → every hit skips the envelope re-parse;
the deserialized T memoizes inside the atom only when the HybridCache immutability rules allow → immutable types get stock's instance-sharing semantics, mutable types deserialize fresh per caller exactly as stock defensively does;
per-read checks stay outside the memo where they belong: envelope expiry re-checked against the atom's timestamps (time-dependent), tag currency via AreTagsCurrentAsync against the atom's retained tag region (the tag keys are themselves locally cached — and as long results they get the core's automatic memo enrollment for free). Key-echo only needs checking on first parse: the memo is per entry, same key.
This also retires the parity divergence "no cross-call immutable instance sharing" noted in docs/parity-plan.md. Scope cuts carried over from the PR: fill-path not memoized (one extra parse per entry lifetime per client — today we pay it on every hit), one slot per entry first-parser-wins (fine: one T per key in HybridCache usage). Handler opt-in works on all TFMs, including net472/ns2.0 (the down-level restriction only limits the core's auto-enrollment).
Out of scope here, still blocked: #7 (needs the outbound-invalidation hook) and #5 (read/store flag split).
Implemented at the commit just pushed, on SE.Redis 4.0.161-alpha's IRespResultReuse. Shape as discussed: immutable parsed atom (CachedEnvelope) memoized by the client cache; envelope expiry/key-echo/tag checks stay per-read; first-wins deserialized-value memo gated on ImmutableTypeCache. ResultMemoTests pins the contract (immutable T: one shared instance; mutable T: fresh instance per call).
Activity
mgravell commentedon Nov 12, 2026
Assessed StackExchange/StackExchange.Redis#3349 against this issue: it unblocks it, and in a better shape than the "opaque version token" this issue asked for — instead of exposing entry identity for us to key an external memo (side table, lifetime management, leak risk), the memo lives on the cache entry and dies with it (invalidation, expiry, eviction, replacement — push invalidation included), with
MaxBytesaccounting handled by the core. No external map to keep coherent.How it fits:
TryServeFromCache:cache.TryGet(..., maxAgeTicks, ...)first, memo only replaces theParsestep) — so ourWithMaxCacheAgeread contexts and entry TTL semantics are untouched.ReferenceEquals), so we need one handler per (T, serializer) — natural, since we already resolve one serializer perT.IRespResultReuse<TResult>(MayReuse(in T)per-value veto +SizeOf(in T)budget estimate); its docs literally name "the HybridCache immutability conventions" as the intended caller.What changes on our side (the real work): today's GET handler is
PooledBlobHandler : IRespHandler<BufferChunk>— a pooled, ownership-transferring buffer that can never be shared, so as the code stands the PR does nothing for the entry path. We move envelope parse + deserialization into a per-Thandler whoseTResultis an immutable parsed atom (creation timestamp, expiry, tag region, value). Then:MayReusetrue,SizeOf≈ payload length) → every hit skips the envelope re-parse;Tmemoizes inside the atom only when the HybridCache immutability rules allow → immutable types get stock's instance-sharing semantics, mutable types deserialize fresh per caller exactly as stock defensively does;AreTagsCurrentAsyncagainst the atom's retained tag region (the tag keys are themselves locally cached — and aslongresults they get the core's automatic memo enrollment for free). Key-echo only needs checking on first parse: the memo is per entry, same key.This also retires the parity divergence "no cross-call immutable instance sharing" noted in docs/parity-plan.md. Scope cuts carried over from the PR: fill-path not memoized (one extra parse per entry lifetime per client — today we pay it on every hit), one slot per entry first-parser-wins (fine: one
Tper key in HybridCache usage). Handler opt-in works on all TFMs, including net472/ns2.0 (the down-level restriction only limits the core's auto-enrollment).Out of scope here, still blocked: #7 (needs the outbound-invalidation hook) and #5 (read/store flag split).
Adopt SE.Redis 4.0.161-alpha: per-read freshness rides WithCachePolicy
mgravell commentedon Nov 12, 2026
Implemented at the commit just pushed, on SE.Redis 4.0.161-alpha's IRespResultReuse. Shape as discussed: immutable parsed atom (CachedEnvelope) memoized by the client cache; envelope expiry/key-echo/tag checks stay per-read; first-wins deserialized-value memo gated on ImmutableTypeCache. ResultMemoTests pins the contract (immutable T: one shared instance; mutable T: fresh instance per call).