Repository navigation
feat: support Valkey/Redis Cluster - #128
Open
tomvardasca wants to merge 11 commits into
Open
tomvardasca wants to merge 11 commits into
tomvardasca wants to merge 11 commits into
Conversation
`pipeline.exec()` resolves even when its commands fail, and the write stream's final callback was never called on error, so a failed write was silently treated as stored. Check the pipeline results and pass the error to the stream callback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- compare Vary headers case-insensitively, and array values by content - return the most specific matching variant instead of the first one - skip and drop entries past their deleteAt - a new entry only replaces the entry for the same variant, instead of whichever entry matched a request without headers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Move-only, no behavior change: - lib/keys.js: key serialization and parsing - lib/entries.js: deletion and scan helpers shared by the store and the manager - lib/vary.js: Vary header matching, used by the tracking cache - lib/redis-cache-store.js: RedisCacheStore - lib/redis-cache-manager.js: RedisCacheManager The public exports of index.js are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
get() scanned the whole keyspace for `metadata:{origin}:{path}:{method}:*`
and then read every match, so each lookup cost O(database size) round
trips. With 100k unrelated keys a lookup took seconds.
Each URL now has an index hash, `index:{urlHash}`, with one field per
method and Vary variant. A lookup is an HGETALL of that index plus a GET
of the selected value, independent of the database size.
- delete(), deleteKeys() and deleteTags() use the index and a per-tag
`tag-index:{tagHash}` set, so they don't scan either. RedisCacheManager
keeps its key layout and its scan-based maintenance operations.
- Entry ids are derived from the URL, method and Vary values, so writing
a variant again overwrites it in place instead of creating a duplicate
that has to be found and deleted later.
- Vary names are normalized to lower case, `Vary: *` is not cached, and
the most specific matching variant wins, as before.
- Index and tag-index keys expire with their longest-lived entry, and
expired variants are dropped from the index on the next write.
- Every key of a URL carries `{urlHash}`, so a tracking invalidation of
any of them drops that URL from the tracking cache. Writes and deletes
also invalidate locally as soon as they are sent, so a lookup in flight
doesn't cache what it read before them. Tracking is re-enabled, and the
local cache cleared, when either connection reconnects; before,
invalidations were silently lost after a reconnect.
- Concurrent writes, deletes and purges of a URL are safe: index and
tag-index keys always get both EXPIRE NX and GT (a concurrent first
write or delete can't leave them short-lived or without expiration),
tag-index members identify a single write so a slow purge can't remove
the membership of a rewrite, and an expired variant is removed from the
index only if it wasn't rewritten in the meantime.
- close() waits for in-flight writes instead of sleeping.
- New `client` option: use an existing iovalkey client, which close()
does not quit. The tests use it to delay chosen commands and force the
interleavings of the concurrency bugs above.
lib/keys.js owns the key layout, lib/entries.js every read and write of
an entry's keys, and lib/vary.js Vary normalization.
Requires Redis >= 7.0 (EXPIRE NX/GT) or Valkey >= 7.2, with EVAL allowed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test/multi-process.test.js starts three processes, each with its own store and tracking cache, sharing one server. Writes, rewrites, more specific variants and purges done by one process must reach the tracking cache of the others, and while the hot URLs are rewritten in a loop, no process may return a version older than one that was stored and had time to propagate. With invalidations ignored, every scenario fails. benchmarks/pods.js measures the same setup: throughput, Redis commands per request and how many lookups each process answers from memory, for a range of hit ratios and workloads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
New options:
- `clusterUrl` / `startupNodes` / `clusterOptions` / `mode`: create a
Cluster client, e.g. from an ElastiCache configuration endpoint.
- `client` also accepts an existing Cluster client.
- `keyPrefix`: prefix applied by the store, as an alternative to
`clientOpts.keyPrefix`.
Every key read by a lookup shares the `{urlHash}` hash tag, so lookups
stay on one shard. Writes are sent as individual auto-pipelined commands,
which keeps them ordered per slot. Tag invalidation reads per-tag sets on
their own shards and needs no cross-slot command.
Client-side tracking is disabled in cluster mode, because invalidations
are delivered per node.
docker-compose.yml gets a three-master cluster, and `npm run
test:cluster` runs the store against it, also in CI.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Publish URL entries atomically, guard stale deletes, sweep expiry-scored tag indexes, and preserve complete-index Vary selection in tracking. Add regressions for races, partial failures and manager notifications; run CI on feature branch pushes.
Preserve endpoint TLS and credentials on all shards, require master reads, colocate atomic entry keys, and test authenticated TLS, resharding and replica promotion.
Wait for replicated writes before forced planned promotion and give each fixture node its own RDB filename.
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.
What it adds
clusterUrl(for example an AWS ElastiCache configuration endpoint),startupNodes,clusterOptionsandmodecreate a Cluster client.clientalso accepts an existing Cluster client.keyPrefix: the store applies this prefix, as an alternative toclientOpts.keyPrefix.How it works
{urlHash}hash tag, introduced in perf: look up entries through a per-URL index instead of SCAN #127.Tests
docker-compose.ymlgets a three-master cluster in one container, so the node addresses it announces also work from the host.npm run test:clusterruns the store against it, and CI runs it in the integration job.Clusterclient.keyPrefix.Benchmarks
The same workloads as #127, on the three-node cluster (100k unrelated keys spread over the shards, 1 process, 64 concurrent requests, tracking unavailable in cluster mode).
maindoesn't support Cluster, so there is no comparison. All three nodes share one Docker container on one machine, so this shows the cost per request, not how a real cluster scales.The Redis commands per request match the standalone ones in #127: a lookup stays at two commands, on one shard.
🤖 Generated with Claude Code