ADR 0018: The Redis layer gets a keyspace engine before more commands
ADR 0018: The Redis layer gets a keyspace engine before more commands
Status: Proposed Date: 2026-10-25 Scope: orbit-rs Deciders:
Context
ADR 0005 and ADR 0006 make Redis RESP a layer targeting Redis 8. The first corpus for it (tests/conformance/redis/, 666 records recorded from Redis 8.10.2 in Docker, idempotent on re-recording, 12 files: strings, keys, lists, hashes, sets, sorted sets, transactions, bitmaps and HyperLogLog, geo, streams, scripting, server) was run against orbit-rs on 2026-10-25: 247 of 666 records (37.1%). About 10 points of that came from one fix (error replies were wrapped as ERR RESP protocol error: ...).
The failures are not scattered command bugs. They come from the structure in orbit/server/src/protocols/resp/:
- There is no keyspace. Each data type is a separate map of “actors” in
SimpleLocalRegistry, soDBSIZEreturns 0 (a stub),FLUSHDBandFLUSHALLdo nothing (a stub, so state leaks between runs),KEYS,SCAN,RANDOMKEY,RENAME,COPY,MOVE,TYPEandEXISTScannot work across types, andSELECThas no database to select. - Expiry,
WRONGTYPE,OBJECT ENCODINGand key-level commands have no single owner; commands are dispatched by a hand-written name whitelist (get_command_category), so many real commands (GETDEL,INCRBYFLOAT,MSETNX,SETRANGE,LCS,BITOP,GEOSEARCH, …) answer “unknown command”. COMMAND,INFO,CONFIG,CLIENT,HELLOare stubs.
Adding commands to this structure repeats the same gaps one command at a time.
Decision
Build one keyspace engine for the Redis layer and move the command families onto it, in the order of the corpus files, with the ratchet rising at each step:
- A
Keyspaceof 16 numbered databases mapping a key to a typed value (string, list, hash, set, sorted set, stream, HyperLogLog, geo as sorted set) with an expiry, owned by one module that implements the key-level commands (DEL,EXISTS,TYPE,KEYS,SCAN,RENAME,COPY,MOVE,EXPIREfamily,FLUSH*,DBSIZE,RANDOMKEY,OBJECT,SELECT,SWAPDB) andWRONGTYPE. - Persistence and replication reuse the store decided in ADR 0004 (RocksDB and the Raft log) rather than a second mechanism. Owner direction, 2026-10-25: the keyspace is persisted as an operation log on that store, so recovery and replication stay in one place.
- Command tables come from the real server’s
COMMAND DOCS(arity, flags, key positions) so arity and unknown-command errors match; the dispatch whitelist is removed. - Families move over one at a time (strings, keys, lists, hashes, sets, sorted sets, transactions, bitmaps and HyperLogLog, geo, streams, scripting, server), each only when its corpus file does not go down. The corpus grows with the move (pub/sub, blocking commands, RESP3, ACL, modules such as JSON, time series, search and bloom, and Redis 8 data types are not yet in it).
Until each step is done the layer stays Experimental (ADR 0015) and documents say “Redis compatibility: N of M records (P%), reference Redis 8.10.2”.
Consequences
- A rewrite of the layer’s internals, not an extension; the old
commands/modules are removed family by family as the new ones pass the same corpus records. - Other protocol layers have the same shape of problem until measured; each gets a corpus first (ADR 0005) and its own record if it needs a structural change.
- Modules are in scope (owner, 2026-10-25): the Redis 8 modules (JSON, time series, search, bloom, vector sets) are implemented in Rust as compatible modules, each measured by its own corpus recorded from the reference modules. ADR 0006’s target for Redis therefore means Redis 8 with those modules.
Canonical wording
Redis RESP: N of M records (P%), measured YYYY-MM-DD against the Redis 8.10.2 corpus. The corpus does not yet cover: pub/sub, blocking commands, RESP3, ACL, modules.
Current: 247 of 666 (37.1%), 2026-10-25.
Verification
make conformance-redis (ratchet, against a running orbit-server); recording from the real server with tests/conformance/redis_conformance.py record must be idempotent.