Skip to content

Tag-timestamp memoization + active eviction of tagged entries (blocked: v4 outbound-invalidation hook) #7

Closed

Description

@mgravell

Tagged hits currently re-check N tag timestamps per read (CSC-local GETs — cheap but nonzero). Memoizing them, and actively evicting tagged entries on tag invalidation, both need the per-key outbound invalidation notification on ClientCache — v4 candidate API #2 (now three consumers, incl. a potential FusionCache backplane adapter). Prototype on the v4 branch; blocked on v4.

Activity

  1. mgravell commented on Nov 12, 2026

    @mgravell
    MemberAuthor

    Design update (discussion 2026-11-12), two developments:

    1. StackExchange/StackExchange.Redis#3349 halves this for free. TagTicksHandler returns long (a no-reference struct), so the core auto-enrolls it in the result memo: once that lands, a tag check never re-parses. What remains per tag per read is lookup machinery only — our tag-key assembly (rent+copy), the frame render, and the frame-bytes hash — ~50–100ns vs ~10ns for a direct dictionary hit. The memoization half of this issue drops from "needed" to "measurable optimization"; the wildcard (every read) is where it still shows.

    2. Candidate API #2 shrinks to "a callback at the existing funnel". v4 has no outbound notification today (ClientCache.Invalidate* is inbound), but every incoming invalidation already converges on the internal RespClientCache.OnInvalidate(ReadOnlySpan<byte>) / OnFlush() — RESP3 push dispatch, the keyspace-notification subscription, and the reconnect/tracking-lost paths all funnel there. One span-based callback at that choke point covers every mode uniformly. Requirements for the v4 PR:

    • fire before the key-table lookup (a MaxBytes-evicted key is still server-tracked; its push no-ops in the table but our memo must hear it);
    • deliver flushes (reconnect, tracking downgrade, server flush) as a distinct "everything" signal.

    The SE.Hybrid side then becomes a direct tag → ticks memo — stock's _tagInvalidationTimes, but with its structural flaw (foreign invalidations invisible; invalidation-demo scenario 4) fixed by the hook:

    • prefix pre-filter: every relevant key starts {KeyPrefix}__SER_HCT__, one span StartsWith rejects all entry invalidations;
    • our own RemoveByTag stamps the memo directly at the write (stock-style); the hook is for foreign invalidations;
    • entries stamped with the cache clock, CachePolicy.TimeToLive enforced as max-age — under None/KeyspaceNotifications modes the memo then degrades identically to the client cache rather than becoming unbounded-stale;
    • a global epoch bumped by hook+flush, snapshot-before-read / store-only-if-unchanged, closes the invalidation-raced-the-fill window;
    • re-arming is sound under per-key tracking: push → memo evicted → next read goes through the CSC GET → server re-tracks;
    • lookup keyed on UTF-8 (both hot sides are bytes: AreTagsCurrentAsync walks envelope segments, the hook delivers key spans; strings only on the cold write/probe side) via net9+ alternate lookup — net10-gated like the DISH overloads, downlevel falls back to the CSC path (memo-served after #3349 anyway); the wildcard gets a dedicated field, not a dictionary slot.

    Active eviction of tagged entries stays parked: per-read tag checks keep correctness regardless; eviction would be memory reclamation only, and needs a local tag→keys index.

  2. mgravell commented on Nov 12, 2026

    @mgravell
    MemberAuthor

    The memoization half is implemented at the commit just pushed, riding the per-key removal notification that landed in SE.Redis 4.0.161-alpha (#3350). Shape per the plan posted here: direct tag→ticks memo with UTF-8 alternate lookup (net10-gated; downlevel = client-cache path), epoch-guarded populate, TimeToLive freshness so degraded modes degrade identically, drop-not-stamp on own writes for read-your-own-writes. TagMemoTests asserts own-write immediacy (no polling) and foreign-invalidation convergence. Active eviction stays parked as discussed (needs a tag→keys index; CSC TTL already bounds memory) - shout if you want it tracked separately.

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