feat: coordinate process-local invalidation - #107
Draft
lan17 wants to merge 1 commit into
Draft
Conversation
lan17
force-pushed
the
feat/local-invalidation-coordination
branch
from
July 29, 2026 16:46
cbb2378 to
ff1b70c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
trackForInvalidationprocess-local entries while keepinginvalidateRemote(keyType, id, futureBufferMs)as the only application invalidation callDialCacheinstance in healthy peer processesCloses #106.
Why
Redis watermarks already protect tracked Redis values, but a process-local hit stops before Redis and cannot observe that watermark. Mutation-sensitive use cases therefore had to choose between process-local performance and accepting the full local TTL as a staleness window.
This adds a best-effort notification path without moving correctness authority away from Redis and without adding a generation lookup to every local hit.
Architecture
There is no generation in
DialCacheKey.urn, no change to the local entry shape, and no Redis or fence-map work on an ordinary local hit.Redis watermark and event protocol
The coordinated path uses a new parallel
INVALIDATE_AND_PUBLISH_CACHE_SCRIPT; the legacy invalidation script and exact integer1reply remain unchanged.The new one-key Lua command:
TIME;Timing values remain canonical decimal strings so JavaScript precision is validated before conversion. Receivers use
effectiveWatermarkMs - redisNowMsonly as a duration and start that duration onperformance.now(); pod wall clocks are never compared with Redis epoch time.The full UTF-8 JSON event is capped at
MAX_REDIS_INVALIDATION_EVENT_BYTES(16 KiB). Oversized events reject beforeSET/PUBLISH. Channel components use strict RFC 3986 encoding so Redis ACL glob metacharacters such as*cannot broaden an exact-channel grant.Local eviction and publication safety
Each participating
DialCachemaintains bounded per-identity monotonic deadlines. Every valid signal installs or extends the fence before scanning the LRU for the exact closed-brace tracked prefix, covering all use cases and argument variants while leaving adjacent ids, other key types/namespaces, and untracked entries untouched.A tracked miss captures the coordinator health epoch. Immediately before the final synchronous LRU
set, publication requires:ready;Fence cardinality is bounded by
localMaxSize. If all slots contain active identities, state collapses into one global deadline through the furthest active fence instead of dropping safety state. There is no timer per identity.Existing process-flight response semantics are unchanged: a caller arriving after invalidation can still join an older exact-key leader and receive its result. The fence controls cache publication, not application-result ordering. Request-local memoization remains the existing outer-request snapshot.
Subscriber health and lifecycle
The backend-neutral coordinator starts unavailable and delivers state/event callbacks synchronously while isolating listener failures. Detected errors, reconnects, connection end, wrong channels, malformed payloads, or invalid custom signals clear tracked local state and suppress tracked publication. A newly acknowledged ready epoch clears once more before allowing new publication.
Protocol-failure epochs prevent a malformed message arriving beside an initial subscribe or resubscribe acknowledgement from being erased by a pending
readycallback. Disposal is terminal, reentrantly idempotent, stops stale fan-out, detaches health listeners synchronously, and performs listener-specific unsubscribe without closing the caller-owned client.The node-redis helper requires a connected, dedicated standalone subscriber and owns only its subscription/listeners. The GLIDE adapter supports the combined publisher script but intentionally does not advertise a first-party subscriber because its available lifecycle surface cannot establish the same health contract.
Compatibility and public surface
RedisConfigremains an interface so existingextends/implementsconsumers compile unchanged.CoordinatedRedisConfig/DialCacheRedisConfigtype the opt-in branch and requireDialCacheCoordinatedRedisClient.invalidateAndPublish.dialcache/redis-protocol.dialcache/node-redisexports the coordinated script registration and subscriber coordinator factory.DialCache.dispose()detaches only that instance; it never drains or closes Redis resources.redis.coordinatorkeeps the v0.11.0 command path, local-hit path, local entry shape, and remote-only invalidation behavior exact.Delivery and failure semantics
Redis Pub/Sub remains at-most-once: there is no replay, peer acknowledgement, sequence check, or all-pod completion barrier. Detected gaps fail closed for tracked process-local storage, but an undetected missed event can leave a stale peer value until TTL. Strict cross-pod read-after-invalidation still requires disabling the local layer or a durable validation/barrier design.
The combined Lua command stores before
PUBLISH. If publication is ACL-denied or the client loses the response after dispatch,invalidateRemote()rejects while the watermark may already have advanced. The origin deliberately keeps its provisional local eviction/fence; callers must treat the outcome as ambiguous and retry according to their mutation protocol.Redis Cluster and operations
The Lua command has one watermark key and preserves the existing hash-tag routing. It uses ordinary Redis 6.2-compatible Pub/Sub, which propagates cluster-wide; sharded Pub/Sub remains out of scope. Plan one dedicated subscriber connection per process and namespace and include cluster-wide Pub/Sub fan-out plus LRU scans in capacity planning.
Safe rollout keeps tracked process-local ramp at zero until every writer uses the combined publisher and every cache-serving process has an acknowledged subscriber. Validate identity sizes against the 16 KiB limit, then ramp local caching gradually. Rollback disables tracked local caching before removing publishers/subscribers.
Performance
The maintained benchmark alternates seven rounds of identical unconfigured/coordinated healthy local hits, then measures single peer scans at 1k/10k/50k occupancy and the origin's normal three-signal path (provisional + returned event + Pub/Sub echo) at 10k.
Final Node 22.22.0 run:
+0.02%elapsed; noise)Every equal-watermark signal still scans because equal timing can represent a separate source mutation. Local invalidation metrics therefore count authoritative event applications, not logical mutations: a healthy origin commonly records the returned event and echo, while a peer commonly records one event.
Validation
Full validation ran on Node.js 22.22.0 at
cbb2378. The history-only rebase producedff1b70cwith the identical source tree;corepack pnpm checkandgit diff --checkwere rerun successfully at the new head:corepack pnpm checkcorepack pnpm test:integrationSCRIPT FLUSH, no-subscriber publication, pre-mutation event rejection, and ACL-deniedPUBLISHafter committedSETcorepack pnpm benchmark:invalidationpassed with semantic assertions and the results abovecorepack pnpm audit --prod: no known vulnerabilitiescorepack pnpm audit: no known vulnerabilitiesgit diff --checkBase and concurrent work
#96 landed on
mainas500d5e7. This branch was rebased directly onto that commit atff1b70c; the source tree is byte-for-byte identical to the previously validated head, and the PR now targetsmainwith one feature commit.#105 is independent but touches overlapping core/docs files, so whichever feature lands second will need a normal rebase/conflict-resolution pass.