Snapshot of the in-flight state that already existed, identically, in both this worktree and the main checkout before this session began: the initial HTTP Client platform implementation (previously untracked), the redis-lab removal, and the JPA / object-storage / notification integration work. Kept separate from this session's HTTP Client review response, which lands in the following commit, so the two bodies of work stay reviewable apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
10 KiB
Redis SDK support matrix
This file is a gate, not a summary. RedisSupportMatrixTest parses the tables below and fails when
the SDK grows a package or a capability that is not listed, so a module cannot ship without someone
stating its minimum version, its topology support, and what it does not do.
Design: docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md.
Delivery status and the decisions behind each module: docs/superpowers/plans/2026-08-07-redis-wrapper-typed-api-status.md.
Modules
| Module | Minimum Redis | Topology | Risk exposure | Sync | Reactive | Known limitations |
|---|---|---|---|---|---|---|
api |
7.2 | all | none | n/a | n/a | contract only; no driver types |
api/key |
7.2 | all | none | n/a | n/a | slot tags must be low-cardinality |
api/codec |
7.2 | all | none | n/a | n/a | no Java native serialization |
api/command |
7.2 | all | none | n/a | n/a | permits never widen the ACL account |
api/error |
7.2 | all | none | n/a | n/a | failure metadata carries no key or value |
api/operations |
7.2 | all | none | n/a | n/a | contract only |
api/reactive |
7.2 | all | none | n/a | n/a | Reactor confined to this package |
lettuce |
7.2 | all | R1–R2 | yes | yes | pinned to Lettuce 6.8.2 |
lettuce/codec |
7.2 | all | none | yes | yes | UTF-8 and byte array codecs only |
lettuce/command |
7.2 | all | R1–R2 | yes | yes | policy catalog is the only command authority |
lettuce/connection |
7.2 | all | none | yes | yes | five lanes; blocking work never shares the regular lane |
lettuce/observability |
7.2 | all | none | yes | yes | command family only, never a key |
lettuce/operations |
7.2 | all | R1–R2 | yes | yes | hash field TTL needs 7.4; sharded pub/sub needs 7.0; stream deletion needs 8.2 |
config |
7.2 | all | none | n/a | n/a | permit provenance is HMAC-signed per process |
cluster |
7.2 | cluster | none | n/a | n/a | slot arithmetic only; no redirect following |
programmability |
7.2 | all | R2 | yes | no | transactions never roll back; scripts return one bulk reply; FUNCTION LOAD is admin-plane |
raw |
7.2 | all | R2 | yes | no | RAW_ONLY commands only; movable key specs unapprovable |
admin |
7.2 | all | R3 read-only | yes | no | replies are projected; no destructive command exists |
extensions |
8.0 | all | none | yes | no | shared command runner; every extension declares its key |
extensions/json |
8.0 | all | R1–R2 | yes | no | narrow JSONPath grammar; documents exchanged as text |
extensions/search |
8.0 | all | R2 | yes | no | index names namespaced by the SDK; no drop index |
extensions/timeseries |
8.0 | all | R1–R2 | yes | no | retention mandatory at creation |
extensions/probabilistic |
8.0 | all | R1–R2 | yes | no | every answer is approximate by construction |
Capabilities
| Capability | Minimum Redis | Gate | Bean when absent |
|---|---|---|---|
SHARDED_PUBSUB |
7.0 | probe and catalog minimum | none |
FUNCTIONS |
7.0 | probe and catalog minimum | none |
HASH_FIELD_EXPIRATION |
7.4 | probe and catalog minimum | none |
HASH_FIELD_EXPIRATION_COMBINED |
8.0 | probe and catalog minimum | none |
STREAM_ACKNOWLEDGE_DELETE |
8.2 | probe and catalog minimum | none |
STREAM_NEGATIVE_ACKNOWLEDGE |
8.8 | probe and catalog minimum | none, and no bean exists yet |
JSON |
8.0 | probe is authoritative | none |
SEARCH |
8.0 | probe is authoritative | none |
TIME_SERIES |
8.0 | probe is authoritative | none |
PROBABILISTIC |
8.0 | probe is authoritative | none |
Certified versions
A version is certified by its lane producing evidence, not by the version number being newer. An
evidence claim here must name the test class that produced it; RedisSupportMatrixTest fails the
build on a row that claims anything else, so "verified" cannot be written into this table without a
test behind it.
All three lanes have now run on 7.4. The other declared versions are declared, not certified: nothing in this repository has executed against 7.2 or 8.2.
| Topology | Versions declared | Evidence status |
|---|---|---|
| Standalone | 7.2, 7.4, 8.2 | RedisTopologyContractTest, LiveRedisGuardrailTest on 7.4 |
| Sentinel | 7.4, 8.2 | RedisTopologyContractTest, LiveRedisSentinelPromotionTest on 7.4 |
| Cluster | 7.4, 8.2 | RedisTopologyContractTest, LiveRedisClusterTest on 7.4 |
What the standalone ACL run established
RedisTopologyContractTest runs the four accounts in infra/redis-sdk/acl against a live server and
asserts that each CommandAccess level grants exactly what the command policy catalog says it may
issue. Writing it found five defects that no amount of reading the files would have surfaced:
-
A Redis ACL file accepts neither comments nor line continuations — the original files did not load at all, and the server refused to start.
-
The advanced account granted
SMEMBERSandSORT, bothRAW_ONLYand therefore the raw gateway account's alone. -
The ordinary account granted
SORT_ROfor the same reason. -
The ordinary account could not run
PUBLISH,SUBSCRIBE, orPING, all classifiedTYPED. -
The ordinary account could not run
MULTI,EXEC,UNWATCH, orDISCARD, alsoTYPED. -
The admin account was missing twelve read-only diagnostics the catalog exposes — the
OBJECT,PUBSUB,XINFO,FUNCTION LIST/STATS, andCLUSTER KEYSLOTsubcommands. -
The cursor-scan reply budget was sized to the requested
COUNT, which Redis treats as a hint — a realHSCAN COUNT 500came back with 501 entries and the SDK refused a correct reply.
Points 2 and 3 are the ones that matter: the account is the last enforcement boundary, so an account wider than the catalog silently removes the second control the design relies on.
What the standalone guardrail run established
LiveRedisGuardrailTest wires the real guard, catalog, and typed operations to a live server —
the first time LettuceRedisCommandGateway, the one class that encodes commands, runs under the
SDK's own contracts rather than against the in-memory stand-in. It carries the plan's datasets: a
value at the 1 MiB ceiling, a hundred-thousand-field hash, hundred-thousand-member set and sorted
set, a twenty-thousand-element list, a stream trimmed to 1,000 while twenty thousand entries are
appended, and a five-hundred-command batch.
The assertions are about limits holding, not throughput. A guardrail test that measured absolute speed would fail on a loaded laptop and teach nobody anything.
What the Sentinel promotion run established
LiveRedisSentinelPromotionTest forces one real promotion and asserts several independent claims
about it. Every write carries a token unique to the run, so the list on the promoted primary is a
verbatim record of what happened and each per-call verdict can be checked against it.
It found the most serious defect in this delivery, and it is not in the SDK's code:
A superseded primary keeps acknowledging writes. Sentinel promoted the replica at
05:56:12.503and did not demote the old primary until05:56:23.529— eleven seconds in which the client, still connected, wrote and was told+OK2,086 times. Every one of those writes was discarded when the old primary resynced from the new one. Exactly one command failed. No client-side signal exists for this: the server answered, so the driver, the SDK, and the caller all correctly recorded a success.
SentinelFailoverObserver counts ambiguous writes, and its documentation used to call those "the
ones an operator has to reconcile". That was wrong by three orders of magnitude, and the class now
says so.
What closes the window is on the server, not the client. Re-running the identical promotion with
min-replicas-to-write 1 and min-replicas-max-lag 1 configured cut acknowledged-and-discarded
writes from 2,086 to 1: the orphaned primary refused 2,020 writes with NOREPLICAS, which the
SDK translates to a definite, non-ambiguous failure the caller can act on. Both settings are now in
the lane, and acknowledgedWriteLossIsBounded ties the tolerated loss to the configured lag window,
so removing them makes the count jump by an order of magnitude and fails the test.
That assertion then caught a second version of the same mistake within a day of being written. The
first guarded run passed; the second failed with 2,099 lost writes, because the setting had been
written into the lane's primary service only. The two data nodes swap roles on every failover, so
a guardrail applied to whichever one happens to start as primary stops applying the moment the lane
does the thing it exists to do. Both nodes now take their whole configuration from one definition.
Three consecutive promotions in both directions since: 0, 0, and 1 acknowledged write lost.
The run also found a translator defect. A promotion closed the channel under an in-flight RPUSH
and the driver raised a bare RedisException, which matched no branch and fell through to a generic
failure reported as definitely did not run. Nothing about an unrecognised failure supports that
claim, and a caller who believes it retries a non-idempotent write. The fallback now treats an
unclassified write failure as ambiguous.
What the Cluster run established
LiveRedisClusterTest checks the part of sdk.cluster that is pure client-side arithmetic against
the server that has the last word. The calculator agreed with CLUSTER KEYSLOT on every entry of a
corpus built from the brace rules a hand-written implementation gets wrong — an empty tag {},
foo{}{bar}, foo{{bar}}zap, an unclosed brace, }{, the empty key, and non-ASCII keys — and the
rendered-key invariant holds: the slot the SDK computes from a tag alone equals the slot the server
computes from the whole rendered key.
Cross-slot refusal was checked in both directions, because a guard stricter than the cluster costs
availability for no reason and a looser one sends requests that cannot succeed. The same key pair
the guard refuses is the pair the server answers CROSSSLOT for.
Redirects were observed rather than assumed: a MOVED names the slot the client computed, and a
slot put into a real MIGRATING/IMPORTING state answers ASK for an absent key and TRYAGAIN
for a multi-key request that straddles the migration. The lane restores the slot to STABLE
afterwards, so a run leaves the cluster as it found it.