Repository navigation
feat(provider-redis): move to plugin-redis v3 and node-redis - #239
Draft
derrickmehaffy wants to merge 9 commits into
Draft
derrickmehaffy wants to merge 9 commits into
derrickmehaffy wants to merge 9 commits into
Conversation
Placeholder commit for a draft PR. Code lands once a plugin-redis 3.0.0 beta is published.
derrickmehaffy
force-pushed
the
dev/plugin-redis-v3
branch
from
October 12, 2026 21:52
6e6e70b to
83eea2d
Compare
6 tasks
derrickmehaffy
added a commit
that referenced
this pull request
Oct 12, 2026
@keyv/redis 4 and 5 already run on @redis/client and peer on keyv ^5, so the provider's move to node-redis (#239) can land without keyv 6. The previous comment implied the two had to move together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 of 2 tasks
plugin-redis v3 moves to node-redis and keeps an ioredis-compatible `.client` only as a deprecated shim, removed in v4. This lets the provider use `connections[name].native` when v3 is installed, while plugin-redis v2 keeps the existing @keyv/redis 3 path untouched - so nobody on v2, or on Node 20, which v3 does not support, sees a change. v3 is detected by capability: its `connection` service has `whenReady`, which the provider now waits on instead of polling the client, and whose rejection is reported as an init failure rather than a hang. On v3 the cache runs on NodeRedisStore, a keyv store that writes exactly what @keyv/redis 3 writes: the `keyv:` keys keyv prefixes, PX lifetimes in milliseconds, the `namespace:keyv` tracking set, and the `sets:` naming used when redis sets are off. Entries cached before an upgrade therefore stay readable and purgeable after it, and after a rollback. @keyv/redis 5 was not usable: it keeps no tracking set, separates keys with "::", and destroys the client it is handed when a connection fails, which would take a shared plugin connection away from every other consumer. The store never closes the client. On a cluster it never issues a multi-key command or a MULTI spanning hash slots (#100): entries are written and tracked in two commands, read one by one, and unlinked key by key, with SREM against the single tracking set still batched. Tests: - scripts/redis-key-continuity.cjs writes with one backend and reads, lists, purges and clears with the other against a real Redis, and compares the raw keys, types and lifetimes each leaves. It fails if the tracking set is renamed or a lifetime is converted. Runs in e2e_redis. - redis-cluster-check covers the v3 store with a recording node-redis client; each check fails when cluster detection is disabled. - provider-smoke drives init() against v2 and v3 stand-ins. Also declares @strapi-community/plugin-redis as a peer (^2 || ^3, prereleases included); the provider has always required it at runtime.
derrickmehaffy
added a commit
that referenced
this pull request
Oct 12, 2026
* chore(deps): do not offer keyv majors until cache-manager supports them The production group bumped keyv to 6.1.0 on its own. Both providers pass their Keyv stores to cache-manager, which still depends on keyv ^5.6 in its latest release, and @keyv/redis 6 - already held back for its own reasons - is the release that peers on keyv 6. Alone, the bump broke every redis purge (#234). keyv has to move together with cache-manager and @keyv/redis, as one deliberate migration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(deps): do not offer mermaid majors; explain the real keyv port cost mermaid 12 clips the second line of our flowchart labels and is outside vitepress-plugin-mermaid's declared peer range. No CI job renders the docs, so #231 was green while visibly breaking them. The keyv comment said keyv 6 "no longer populates" opts.store; it has no opts at all. More importantly it implied the port was an accessor fix. It is not: keyv 6 bridges @keyv/redis 3 and drops the default namespace, which would turn purges into silent no-ops, and the @keyv/redis that supports it moves to @redis/client while the redis plugin is being rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(deps): note that the node-redis move does not wait on keyv 6 @keyv/redis 4 and 5 already run on @redis/client and peer on keyv ^5, so the provider's move to node-redis (#239) can land without keyv 6. The previous comment implied the two had to move together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
5 tasks done
plugin-redis v3 replaces the node-redis client of a cluster or sentinel connection when it has to reconnect from scratch, destroying the old one; `connection.native` is a getter for whichever client is current. The store kept the client it was given at bootstrap, so after a swap every cache operation went to a destroyed client. The store now takes the client or a function returning the current one, and init passes `() => connection.native`. A smoke check swaps the client behind the getter and fails if the store or `ready` still see the old one.
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.
…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.
…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.
With useRedisSets: false there is no tracking set: entries are named `sets:namespace:keyv:keyv:<key>`, as @keyv/redis 3 names them. #245 made the v2 path's keys() find them by that name, on every master of a cluster. The v3 store's keys() still read the empty tracking set, so every pattern purge matched nothing and entries outlived it until maxAge - and after #245, v2 and v3 listed different keys for the same data, which test:continuity caught. NodeRedisStore.namedKeys() SCANs for the `sets:` names. SCAN only sees the node it is sent to, so on a cluster it walks `masters` through nodeClient(), the @redis/client 5+ API, and refuses rather than scan one node when a client lacks them. clear() uses it too, unlinking key by key on a cluster instead of a multi-key UNLINK, and SCAN instead of KEYS, which blocks the server for the whole keyspace.
#249 added a background prune of the tracking set: an entry that expires through its PX lifetime is removed by Redis, but its member stays, so keys() listed expired entries and the set grew for as long as the cache ran. The prune only knew @keyv/redis on ioredis, so on plugin-redis v3 it never ran, and prune() returned without doing anything. The prune's logic - SSCAN in batches, EXISTS per member, SREM the dead, re-check and re-add, fail safe, rate limited from keys() and set() - stays in one place and now drives a small TrackingSet adapter. The v2 adapter issues exactly the commands it issued before. NodeRedisStore.trackingSet() provides the v3 one: - EXISTS per key, issued together, which node-redis sends as one pipeline on a standalone server and routes key by key on a cluster; - a failed EXISTS counts as live, so a check that cannot be made never untracks an entry; - every command resolves the client as it is sent. A pass can take seconds on a large set, and plugin-redis v3 replaces a cluster or sentinel client that reconnects from scratch, destroying the old one. Seen failing on purpose as well as before the fix: counting a failed EXISTS as dead fails the two re-check checks, and resolving the client once per pass fails the client-swap check.
The guide still said that with useRedisSets off the provider falls back to a SCAN that misses entries on a cluster. Since #245 it SCANs every master, so purges are complete; the reason left to keep the tracking set is cost, and the warning now says that. A new section covers plugin-redis v3: how the provider detects it, that it runs on `.native` and follows a client swap, that entries survive an upgrade and a rollback, and that v2 - and with it Node 20 - is still supported.
This branch has not been deployed
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.
Why
@strapi-community/plugin-redisv3 is a TypeScript rewrite on node-redis (@redis/client6.x) instead of ioredis (plugin-redis#31).connections[x].clientbecomes a deprecated, ioredis-compatible shim, so the published provider keeps working on v3 unchanged. plugin-redis CI gates on that..nativeis the real node-redis client. The shim is removed in v4, so the provider has to move to.nativefirst.What this does
plugin-redis v2: no change. The provider still runs on
@keyv/redis3 overconnections[name].client, with main's code path, keys and polling, including #244, #245 and #249. Node 20 users, whom v3 doesn't support, stay here. The only change on this path is that #249's prune now drives a smallTrackingSetadapter (below); the v2 adapter issues the same commands as before.plugin-redis v3: runs on
.native.Detection: by capability, not version. v3's
connectionservice haswhenReady, and v2 has no such service. The provider initialises in bootstrap, after every plugin's register.Waiting:
whenReady(name)replaces client polling. A rejection becomesCould not initialize REST Cache provider "redis": …instead of a hang.Store:
NodeRedisStore(src/NodeRedisStore.ts), a keyv 5 store that writes exactly what@keyv/redis3 writes:keyv:<key>names keyv prefixes;PXlifetimes in milliseconds, never converted;namespace:keyvtracking set;sets:namespace:keyv:<key>naming used whenuseRedisSets: false.Entries cached before an upgrade stay readable and purgeable after it, and after a rollback.
Client swap: the store resolves
connection.nativefor every command, never once. v3 replaces the client of a cluster or sentinel connection that reconnects from scratch.Shared client: the store never closes it. Its
disconnect()is a no-op.provider.client: keeps v3's ioredis shim, for anyone typed against it. The v3 path never calls it.Cluster (
connections[x].type === 'cluster', Purge Cache - Redis Cluster #100):Tracking-set prune (fix(provider-redis): keep the tracking set in step with live entries #249): the same background, rate-limited pass as v2 (SSCAN batches, EXISTS, SREM the dead, re-check and re-add, fail-safe, at most once per 5 minutes, started from
keys()andset(), plusprune()). The logic lives once inRedisCacheProvider;NodeRedisStore.trackingSet()supplies EXISTS per key (one auto-pipeline on standalone, routed on a cluster), and a failed EXISTS counts as live.useRedisSets: false(fix(provider-redis): let purges find entries when useRedisSets is off #245):keys()SCANs for thesets:names, on every master of a cluster (masters+nodeClient()).clear()uses the same listing and unlinks key by key on a cluster.Why not
@keyv/redis5:::.destroy()s the client it's handed when a connection fails, which would kill plugin-redis's shared connection.@redis/client ^5.10, while plugin-redis v3 uses 6.x.Packaging:
@strapi-community/plugin-redisis now a declared peer,^2.0.0 || ^3.0.0-0(prereleases included). The provider has always required it at runtime. A^3-only range would be breaking.Docs:
docs/guide/providers/redis.mdgains a plugin-redis v3 section (detection,.native, client swap, same data, v2 and Node 20 still supported), and no longer says thatuseRedisSets: falseunder-deletes on a cluster, which #245 fixed.Tests
Each v3 check below was seen failing before the fix it covers, and passing after.
scripts/redis-standalone-live.cjs(test:redis:live): main's v2 checks, plus the same checks on a real node-redis client: listing and purging with and without redis sets,clear(), prune of expired members, prune started by writes, a prune larger than one batch, a write racing the prune (with and without a failing re-check), and delMany/del racing set on jittered connections.scripts/redis-cluster-live.cjs(test:cluster:live, CIredis_cluster): every check runs against both backends on a real cluster; v3 usescreateCluster. Adds a prune larger than one batch andclear()without redis sets. The prune race now also requires that expired members nobody rewrites get untracked, since a prune that does nothing passed it.scripts/redis-cluster-check.cjs(test:cluster): the node-redis recorder gains EXISTS, SSCAN, per-master clients and an SREM hook. New v3 checks: SREM before UNLINK indelMany,delandclear; key-by-key prune; a write racing the prune; a failing re-check; at most one prune per interval; the client resolved for every prune command across a swap; SCAN on every master without redis sets; no multi-keyclear().scripts/redis-key-continuity.cjs(test:continuity): writes with one backend, then reads, lists, purges and clears with the other, in both directions, with and without redis sets.keys()must now list every entry the other backend wrote, not only agree with it. WithREDIS_CLUSTER_NODESset it runs against a cluster, and CI'sredis_clusterjob now does.scripts/provider-smoke.cjs:init()against v3 (standalone, cluster, rejected connection) and v2 stand-ins.Red before the port:
keys()couldn't see after the delMany/del races;The standalone race checks pass before the port, since a standalone server takes each write and delete as one MULTI. Run through the cluster code path on the old code, they failed with 16 and 18 unseen entries.
Also failed on purpose after the port: counting a failed EXISTS as dead broke both re-check checks, and resolving the client once per pass broke the client-swap check.
Local results (Redis 7, standalone and a three-node cluster):
test:smoke8/8,test:cluster31/31,test:redis:live22/22,test:cluster:live28/28.test:continuity10/10 standalone and 10/10 on the cluster.test:e2e:redis248/248 on plugin-redis v2.test:lint,test:deps,test:esmgreen.Before this leaves draft
3.0.0-next.x:e2e_rediswith the playground on v3, andtest:continuitywith the provider created through the realwhenReady().test:cluster:liveandtest:continuityagainst acreateClusterclient; both run in CI'sredis_clusterjob.whenReady(name, { timeoutMs })or rely on v3's default.@strapi-community/plugin-redis/types. That's only safe if the import is type-only, since v2 ships no types.docs/guide/providers/redis.md, and theuseRedisSets: falsecluster warning corrected.redis.md,multi-instance.md):settings.clusteradvice, which never worked, and the strayredlock: {}block.playgrounds/redis: bump to plugin-redis v3, and optionally add a cluster topology toe2e_redis.Related