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
{{ message }}
Repository navigation
Tag-timestamp memoization + active eviction of tagged entries (blocked: v4 outbound-invalidation hook) #7
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.
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.
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.
Activity
mgravell commentedon Nov 12, 2026
Design update (discussion 2026-11-12), two developments:
1. StackExchange/StackExchange.Redis#3349 halves this for free.
TagTicksHandlerreturnslong(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 internalRespClientCache.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:MaxBytes-evicted key is still server-tracked; its push no-ops in the table but our memo must hear it);The SE.Hybrid side then becomes a direct
tag → ticksmemo — stock's_tagInvalidationTimes, but with its structural flaw (foreign invalidations invisible; invalidation-demo scenario 4) fixed by the hook:{KeyPrefix}__SER_HCT__, one spanStartsWithrejects all entry invalidations;RemoveByTagstamps the memo directly at the write (stock-style); the hook is for foreign invalidations;CachePolicy.TimeToLiveenforced as max-age — underNone/KeyspaceNotificationsmodes the memo then degrades identically to the client cache rather than becoming unbounded-stale;AreTagsCurrentAsyncwalks 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.
Adopt SE.Redis 4.0.161-alpha: per-read freshness rides WithCachePolicy
mgravell commentedon Nov 12, 2026
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.