Skip to content

Deserialized-object memo / immutable instance sharing (blocked: v4 entry-identity token) #6

Closed

Description

@mgravell

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.

Activity

  1. mgravell commented on Nov 12, 2026

    @mgravell
    MemberAuthor

    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).

  2. mgravell commented on Nov 12, 2026

    @mgravell
    MemberAuthor

    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).

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions