Repository navigation
fix(provider-redis): cache anything at all on a Redis Cluster - #244
Merged
Merged
Conversation
@keyv/redis 3 writes an entry and adds it to the tracking set in one MULTI, and deletes them the same way. The entry and the set are two keys in different hash slots, which a cluster refuses: All keys in the pipeline should belong to the same slots allocation group Keyv catches that and only emits it as an event, so set() resolved, nothing was stored, and every request on a cluster was a miss. del() failed the same way. The #100 work made purges slot-safe, but it was checked with a recording client, which cannot see a failure inside a dependency. On a cluster the store now issues the same two commands separately: the same keys, the same PX lifetime, the same tracking set. The entry is written before it is tracked and untracked after it is removed, so an interruption leaves at worst an untracked entry that expires. Standalone servers keep the MULTI and are unaffected. scripts/redis-cluster-live.cjs drives the provider against a real three-master cluster and treats keyv's error events as failures; on main five of its six checks fail. CI now starts such a cluster and runs it.
4 tasks done
derrickmehaffy
added a commit
that referenced
this pull request
Oct 13, 2026
Brings in the redis provider fixes from #244, #245 and #249. The plugin-redis v2 path is main's as merged: makeClusterSafe, the SREM-before-UNLINK cluster delete, the background tracking-set prune, and keys() scanning every master when useRedisSets is off. The constructor now builds @keyv/redis only when there is no node-redis connection, as this branch did before, so the v3 path never wraps the ioredis shim. The v3 store itself is unchanged here; porting the same fixes to it follows separately so each one can be shown failing first. Until then test:continuity fails its two useRedisSets: false checks, because v2 now enumerates those entries and v3 does not. The lockfile is main's plus this branch's two devDependencies.
derrickmehaffy
added a commit
that referenced
this pull request
Oct 13, 2026
…ecks The merged fixes have live and recording-client checks for the plugin-redis v2 path only. The node-redis store this branch adds has the same three problems, and nothing exercised them: - on a cluster its delete() and unlinkTracked() UNLINK before they SREM, so a purge racing a write leaves a live entry outside the tracking set (#244); - nothing prunes its tracking set, so expired members are listed by keys() and the set grows for as long as the cache runs (#249); - with useRedisSets off, keys() reads the empty tracking set, so pattern purges match nothing (#245). redis-standalone-live and redis-cluster-live now also drive the provider on a real node-redis client (createClient, createCluster), constructed the way init() does on v3. The cluster harness runs every check against both backends. redis-cluster-check's node-redis recorder gains EXISTS, SSCAN, per-master clients and an SREM hook, for the command-order, prune and client-swap assertions. redis-key-continuity now requires keys() to list every entry the other backend wrote, not only to agree with it, and runs against a cluster when REDIS_CLUSTER_NODES is set, which CI's redis_cluster job now does. The new v3 checks fail at this commit; the fixes follow. The prune-race checks also assert that a SREM was issued, since a prune that does nothing passes them otherwise.
derrickmehaffy
added a commit
that referenced
this pull request
Oct 13, 2026
…store #244 changed the plugin-redis v2 path to SREM before UNLINK on a cluster, where a delete is two commands rather than one MULTI. NodeRedisStore still unlinked first, in both delete() and unlinkTracked(), so a purge racing a response being cached for the same key could run UNLINK (purge) -> SET, SADD (write) -> SREM (purge) and leave a live entry outside the tracking set. keys() cannot list such an entry, so no purge reaches it and it is served stale until its maxAge. Against a real three-node cluster, the race checks left 16 and 22 such entries before this change and none after. The trade-off is the one makeClusterSafe documents: a failure between the two commands now leaves an untracked entry until its maxAge, where it used to leave only a dead member.
2 of 7 tasks
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.
What changed and why
On a Redis Cluster, the redis provider on plugin-redis v2 cached nothing, and nothing reported it.
@keyv/redis3 writes an entry and adds it to thenamespace:keyvtracking set in a single MULTI. Deletes work the same way. The entry and the set are two keys in different hash slots, and a cluster refuses that:Keyv catches the failure and only emits it as an
errorevent, so:set()resolved and nothing was stored, so every request was a miss;del()failed the same way.The #100 work made purges slot-safe, but it was verified with a recording client (
test:cluster), which can't see a failure inside a dependency. I reproduced this on a real three-master cluster: onmain,set()returns,get()returnsundefined, and no node holds a single key.Fix: when the client reports
isCluster, the store'ssetanddeleteissue the same two commands separately:Nothing else changes: same keys, same commands on standalone, and no new configuration.
Tests
scripts/redis-cluster-live.cjs(pnpm run test:cluster:live) drives the provider against a real cluster: set/get, tracking and lifetime,keys(),del(),delMany()across slots, andclear(). It treats keyv's swallowederrorevents as failures.main, 5 of 6 checks fail.clear()only passes because nothing was ever stored.redis_clusterstarts threeredis:8.10masters on the host network and runs the script. Service containers can't form a cluster.Locally:
test:lintgreen,test:smoke3/3,test:cluster5/5,test:depsok.test:e2e:redis135/135 on standalone.Checklist
test:cluster:live, 5 of 6 red onmain.pnpm run test:lintpasses.test:e2e:redis,test:smoke,test:clusterrun.main.Notes for the reviewer
test:cluster:liveto cover it too.