chore: initialize from backend template 0a6dd0e

This commit is contained in:
DongHyeonka
2026-08-13 20:31:02 +09:00
commit e64e701fe5
3223 changed files with 388401 additions and 0 deletions
+159
View File
@@ -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 | R1R2 | 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 | R1R2 | 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 | R1R2 | 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 | R1R2 | 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 | R1R2 | yes | no | retention mandatory at creation |
| `extensions/probabilistic` | 8.0 | all | R1R2 | 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.