Skip to content

feat(redis): implement COMMAND (INFO / COUNT / LIST / DOCS / GETKEYS)#607

Open
bootjp wants to merge 3 commits intomainfrom
feat/redis-command-command
Open

feat(redis): implement COMMAND (INFO / COUNT / LIST / DOCS / GETKEYS)#607
bootjp wants to merge 3 commits intomainfrom
feat/redis-command-command

Conversation

@bootjp
Copy link
Copy Markdown
Owner

@bootjp bootjp commented Apr 23, 2026

Motivation

COMMAND is issued at connect time by ~every modern Redis client
(go-redis, redis-py >= 4, ioredis, node-redis) for capability probing
and key-routing inference. elastickv previously rejected it as
unsupported command, inflating the "unknown" error bucket and
breaking client initialization in strict libraries that actually
consult COMMAND INFO output.

Companion to #594 (bounded unsupported-command metric) and #601
(HELLO). After this + HELLO land, those two commands should cover

95% of the previously-"unknown" error traffic.

Subcommand matrix

Implemented:

  • COMMAND — flat array of per-command info (6-element shape)
  • COMMAND COUNT — integer count of routed commands
  • COMMAND LIST — flat array of command names
  • COMMAND INFO [name ...] — per-command info array, nil per unknown
  • COMMAND DOCS [name ...] — minimal map-shaped doc entry per command
  • COMMAND GETKEYS <command> <args> — positional key extraction via
    per-command first_key/last_key/step

Explicitly rejected (returns ERR unsupported):

  • COMMAND GETKEYSANDFLAGS — Redis 7 key-specs shape we do not emit
  • COMMAND LIST FILTERBY MODULE|ACLCAT|PATTERN — elastickv has no
    modules and no ACL categories

Metadata table

Single source of truth: adapter/redis_command_info.go. One row per
routed command holding (arity, flags, first_key, last_key, step).
Adding a new command handler now takes three steps:

  1. Register the handler in RedisServer.route (adapter/redis.go).
  2. Add an argsLen entry (adapter/redis.go).
  3. Add a row to redisCommandTable in redis_command_info.go.

Forgetting step 3 is caught at CI time by
TestCommand_RouteMatchesTable. In production the runtime falls
through to a zero-metadata row and emits a single deduplicated log
warning per missing command name, so the command still appears in
COMMAND output (degraded) rather than vanishing entirely.

Flags follow a three-value taxonomy: readonly / write / admin.
denyoom / pubsub / stale etc. are intentionally not emitted —
no real client consults them for routing decisions.

Wire count, LIST length, and the bare-COMMAND reply are driven off
argsLen (the 1:1 route-keyed set) rather than the table, so the
three subcommands stay mutually consistent even during the brief
window when a new route has been added but the table row is pending.

Test plan

  • COMMAND COUNT returns len(argsLen) and equals len(redisCommandTable)
  • COMMAND (no args) returns an array of that length
  • COMMAND INFO GET — name/arity/flags/positions match spec
  • COMMAND INFO SET — arity=-3, flags contain "write"
  • COMMAND INFO nosuchcommand — nil entry, not an error
  • COMMAND INFO GET NOSUCH SET — 3 entries, middle is nil
  • COMMAND GETKEYS SET foo bar["foo"]
  • COMMAND GETKEYS DEL k1 k2 k3["k1","k2","k3"]
  • redisCommandGetKeys MSET-shape helper test (step=2)
  • COMMAND GETKEYS NOSUCH → error
  • COMMAND LIST — sorted names, length = count
  • COMMAND LIST FILTERBY MODULE foo — rejected
  • COMMAND DOCS GET — 4-element map-shaped entry
  • COMMAND BADSUB — "Unknown COMMAND subcommand"
  • COMMAND GETKEYSANDFLAGS — rejected
  • Route-wiring test confirms COMMAND dispatches to the handler
  • TestCommand_RouteMatchesTable — invariant that every routed
    command has a metadata row
  • golangci-lint run ./adapter/... ./monitoring/... is clean
  • go test -race -run 'TestCommand|TestRedisCommand' ./adapter/... ./monitoring/... passes

Add the Redis `COMMAND` handler (COUNT / LIST / INFO / DOCS / GETKEYS
subcommands) so modern clients (go-redis, redis-py, ioredis) stop
polluting the unsupported-command metric at connect time. Metadata lives
in a single static table in adapter/redis_command_info.go so adding a
new command is a one-line row addition.

- Route `COMMAND` in RedisServer.route and register `-1` in argsLen.
- Extend monitoring.redisCommandSet so the metric shows the real name.
- Return 6-element INFO arrays (name, arity, flags, first/last/step) —
  enough for every client that consumes COMMAND output.
- GETKEYS walks the per-command first_key/last_key/step positionally.
- GETKEYSANDFLAGS and LIST FILTERBY variants are explicitly rejected.
- Routed-but-missing-from-table commands emit a one-shot warning and a
  zero-metadata fallback row so the omission is discoverable in prod;
  TestCommand_RouteMatchesTable is the hard CI gate.
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@bootjp has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 6 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 13 minutes and 6 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89d0a4e7-2e1d-47a0-9f8b-1b6d2063cc58

📥 Commits

Reviewing files that changed from the base of the PR and between 5b4af15 and fb540a2.

📒 Files selected for processing (5)
  • adapter/redis.go
  • adapter/redis_command_info.go
  • adapter/redis_command_test.go
  • adapter/redis_compat_commands.go
  • monitoring/redis.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/redis-command-command

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 046169bd9b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_command_info.go Outdated
Comment on lines +234 to +236
if last < 0 {
// "to end": last arg index is len(argv)-1.
last = len(argv) - 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Interpret negative last-key offsets correctly

redisCommandGetKeys currently collapses every negative LastKey to len(argv)-1, but the table includes commands like BZPOPMIN with LastKey: -2 to exclude the trailing timeout argument. With the current logic, COMMAND GETKEYS BZPOPMIN k1 k2 0 incorrectly returns 0 as a key, which can break client key-routing decisions. Negative LastKey values need to be handled as relative offsets (e.g., len(argv)+LastKey) instead of always meaning “to end.”

Useful? React with 👍 / 👎.

Codex P1: redisCommandGetKeys collapsed every negative LastKey to
"to end" (len(argv)-1), but the command table includes entries like
BZPOPMIN with LastKey=-2 to EXCLUDE the trailing blocking timeout arg.
With the previous logic, COMMAND GETKEYS BZPOPMIN k1 k2 0 returned
0 as a key, which can misdirect client key-routing decisions.

Negative LastKey is now treated as an offset from the end: -1 is the
final arg, -2 is the second-to-last, and so on (len(argv)+last). This
matches the Redis key-specs semantics.

Tests pin the BZPOPMIN case end-to-end via the handler and a helper-
level test for synthetic -2 metadata, so a future change that reverts
to the "every negative means to end" shortcut trips CI.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 24, 2026

Addressed Codex P1 in 9a90b7e:

Interpret negative LastKey correctly in COMMAND GETKEYS.
redisCommandGetKeys previously collapsed every negative LastKey to len(argv)-1 ("to end"), but the command table uses entries like BZPOPMIN: LastKey=-2 to exclude the trailing blocking timeout argument. Under the old logic, COMMAND GETKEYS BZPOPMIN k1 k2 0 returned 0 as a key — which would misroute the command in a key-sharded client.

Negative LastKey is now treated as an offset from the end (len(argv)+last), matching the Redis key-specs semantics. -1 still means the final arg; -2 means second-to-last, and so on.

Two new regression tests pin the fix:

  • TestCommandGetKeys_BZPOPMIN_ExcludesTimeout — end-to-end through the handler.
  • TestRedisCommandGetKeysHelper_NegativeLastKeyOffset — synthetic -2 metadata against the helper directly.

/gemini review
@codex review

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a90b7edd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_compat_commands.go Outdated
Comment on lines +336 to +337
conn.WriteArray(len(requested))
for _, raw := range requested {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return all docs for bare COMMAND DOCS

COMMAND DOCS is defined to return documentation for all commands when no command names are provided, but this implementation sizes the reply from len(requested) and iterates only requested, so COMMAND DOCS (without names) always returns an empty array. That makes the subcommand non-compliant and breaks clients/tools that rely on the default full-docs behavior.

Useful? React with 👍 / 👎.

Comment on lines +342 to +346
conn.WriteArray(docEntryLen)
conn.WriteBulkString("summary")
conn.WriteBulkString("")
conn.WriteBulkString("arguments")
conn.WriteArray(0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit command-name keys in COMMAND DOCS replies

The RESP2 form of COMMAND DOCS is a flattened map where each key is the command name and each value is that command's doc map, but this code writes only the value map fields (summary/arguments) and never writes the command-name key. Clients decoding the response as a map (e.g., command name -> docs) will fail or see malformed data.

Useful? React with 👍 / 👎.

Codex P2 (x2) on COMMAND DOCS wire shape:

1. Bare COMMAND DOCS (no command names) previously returned an empty
   array — the implementation sized the reply from len(requested) and
   iterated only the requested slice. Real Redis returns docs for
   every command in that case (same pattern as bare COMMAND and bare
   COMMAND INFO), which is what redis-cli --docs and capability-
   probing clients rely on. Bare DOCS now iterates the routed set via
   routedRedisCommandMetas().

2. The RESP2 form of COMMAND DOCS is a flattened map: alternating
   command-name keys and 4-element doc-map values. Previously we wrote
   only the value slots (summary / arguments) and never the command-
   name key, so any client decoding the response as a name -> docs
   map saw malformed data. Now each entry is a (name, docs) pair;
   unknown commands emit (name, nil) preserving the flat-map layout.

Updated the existing TestCommandDocs_Get to assert the new shape
(outer array length 2, name bulk, then doc-map). Added
TestCommandDocs_BareReturnsAllDocs pinning the full-set default and
TestCommandDocs_UnknownReturnsNamedNil pinning the (name, nil) shape
for unknown commands.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 24, 2026

Addressed Codex P2 (x2) in fb540a2:

Bare COMMAND DOCS now returns docs for all routed commands. The prior implementation sized the reply from len(requested) and iterated only the requested slice, so bare DOCS returned an empty array — non-compliant with real Redis and with how redis-cli --docs and capability-probing clients consume the reply. Bare DOCS now iterates routedRedisCommandMetas(), matching the bare-COMMAND / bare-COMMAND-INFO pattern. Pinned by TestCommandDocs_BareReturnsAllDocs.

Command-name keys are now emitted in the RESP2 flat-map shape. Redis replies to COMMAND DOCS with alternating (name, doc-map, name, doc-map, ...) pairs; previously we wrote only the doc-map slots, so any client decoding the response as a name -> docs map saw malformed data. Every entry now writes both the name bulk and the doc-map (or (name, nil) for unknown commands to preserve the flat-map layout). Existing TestCommandDocs_Get updated to pin the new shape; new TestCommandDocs_UnknownReturnsNamedNil covers the unknown-command pair.

/gemini review
@codex review

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant