chore: initialize from backend template 0a6dd0e
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user