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>
160 lines
10 KiB
Markdown
160 lines
10 KiB
Markdown
# 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:
|
||
|
||
1. A Redis ACL file accepts neither comments nor line continuations — the original files did not load
|
||
at all, and the server refused to start.
|
||
2. The advanced account granted `SMEMBERS` and `SORT`, both `RAW_ONLY` and therefore the raw gateway
|
||
account's alone.
|
||
3. The ordinary account granted `SORT_RO` for the same reason.
|
||
4. The ordinary account could not run `PUBLISH`, `SUBSCRIBE`, or `PING`, all classified `TYPED`.
|
||
5. The ordinary account could not run `MULTI`, `EXEC`, `UNWATCH`, or `DISCARD`, also `TYPED`.
|
||
|
||
6. The admin account was missing twelve read-only diagnostics the catalog exposes — the `OBJECT`,
|
||
`PUBSUB`, `XINFO`, `FUNCTION LIST`/`STATS`, and `CLUSTER KEYSLOT` subcommands.
|
||
|
||
7. The cursor-scan reply budget was sized to the requested `COUNT`, which Redis treats as a hint —
|
||
a real `HSCAN COUNT 500` came 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.503` and did not demote the old primary until `05:56:23.529` — eleven seconds in which
|
||
> the client, still connected, wrote and was told `+OK` **2,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.
|